幻读是什么?
- 先来看以下这个场景
- 可以看到,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讲:幻读是什么,幻读有什么问题?》