V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
leeqingshui
V2EX  ›  程序员

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

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

    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 有好处的情况
    liprais
        5
    liprais  
       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   ·   实用小工具   ·   3587 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 00:57 · PVG 08:57 · LAX 16:57 · JFK 19:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.