幻读是什么?有什么问题?
幻读是什么?
场景假设:
1.在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
2.上面 session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。
幻读有什么问题?
1.会破坏之前语句的加锁声明。
2.导致数据在数据内部状态在此刻的一致性问题和数据和日志在逻辑上的一致性问题。如下图:
update 的加锁语义和 select …for update 是一致的,所以这时候加上这条 update 语句也很合理。session A 声明说“要给 d=5 的语句加上锁”,就是为了要更新数据,新加的这条 update 语句就是把它认为加上了锁的这一行的 d 值修改成了 100。现在,我们来分析一下图 3 执行完成后,数据库里会是什么结果。
1.经过 T1 时刻,id=5 这一行变成 (5,5,100),当然这个结果最终是在 T6 时刻正式提交的 ;
2.经过 T2 时刻,id=0 这一行变成 (0,5,5);
3.经过 T4 时刻,表里面多了一行 (1,5,5);
4.其他行跟这个执行序列无关,保持不变。
这样看,这些数据也没啥问题,但是我们再来看看这时候 binlog 里面的内容。T2 时刻,session B 事务提交,写入了两条语句;T4 时刻,session C 事务提交,写入了两条语句;T6 时刻,session A 事务提交,写入了 update t set d=100 where d=5 这条语句。我统一放到一起的话,就是这样的:
1.update t set d=5 where id=0; / (0,0,5) /
2.update t set c=5 where id=0; / (0,5,5) /
3.insert into t values(1,1,5); / (1,1,5) /
4.update t set c=5 where id=1; / (1,5,5) /
5.update t set d=100 where d=5;/所有d=5的行,d改成100/
导致用这个日志不管去备库执行或者克隆一个库,这三行数据都变了,这个问题很严重。
就算我们把所有的记录都加上锁,一旦在某一个时刻,添加了一条所谓符合加锁条件的记录,但是在加锁的时候,添加的这条数据还不存在,会导致这条新数据加不上锁,所以即使把所有的记录都加上锁,也阻止不了插入新的记录。
如何解决幻读?
现在你知道了,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个记录,这就产生了 7 个间隙。
这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。
也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。现在你知道了,数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。但是间隙锁跟我们之前碰到过的锁都不太一样。
比如行锁,分成读锁和写锁。下图就是这两种类型行锁的冲突关系。
也就是说,跟行锁有冲突关系的是“另外一个行锁”。但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。
间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。也就是说,我们的表 t 初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
备注:这篇文章中,如果没有特别说明,我们把间隙锁记为开区间,把 next-key lock 记为前开后闭区间。
你可能会问说,这个 supremum 从哪儿来的呢?这是因为 +∞是开区间。实现上,InnoDB 给每个索引加了一个不存在的最大值 supremum,这样才符合我们前面说的“都是前开后闭区间”。
间隙锁和next-key lock的引入,解决了幻读的问题,但是有没有别的问题呢?
当我开启一个事物sessionA时,我执行select * from t where id=9 for update; 这时我在开启一个事物sessionB,sessionB也执行select * from t where id=9 for update; 和insert into t values(9,9,9);这时sessionB的新增操作会被锁住,因为被sessionA的间隙锁挡住了,进入等待,这时我sessionA也insert into values(9,9,9),这时会被sessionB的间隙锁挡住,产生死锁。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
解决办法:间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。