间隙锁和next-key lock——幻读是什么,幻读有什么问题?

目录

幻读是什么?

幻读有什么问题?

(1)语义上的问题

(2)数据一致性问题

如何解决幻读?


我们在举例加锁读的时候,用的是这个语句,select * from t where id=1 lock in share mode。由于 id 上有索引,所以可以直接定位到 id=1 这一行,因此读锁也是只加在了这一行上。

表结构:

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

但如果是下面的 SQL 语句,

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

这个语句序列是怎么加锁的呢?加的锁又是什么时候释放呢?

说明:一下分析设定在可重复读隔离级别下

幻读是什么?

如果只在 id=5 这一行加锁,而其他行的不加锁的话,会怎么样。

这三个查询都是加了 for update,都是当前读。而当前读的规则,就是要能读到所有已经提交的记录的最新值。并且,session B 和 sessionC 的两条语句,执行后就会提交,所以 Q2 和 Q3 就是应该看到这两个事务的操作效果。

但是,这里有什么问题呢?

幻读有什么问题?

(1)语义上的问题

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

 假设只在 id=5 这一行加行锁 -- 语义被破坏:

由于在 T1 时刻,session A 还只是给 id=5 这一行加了行锁, 并没有给 id=0 这行加上锁。因此,session B 在 T2 时刻,是可以执行这两条 update 语句的。这样,就破坏了 session A 里 Q1 语句要锁住所有 d=5 的行的加锁声明。

session C 也是一样的道理,对 id=1 这一行的修改,也是破坏了 Q1 的加锁声明。

(2)数据一致性问题

不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。

假设只在 id=5 这一行加行锁 -- 数据一致性问题:

上述语句的实际执行过程是怎么样的呢

也就是说,本来只是更新id=5这一行的数据d为100,但是这三行的结果,都变成了 (0,5,100)、(1,5,100) 和 (5,5,100)。也就是说,id=0 和 id=1 这两行,发生了数据不一致。这个问题很严重,是不行的。

这个数据不一致到底是怎么引入的?

这是我们假设“select * from t where d=5 for update 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁”导致的。所以我们认为,上面的设定不合理,要改。

假设扫描到的行都被加上了行锁,上述语句的实际执行过程是怎么样的呢

由于在扫描过程中会扫描到id=0的行,被锁住,所以在sessionA commit之前,sessionB会被阻塞

可以看到,按照日志顺序执行,id=0 这一行的最终结果也是 (0,5,5)。所以,id=0 这一行的问题解决了。但同时你也可以看到,id=1 这一行,在数据库里面的结果是 (1,5,5),而根据 binlog 的执行结果是 (1,5,100),也就是说幻读的问题还是没有解决。

原因:在 T3 时刻,我们给所有行加锁的时候,id=1 这一行还不存在,不存在也就加不上锁。

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

如何解决幻读?

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

 表 t 主键索引上的行锁和间隙锁:

当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁,分别是分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。这样就确保了无法再插入新的记录。

我们都知道,读读锁之间不存在冲突,读写锁之间存在冲突,写写锁之间也存在冲突

而跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。

举例:间隙锁之间不互锁

这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。

间隙锁和 next-key lock 的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”——死锁:

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

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

小结:

即使给所有的行都加上行锁,仍然无法解决幻读问题,因此引入了间隙锁的概念。

很多对数据库有一定了解的业务开发人员,他们在设计数据表结构和业务 SQL 语句的时候,对行锁有很准确的认识,但却很少考虑到间隙锁。最后的结果,就是生产库上会经常出现由于间隙锁导致的死锁现象。

发布了114 篇原创文章 · 获赞 21 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_38151401/article/details/104792913