七. 幻读是什么,幻读有什么问题

什么是幻读

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

  • 在可重复读隔离级别下,普通的查询是快照读,是不会看到 别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
  • 上面session B的修改结果,被session A之后的select语句用“当前读”看到,不能称为“幻读”。幻读仅专指“新插入的行”。

我们新建一张表,插入6条数据:

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);

这个表除了主键id之外,还有一个索引c,初始化语句在表中插入了6行数据。下面的语句中,是怎么加锁的,加的锁又是什么时候释放的。

begin;
select * from t where d = 5 for update;
commit;

上面这个语句会命中d=5的这一行,对应的主键id=5,因此在select语句执行完成后,id=5这一行会加一个写锁,而且由于两阶段锁协议,这个写锁会在执行commit语句的时候释放。除了行锁,还加了间隙锁(5,10)。

在这里插入图片描述

上图中,假如只加了行锁(实际情况并不是如此),Q3会读到id=1这一行,这就是幻读。

上面三个查询都是加了for update,都是当前读。而当前读的规则,就是要能读到所有 已经提交的记录的最新值。

幻读有什么问题

首先是语意上的。session A在T1时刻就声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。而实际上,这个语义被破坏了。
在这里插入图片描述

其次,是数据一致性的问题。这个不一致包括数据库内部状态的不一致和数据和日志在逻辑上的不一致。

如果发生幻读,把主库statement格式的binlog 拿到备库上去执行,主备执行的结果是不一样的。幻读会造成数据和日志在逻辑上的不一致。

如何解决幻读

幻读产生的原因,是 因为行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入了新的锁,也就是间隙锁(Gap Lock)。顾名思义,间隙锁,锁的就是两个值之间的间隙。

比如文章开头的表t,初始化插入了6个记录,这就产生了7个间隙。
在这里插入图片描述

这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7个间隙锁。这样就确保了无法再插入新的记录。也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。

但是间隙锁和行锁的冲突关系不太一样:

  • 跟行锁有冲突关系的是“另一个行锁”。
  • 跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作,间隙锁之间都不存在冲突关系。
    在这里插入图片描述

上图中,session A和session B都加了间隙锁(5,10),它们之间是不冲突的。

间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。也就是说,我们的表t初始化后,如果用select * from t for update要把整个表所有记录锁起来,就形成了7个next-key lock,分别是(-8,0],(0,5],(5,10],…。

间隙锁合next-key lock的引入,帮我们解决了幻读的问题,但同时也带来了一些困扰。间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
在这里插入图片描述

上面这张图中,就显示了间隙锁造成死锁的情况。

1.session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上间隙锁 (5,10);

2.session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此这个语句可以执行成功;

3.session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;

4.session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。

至此,两个 session 进入互相等待状态,形成死锁。当然,InnoDB 的死锁检测马上就发现了这对死锁关系,让 session A 的 insert 语句报错返回了。

总结

上面分析的问题都是在可重复读隔离级别下的,间隙锁是在可重复读隔离级别下才会生效的。所以,如果把隔离级别设置为读提交的话,就没有间隙锁了。同时,要解决可能出现的数据和日志不一致问题,需要把binlog格式设置味row。

  • 12
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值