幻读简介
1: 定义:
一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
2: 如何出现的?
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
begin;
select from t where d=5 for update;
select from t where d=5 for update;
select from t where d=5 for update;
commit;
3: 带来什么问题?(假设只锁住d=5的行)
a: 首先是语义上的: select from t where d=5 for update;表示我要锁住所有等于d=5的行, 但是如果有其他session把不符合条件的也修改为d=5,就破坏了语义, 或者有session插入d=5的行
b: 其次,是数据一致性的问题 : 锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。说明: sessionA,B,C互不冲突, 各自执行(假设只锁住d=5的行)
数据库内部执行序列: sessionA.update -> sessionB -> sessionC
binlog恢复执行序列: sessionB -> sessionC -> sessionA.update
导致最终的结果的不一致
c: 导致上述两个问题的本质原因: 在假设“select from t where d=5 for update 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁”导致的
4: 应该如何解决?
方法1: 即使把所有的记录都加上锁,还是阻止不了新插入的记录
方法2: 产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
5: 困扰
间隙锁和 next-key lock 的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。
*
- session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上间隙锁 (5,10);
- session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此这个语句 可以执行成功;
- session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;
- session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。
间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。这,也是现在不少公司使用的配置组合。