请教下数据库相关的疑问:什么业务场景下需要禁止幻读?什么业务场景下允许幻读? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
leeqingshui
V2EX    程序员

请教下数据库相关的疑问:什么业务场景下需要禁止幻读?什么业务场景下允许幻读?

  •  
  •   leeqingshui 2022-11-10 10:19:23 +08:00 2669 次点击
    这是一个创建于 1068 天前的主题,其中的信息可能已经有所发展或是发生改变。

    MySQL 默认隔离级别时 RR ,通过 MVCC 版本链 + 锁( Gap Lock 、Next-Key Lock )解决幻读问题。

    具体而言,RR 级别解决幻读是:

    • ① 借助 MVCC 版本链,多次 SELECT 下只有第一次读取一次快照,后续复用这个快照
    • ② 借助锁( Gap Lock 、Next-Key Lock ),在 SELECT FOR UPDATE 、插入和更新三张操作下会锁住间隙

    若不是 RR 级别,为 RC 级别存在幻读问题,因为:

    • ① 借助 MVCC 版本链,多次 SELECT 下每次都读取快照
    • ② 不使用( Gap Lock 、Next-Key Lock )

    所以,RC 级别下同一事务下,多次 SELECT 可能读到不同的行数,插入时若相同记录已存在则爆索引冲突,更新时会将其他事务新增的数据同时更新掉(这里描述不知是否正确)。 那么,现在有以下疑问:

    • 多次 SELECT 下出现了不同了行数,即幻影数据行,出现了幻读,出现了不同行数,又有什么问题?什么业务场景下不能忍受这种情况发生?
    • 插入时爆索引冲突有什么问题?更新时将其他数据也更新了又有什么问题?(一般更新时会加条件,也不会更新新增的数据下)

    最终,在使用 MySQL 时,什么下的业务场景下需要避免上述问题呢(设置 RR 级别解决幻读),什么场景下允许上述问题呢?(设置 RC 级别)

    15 条回复    2022-11-11 16:08:23 +08:00
    7911364440
        1
    7911364440  
       2022-11-10 11:40:51 +08:00
    T1 -> `update t set b = 5 where a = 1`
    T2 -> `insert into t(a, b) values(1, 0)`
    T3 -> `T2 commit`
    T4 -> `T1 commit`

    当前数据库: (1,5), (1, 0)

    binlog:
    1. `insert into t(a, b) values(1, 0)`
    2. `update t set b = 5 where a = 1`
    如果把这个 binlog 发送给从库执行,那从库中的数据就变成 (1,5),(1,5)数据就不一致了
    yianing
        2
    yianing  
       2022-11-10 11:55:05 +08:00 via Android
    @7911364440 这种情况 T1 应该更新的行数为 0 ,把 binlog 改成 raw 模式就没问题了吧,没有更新数据就不会发送 binlog
    lmshl
        3
    lmshl  
       2022-11-10 12:15:59 +08:00   1
    mysql 不是有 select for update 吗?
    https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
    还背这些八股文干嘛?
    momocraft
        4
    momocraft  
       2022-11-10 12:26:02 +08:00
    没有遇到过下调到 RC 有好处的情况
    F281M6Dh8DXpD1g2
        5
    F281M6Dh8DXpD1g2  
       2022-11-10 13:12:55 +08:00
    mysql 的 rr 也是不能解幻读问题的
    7911364440
        6
    7911364440  
       2022-11-10 13:38:18 +08:00
    @yianing 不太明白你在说什么,其实出现幻读的原因就是行锁只能锁住当前数据库中的数据,但是插入数据这个操作需要更新记录之间的空隙,间隙锁就是用来解决这个问题的。
    leeqingshui
        7
    leeqingshui  
    OP
       2022-11-10 14:19:08 +08:00   1
    @momocraft 网上查询资料时,看的很多文章说阿里建议将数据库级别设置为 RC ,用来:
    - 提升并发:RC 在加锁的过程中,是不需要添加 Gap Lock 和 Next-Key Lock 的,只对要修改的记录添加行级锁就行了,另外,因为 RC 还支持"半一致读",可以大大的减少了更新语句时行锁的冲突;对于不满足更新条件的记录,可以提前释放锁,提升并发度。
    - 减少死锁:RR 隔离级别会增加 Gap Lock 和 Next-Key Lock ,这就使得锁的粒度变大,那么就会使得死锁的概率增大,而 RC 不用 Gap Lock 和 Next-Key Lock

    有的业务使用应该还是有好处的
    leeqingshui
        8
    leeqingshui  
    OP
       2022-11-10 14:21:48 +08:00
    @7911364440 这是主从复制 binlog 格式设置为 STATEMENT 的问题,row 格式则不会有这个问题,找到了一篇相关介绍的文章:
    https://www.cnblogs.com/fanguangdexiaoyuer/p/11323248.html
    yianing
        9
    yianing  
       2022-11-10 14:50:53 +08:00 via Android
    @7911364440 我以为说的 a 有唯一索引,测试 a 没有唯一索引时,t1 在 update 之后会获取间隙锁,t2 会 block
    7911364440
        10
    7911364440  
       2022-11-10 16:02:39 +08:00
    @leeqingshui 这种情况跟 binlog 的格式没有关系啊,row 格式只能解决 `update|delete ... limit 10`这种问题。

    我上面举的例子是:T1 按照语义来说会给数据库中所有 a=1 的记录加上行锁,但是 T2 新增的数据还没有提交,这就导致 T1 的语义被破坏了 。
    并且因为 T2 是先提交的,所以 binlog 会先记录 T2 再记录 T1 ,从库通过 binlog 同步数据时,就会先执行 T2 (新增数据),再执行 T1 (修改数据),最终就会导致在从库中,T2 新增的数据也会被改掉。
    7911364440
        11
    7911364440  
       2022-11-10 16:06:56 +08:00
    @yianing 有唯一索引不就直接报错了嘛,间隙锁好像只有 RR 隔离级别才有吧,所以 RR 才能解决幻读的。
    leeqingshui
        12
    leeqingshui  
    OP
       2022-11-10 16:11:50 +08:00
    @7911364440 昂昂,居然还有这种操作嘛,周末搭个主从研究下~
    leeqingshui
        13
    leeqingshui  
    OP
       2022-11-11 15:55:14 +08:00
    @liprais 这个有争议,有的认为有,有的没有
    查了些资料: https://www.zhihu.com/question/47007926
    leeqingshui
        14
    leeqingshui  
    OP
       2022-11-11 16:06:13 +08:00
    这是目前我在网上找到的一篇一定程度下解惑的相关性文章: https://www.zhihu.com/question/47007926/answer/2264785785

    原文搬运到此(侵删)

    真实的场景可以注意一下,代码里面,有没有出现连续 2 次 select 的场景。

    那为什么会出现连续 2 次 select 的场景,第二次不能复用第一次的结果吗?
    这个主要是因为写代码的时候,第一次 select 和第二次 select 很可能不在一个方法里面。要复用,就要在第一个方法里面 return 这个结果,再传给第二个方法。有时候觉得这样写太难看了,干脆就查两次算了,反正小业务不用考虑啥性能的。这个也可以认为是用性能换取代码可读性,代码可维护性。( PS:不过个人还是不喜欢这样查两次,可以再想想办法,在不影响代码可读性上查一次)

    另外,讨论幻读和可重复读的问题,很多时候可能都在考虑选 RR 还是 RC 的问题。

    选 RR 还是 RC 首要考虑的是业务逻辑上会不会出错。
    1.一个判断就是有没有两次 select
    2.另外一个就是要注意 RR 虽然能解决可重复读,幻读的问题,但是并不意味着代码就不出错。有点像 java volatile 虽然能保证可见性,但不是说用 volatile 就能解决并发问题这个道理。

    举个例子说:现在 RC 级别,一个业务失败了可以记录表等待重试,也可以直接取消掉。定时任务拉取了现在记录表等待重试的记录,结果后面重试又失败了,原因是被直接取消掉了。心想为啥呢,要是被取消了,定时任务也拉不起来呀,这个原因就是拉起来后,中途马上被别人改掉了,重试的时候冲突发生了错误。

    这个时候可能心想:哦,原来是读到了别人取消的信号。那简单,直接改成 RR ,保持我这次业务逻辑的正确性(定时任务拉取到,就要能修改),如果是这样做,最后数据库这行记录变成了:取消标志位:true ;执行状态:success 。人傻了。。。

    还有一个场景就是,更新余额的时候。像在上上个公司,更新余额的时候,是事务里面,select for update 查出数据,然后减少余额,应用里算出余额的值,最后 update 。这个不管是 RC 还是 RR 级别,都没问题,因为 select for update 锁行了。

    像在上个公司,更新库存的时候,开了 RC 级别事务,更新的时候,代码里没有像 select for update 锁行,而是在更新语句中 update 表 set 现在的库存=原来的库存-要扣的数量。这个也没问题。

    但是如果你数据库是 RR 级别,没有用 select for update 锁住行,是在应用层算出最后新值,然后再直接 update 这个新值。心里想着,那现在总没问题了吧,RR 级别,可重复读,我这个业务的计算可是正确的。但是实际上也是有问题的,会把别人的覆盖掉。

    所以总的来说,RR 和 RC 我一般会先看下有没有两个 select ,也就是明显的幻读,可重复读问题。另一个会看下会不会有可重复读的问题解决了,本次业务能够保证正确性,但是在更大的维度上可能会出错(把别人的覆盖了)

    PS:不过上述的考虑我觉得还是有点偏理论偏学生,站在更加实际,现实的角度:
    1 、小项目的话,直接默认 RR ,因为没有业务,我没理由改 RC 。改 RC 可能还会有潜在的我不知道的错误(因为毕竟不是默认的),前期需要研发速度,需要尽量简单快速落地
    2 、项目变大了,想要 RR 改 RC 。不敢改。项目变大了,稳字优先,稳字当头
    3 、想要利用 RR 改 RC 提升性能,没有其他技术手段了吗,非要改这个?
    4 、没有其他技术手段了,那就改吧
    leeqingshui
        15
    leeqingshui  
    OP
       2022-11-11 16:08:23 +08:00
    @lmshl 不想背八股文,只是想研究下什么业务下用哪个隔离级别更合适~
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     985 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 22:29 PVG 06:29 LAX 15:29 JFK 18:29
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86