幻读是什么,幻读有什么问题?


幻读是什么?

  • 先来看以下这个场景
    场景
  • 可以看到,session A 里执行了三次查询,分别是 Q1、Q2 和 Q3。它们的 SQL 语句相同,都是 select * from t where d=5 for update。这个语句的意思你应该很清楚了,查所有 d=5 的行,而且使用的是当前读,并且加上写锁。现在,我们来看一下这三条 SQL 语句,分别会返回什么结果。
    • Q1 只返回 id=5 这一行;
    • 在 T2 时刻,session B 把 id=0 这一行的 d 值改成了 5,因此 T3 时刻 Q2 查出来的是 id=0 和 id=5 这两行;
    • 在 T4 时刻,session C 又插入一行(1,1,5),因此 T5 时刻 Q3 查出来的是 id=0、id=1 和 id=5 的这三行。
  • Q3 读到 id=1 这一行的现象,被称为“幻读”。也就是说,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。这里,需要对“幻读”做一个说明:
    • 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
    • 上面 session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。

幻读有什么问题?

语义上的问题

  • 首先以下需要的场景是,“我要把所有 d=5 的行锁住,不准别的事务对 d=5 的数据进行读写操作”
    问题一
  • session B 的第二条语句 update t set c=5 where id=0,语义是“我把 id=0、d=5 这一行的 c 值,改成了 5”。
  • 由于在 T1 时刻,session A 还只是给 id=5 这一行加了行锁, 并没有给 id=0 这行加上锁。因此,session B 在 T2 时刻,是可以执行这两条 update 语句的。这样,就破坏了 session A 里 Q1 语句要锁住所有 d=5 的行的加锁声明。
  • session C 也是一样的道理,对 id=1 这一行的修改,也是破坏了 Q1 的加锁声明(原本声明 d=5 的都需要锁住,实际上这些后来新增或修改的 d=5 的记录都锁不住)。

数据一致性的问题

  • 我们知道,锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
  • 为了说明这个问题,给 session A 在 T1 时刻再加一个更新语句,即:update t set d=100 where d=5。
    问题二
  • update 的加锁语义和 select …for update 是一致的,所以这时候加上这条 update 语句也很合理。session A 声明说“要给 d=5 的语句加上锁”,就是为了要更新数据,新加的这条 update 语句就是把它认为加上了锁的这一行的 d 值修改成了 100
  • 数据库里的结果
    • 经过 T1 时刻,id=5 这一行变成 (5,5,100),当然这个结果最终是在 T6 时刻正式提交的 ;
    • 经过 T2 时刻,id=0 这一行变成 (0,5,5);
    • 经过 T4 时刻,表里面多了一行 (1,5,5);
    • 其他行跟这个执行序列无关,保持不变。
  • 这样看,这些数据也没啥问题,来看看 binlog 里面的内容
    • T2 时刻,session B 事务提交,写入了两条语句;
    • T4 时刻,session C 事务提交,写入了两条语句;
    • T6 时刻,session A 事务提交,写入了 update t set d=100 where d=5 这条语句。
  • 这里就出问题了,这个语句序列,不论是拿到备库去执行,还是以后用 binlog 来克隆一个库,这三行的结果,都变成了 (0,5,100)、(1,5,100) 和 (5,5,100)

如何解决幻读?

  • 现在知道了,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)
  • 间隙锁和之前碰到过的锁不太一样,比如行锁分为读锁和写锁,下图是两种类型锁的冲突关系:
    行锁
  • 但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作,间隙锁之间都不存在冲突关系。
  • 间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间,加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。
    • 原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
    • 原则 2:查找过程中访问到的对象才会加锁。
    • 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
    • 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
    • 1 个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
  • 关于间隙划分有一种理解是这样的,首先索引是有顺序的,相同索引值(比如 d=6)的索引一定是连在一起的,所以间隙锁锁的是第一个 d=6 开始到最后一个 d=6 的索引位置的 next 指针,锁住了的话就无法修改或者插入其他 d=6 的数据,也能解释唯一索引时会退化为行锁,因为唯一索引的 next 指向肯定不是相同值的索引元素,所以无需锁住 next 指针,只要锁住当前行即可。

笔记来源于《极客时间:MySQL实战45讲:幻读是什么,幻读有什么问题?》

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发飙的蜗牛咻咻咻~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值