Mysql(10) 幻读
幻读
1 | CREATE TABLE `t` ( |
这个语句会命中 d=5 的这一行,对应的主键 id=5,因此在 select 语句 执行完成后,id=5 这一行会加一个写锁,而且由于两阶段锁协议,这个写锁会在执行 commit 语句的时候释放。
由于字段 d 上没有索引,因此这条查询语句会做全表扫描。那么,其他被扫描到的,但是不满足条件的 5 行记录上,会不会被加锁呢?会
幻读是什么
如果只在 id=5 这一行加锁,而其他行的不加锁的话,会怎么 样。
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 语句用“当前读”看到,不能 称为幻读。幻读仅专指“新插入的行”。 如果只从事务可见性规则来 分析的话,上面这三条 SQL 语句的返回结果都没有问题。
因为这三个查询都是加了 for update,都是当前读。而当前读的规则,就是要能读到所有 已经提交的记录的最新值。并且,session B 和 sessionC 的两条语句,执行后就会提交, 所以 Q2 和 Q3 就是应该看到这两个事务的操作效果,而且也看到了,这跟事务的可见性规 则并不矛盾。
幻读的问题
首先是语义上的。session A 在 T1 时刻就声明了,“我要把所有 d=5 的行锁住,不准别 的事务进行读写操作”。而实际上,这个语义被破坏了。
再往 session B 和 session C 里面分别加一条 SQL 语句,再看看会出现什么现象。
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 的加锁声明。
数据一致性问题
锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态 在此刻的一致性,还包含了数据和日志在逻辑上的一致性。如果把binlog中的日志拿去恢复,会发现数据是不一致的。就算把所有记录都加上锁,还是会出现这个问题,原因就是在我们给所有行加锁的时候,id=1 这一行还不存在,不存在也就 加不上锁。也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录,这也是为什么“幻读”会被单独拿出来解决的原因。
如何解决
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的 是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙 锁 (Gap Lock)。顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个 记录,这就产生了 7 个间隙。
顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个 记录,这就产生了 7 个间隙。 图 5 表 t 主键索引上的行锁和间隙锁
这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也 加上了间隙锁。
数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。 但是间隙锁跟我们之前碰到过的锁都不太一样。
行锁分为了读锁和写锁,两种类型的冲突关系如下:读锁和读锁兼容,写锁和读锁冲突,写锁和写锁也冲突。也就是和行锁有冲突的是另一个行锁。
跟间隙锁存在冲突关系的,是往这个间隙中插入一个记录这个操作,间隙锁之间不存在冲突关系。
间隙锁和行锁合称 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的引入,帮我们解决了幻读的问题,也带来了一些困扰。
间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交 的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。这,也是现在不少公司使用的配置组合。
Author: corn1ng
Link: https://corn1ng.github.io/2019/12/21/新版Mysql/Mysql(10) 幻读/
License: 知识共享署名-非商业性使用 4.0 国际许可协议