MySQL InnoDB 幻读问题
幻读是什么?InnoDB 怎么解决的幻读?next-key locking 有哪些注意的地方?
一个例子
T1 时刻,child 表里有2条数据,一个id=90,一个id=102
Time | Session A | Session B | 备注 |
---|---|---|---|
T1 | SELECT * FROM child WHERE id > 100 FOR UPDATE; | 看到id=90 和 102 两条记录 | |
T2 | insert child values (101, x); (Query OK) | 插入成功 id=101 | |
T3 | SELECT * FROM child WHERE id > 100 FOR UPDATE; | 看到了包含101的三条记录 |
-
T1 时刻,Session A 执行 for update 独占锁查询,我们假设 其只锁记录,那么就是锁 id=102 这个索引。
-
T2 时刻,Session B 执行插入,因为id=101 不存在且并没有被加锁,因此插入成功。
-
T3 时刻,Session A 再次执行 for update 读,发现了刚插入的 101 记录。 (或者 当T3 时刻,Session A 同样执行插入101 会发现duplicate key 错误。)
以上Session A 在T3 时刻发生了幻象:T1 时 A 的语义是想锁住 id > 100的行,但因为 101 不存在,而被其他Session B 插入成功,此时 A 再读发现了不存在的行 101,没有被锁住。
什么是幻读
以上这个例子就是所谓幻读的含义:一个事务内,同样的查询语句(当前读),不同时间查询到的结果集(行)不同。(The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. )
幻读这里的结果集不同,并非说某一行被修改了,而是强调的被插入了新的行,并且是当前读。 因为快照读本身就是读一致了(也有另一种说法:快照读保持一致了,但新插入时提示该行存在)。
这里对比下常见的脏读、不可重复读:
term | 含义 | 隔离级别 |
---|---|---|
脏读 | 事务内,看到了其他事务中未commit的数据 | 读未提交 |
不可重复读 | 同一事务内,两次 快照读 结果不同 | 读已提交 |
幻读 | 同一事务内,两次 当前读,看到了新插入的行 | - |
InnoDB 如何解决幻读
通过第一个例子,我们发现 只锁记录(index-row locking)是不行的。
InnoDB 为了阻止幻读,便有了新的算法 next-key locking (index-row locking + gap locking,左开右闭) 即 记录锁 + 间隙锁。
把记录和间隙都锁住。
如上例子,T1 时刻 因为没有id=100的行,因此锁住的索引范围是 (90, 102] + (102, +∞),这个范围锁住后,Session B 的插入就会发生锁等待,从而避免幻读发生。
next-key locking 有什么影响
显然对间隙加锁,会导致锁住更大的范围,这会影响到应用的并发度。
还是上边的例子:虽然条件是 > 100,但因为没有 100的记录,因此这会把 (90, 102] 之间的间隙都锁住,看起来把范围扩大的感觉。
间隙锁的锁冲突
记录锁的读写冲突,我们很熟悉:
锁类型 | 读锁 | 写锁 |
---|---|---|
读锁 | 兼容 | 冲突 |
写锁 | 冲突 | 冲突 |
但间隙锁不同,间隙锁和间隙锁(不包含记录)之间本身不冲突,即使在写锁上依然如此。冲突的是间隙锁和往这个间隙插入记录。明白这个很重要,否则遇到一些问题会感觉莫名其妙。
举个栗子
T1 时刻,child 表里有2条数据,一个id=90,一个id=102
Time | Session A | Session B | 备注 |
---|---|---|---|
T1 | begin; SELECT * FROM child WHERE id = 98 FOR UPDATE; | 锁范围 (90, 102),102 是id主键退化开区间 | |
T2 | begin; SELECT * FROM child WHERE id = 100 FOR UPDATE; | 因为 Session B 同样是 锁范围 (90, 102)。只有间隙锁,没有记录锁。间隙锁不冲突,不会 Blocked | |
T3 | insert child values (98, x); (Blocked) | Session A 插入会阻塞,和 B 冲突,需要等待B 释放锁 | |
T4 | Deadlock,事务重启 | insert child values (100, x); (Query OK) | 互相持有锁,发现死锁。 Session A 回滚,B 成功 |
上边这个值得关注的点在于,间隙锁和间隙锁不冲突【虽然都是 for update写锁,但不包含记录】,因此 T2 会继续运行。
如果where中 id = ** 都改成 id > 100,那么 T2 就会阻塞住,由于锁范围变成了 (90, 102] + (102, +∞),这里就包含了 102 这条记录,因此产生锁冲突会等待。 当然如果把 102这条记录删了, > 100 也不会冲突了。
总结
- 幻读强调的是看到了新插入的行。
- InnoDB 使用 next-key locking (记录锁+间隙锁,左开右闭) 解决幻读问题。
- 间隙锁和间隙锁之间是不冲突的,冲突的是间隙锁和插入记录的动作
参考
- MySQL官方文档-幻读
- 《MySQL 45讲》