20 | 幻读是什么,幻读有什么问题?

Page content

20 | 幻读是什么,幻读有什么问题?

建表语句

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

什么是幻读

幻度指的是一个事务在前后两次查询同一个范围的时候,后一次的查询看到了前一次没有看到的行 场景

tips

  • 在可重复读隔离级别下,普通的读是快照读,不会读取到其它事务新插入的数据;所以幻读只在“当前读”下发生
  • 幻读专指“新插入的行”,Q2 读取到的 session B 的更新结果,不是幻读

幻读有什么问题

  • 语义问题:事务A 试图对满足条件的所有行加锁(比如列值=5);加锁以后,事务B 新增了一行(列值也=5),但是它不会被锁住;这就导致 A 想要锁住所有列值=5 的行,但事实是新增但那一行还是可以被更新

  • 数据一致性问题:无论是只对要更新行的行上锁,还是把所有扫描的行都上锁,都无法控制新增的数据行的结果

语义上的问题

场景

session A 的 Q1 语句,语义上是要锁住所有 d=5 的行;但是 session C 的操作,它新增了一条 d=5 的行,而这个行却没有被 session A 锁住,session C 对 d=5(id=1) 的这一行还是可以做更改

数据一致性问题

锁的设计就是为了保证数据的一致性,这个一致性不止是数据库内部数据状态的一致性,还包括数据和日志在逻辑上的一致性。

场景 假设 select * from t where d=5 for update 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁

执行结束后数据库里数据的状态

  • T1 时刻, id=5(用主键标识)这一行变成(5,5,100),这一结果最终在 T6 时刻提交
  • T2 时刻,id=0 这一行变成 (0,5,5)
  • T4 时刻,表里面多了一行 (1,5,5)
  • 其他行跟这个执行序列无关,保持不变

执行结束后 binlog 的情况

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/*所有d=5的行,d改成100*/

id=0 和 id=1 这两行,发生了数据不一致。 结论 只给满足查询条件的行加锁,阻止不了新插入的记录和被其它事务更新的记录

场景 假设把扫描过程中遇到的所有行,都加锁

执行结束后 binlog 的情况*

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/*所有d=5的行,d改成100*/

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/]

id=0 这一行的最终结果也是 (0,5,5),id=0 这一行的问题被解决 id=1 这一行,在数据库里面的结果是 (1,5,5),而根据 binlog 的执行结果是 (1,5,100),仍有问题

结论 即使把所有的记录都加上锁,还是阻止不了新插入的记录

如何解决幻读

间隙锁:给所有扫描的行,以及扫描的行之间加锁

间隙锁的冲突情况

读锁 写锁
读锁 兼容 冲突
写锁 冲突 冲突
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作