MySQL 只改一条数据我这么难的吗 (二)

MySQL 只改一条数据我这么难的吗(二)
上一篇文章主要说明了在可重复读级别事务级别下,mysql 加锁的规则以及通过实践复现了具体的加锁的操作,但是自认为篇幅过长影响阅读体验,只列举了一些主键索引、非主键索引的等值以及范围内的一些简单场景,这一篇文章让我们来复现其他有趣的场景,并加以分析。如果没看过第一篇的可以先看第一篇,最重要的是要动手实践。如发现文章错误,请指出,感激不尽。

先把上篇实践中的总结规则放上,后面在分析中会用来,这个总结规则来自于实践,源头来自于极客时间。还是开头那一句,本篇的内容都是在可重复读级别事务下进行。

MySQL 只改一条数据我这么难的吗 (二)

next-key lock=行锁+间隙锁

为什么先说这个,因为怕你们误认为 next-key lock (加锁的基本单位) 就是间隙锁,上篇文章其实我也提到过 next-key lock=行锁+间隙锁, 因为上篇文章并没有特别提到过,所以第一个例子就是为了说明这个。
还是和上篇一样的表,插入一样的数据。

MySQL 只改一条数据我这么难的吗 (二)

现在我们来复现一下这个场景看看发生了什么

MySQL 只改一条数据我这么难的吗 (二)

按照上面的图,我们来复现一下。

MySQL 只改一条数据我这么难的吗 (二)

这里,我们来模拟两个事务,事务 A 启动的时候,先加了 next-key lock(5,10],注意是前开后闭,根据优化2,索引上的等值查询,向右遍历直到第一个不符合条件的,这个过程加上 next-key lock(10,15],但是因为是等值查询,并且字段 d 不是唯一索引,所以 next-key lock(10,15] 退化成间隙锁即 (10,15),综合下来,事务 AT1 时刻语句加的锁是 next-key lock(5,10] 和间隙锁 (10,15),事务 BT2 时刻执行编辑操作修改 d=10 这一行,即事务 B 也想在索引 d 上加上 next-key lock(5,10] 锁,此时进入锁等待,然后事务 AT3 时刻插入一条语句,被事务 B 的间隙锁锁住,由于互相被锁,出现了死锁,InnoDB检测到死锁, 让事务 B 回滚了,然后事务 A 得以插入成功。

这里的争议点在于事务 A 的插入为什么会被事务 B 锁住,事务 B 执行的时候,在获取 next-key lock(5,10] 锁的时候,不是在等待事务 A 释放锁嘛?其实整个事务 B 在申请 next-key lock 的时候是有步骤的,这也是为什么在分析加锁的过程都是一步步分析的。首先事务 B 先申请的是 (5,10) 的间隙锁,加锁成功,当它获取 d=10 这条行锁的时候才被锁住了。可能你会疑惑,为什么获取行锁的时候才被锁住,A 事务已经锁住了 (5,10) 的间隙锁,为什么事务 B 能获取到 (5,10) 的间隙锁?

这就要提到间隙锁和我们说的行锁是不一样的。行锁分为读锁和写锁。他们之间的冲突可以拿一张图表示。

MySQL 只改一条数据我这么难的吗 (二)

也就是说和行锁有冲突的一定是另一个行锁。但是间隙锁不一样,和间隙锁有冲突的是往这一个间隙之间插入一条记录。还记得我上篇文章提到的,间隙锁是在可重复读隔离级别下才会生效,它的出现是为了解决幻读的问题。什么是幻读,幻读指的是在一次事务中前后两次查询同一范围数据的时候,后一次查询看到了前一次查询没有看到的行。这不就是我提到的间隙锁的冲突吗。

所以上面的总结就是,间隙锁本身是不存在冲突的。事务 B 获取了 (5,10) 的间隙锁,但是被 d=10 的行锁锁住了。事务 A 由于插入的是 (8,8,8),又被事务 B(5,10) 间隙锁锁住了。导致出现了死锁,随后系统回滚了事务 B ,事务 A 得以执行成功。

limit语句

为了演示这个案例,我们需要增加一条数据。下面是现在的数据。

MySQL 只改一条数据我这么难的吗 (二)

接下来我我们会这样操作。
MySQL 只改一条数据我这么难的吗 (二)

很常规的一个操作,按照我们刚才的分析。事务 A 先加 next-key lock(5,10],然后加上 (10,15) 的间隙锁, 所以事务 B 会被锁住。让我们来验证一下。

MySQL 只改一条数据我这么难的吗 (二)

又被打脸了😂,其实我们是知道这条语句加不加 limit 效果是一样的,因为整个表符合这个条件的就两条数据,但是加锁的范围是不一样的。limit 2 明确指出了只要两条,因此 mysql 走到 (c=10,d=30) 就停下来了,也就是说此时的锁是 next-key lock(5,10] ,此时已满足 limit 2,因此并不会给(10,15) 加上间隙锁,所以事务 B 未被间隙锁锁住。这个故事告诉我们,删除的时候尽量带上 limmit 参数,不仅不需要删库跑路,还可以缩小加锁的范围。

锁等待

我们再来看一个有趣的东西。开始之前我还是把数据还原成开始的六条数据。然后开始两个事务,执行以下操作。
MySQL 只改一条数据我这么难的吗 (二)
你可以试着分析,然后再看下面的结果,可能你会惊讶。

MySQL 只改一条数据我这么难的吗 (二)

事务 A 并不会锁住 id=10 这条记录,条件是 >10, id=10 并不符合条件,所以这条数据不会被锁住。因此事务 BT2 时刻可以删除这条记录,但是事务 BT3 时刻想重新插入这条记录的时候被锁住了,场面一度很尴尬。我们可以通过命令来查看锁的信息。

show engine innodb status

主要看下面这些信息

MySQL 只改一条数据我这么难的吗 (二)

index PRIMARY of table t.t 表示这个语句被锁住是因为表主键上的某个锁。
lock_mode X locks gap before rec insert intention waiting 可以理解为这个插入动作本身。
gap before rec 表示这是一个间隙锁而不是记录锁。

知道了插入是被间隙锁锁住了,但是我们不知道两点 一.为什么被锁住了。二.锁的范围是多少。其实它的规则是这样的,本来在事务 A 的时候只是一个(10,15) 的间隙锁,但是在事务 B 做了删除操作以后,此时不存在 id=10 这条记录,导致间隙锁向左进行了扩展,此时锁的范围就是间隙锁(5,15)。因此,事务 B 的插入语句被锁住。我们可以试试加入 (4,30,30)和(6,30,30) 加以验证。
MySQL 只改一条数据我这么难的吗 (二)

可以看到,id=4 的记录可以正常插入,但是 id=6 会被间隙锁(5,15) 锁住。这里我们得出的总结是,所谓的间隙锁,完完全全是由间隙右边的记录值所决定范围的。

这篇文章写到这也告一段落了。剩下对锁感兴趣的可以再列举其他的一些场景实践并加以分析。我相信这个过程会很有趣的。

抬头,看了下手中的劳力士,此时,十二点有余。想起女朋的贴心提醒,正准备宽衣入睡。猛然惊醒。女友何在?

本作品采用《CC 协议》,转载必须注明作者和本文链接
吴亲库里
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

女友悲观锁死,数据已丢失。。

4年前 评论
Remember (楼主) 4年前

间隙锁向右或向左扩展,只是扩展一行记录,还是可以扩展多行记录?

4年前 评论
Remember (楼主) 4年前

测试了一下第一种情况,发现事务 B 是报出的锁等待超时退出,而你的报出的是死锁退出。是不是因为版本原因,还是其他的原因?

file

mysql 版本
file

事务隔离级别:可重复读
Laravel

4年前 评论
Remember (楼主) 4年前
yxhsea (作者) 4年前
Remember (楼主) 4年前
yxhsea (作者) 4年前
Remember (楼主) 4年前
yxhsea (作者) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
46
粉丝
117
喜欢
493
收藏
604
排名:177
访问:5.5 万
私信
所有博文
社区赞助商