对 mysql 唯一索引 delete / insert 产生死锁情况深度分析
阅读本文大约需要 40 分钟。
作为一个后端服务工程师,提到 mysql , 我自认为对它熟悉程度的还算可以:sql优化、索引问题、一般的死锁分析、mysql 基本设计原理 等。
然而这一次的死锁情况没想到此烧脑,是真的烧脑。为了要搞清楚这个死锁为什么会发生,我到处查找资料,默默思考和实验,并且也和同事们进行了交流,最终终于找到了答案。
说明:本篇文章主要探讨了在有唯一索引时,delete / insert 产生的死锁是为什么,以及在有唯一索引的时候,insert 加锁为什么这么复杂。另外,本篇文章的讲解内容直接越过了 innoDB 在执行 sql 时是如何加锁的基本知识。基础加锁知识是我们每一个后端工程师都应该要掌握的,你可以看一下这一篇:
https://blog.csdn.net/qq_29401881/article/details/119918583 .
为什么要写下这篇文章呢?因为我在寻找本文死锁案例的时候,至少找了 10+ 篇文章,而且关于此死锁案例的博客内容有很多都不准确、也有的就是给你一个死锁日志就放那了(一头雾水)、也有的博主自己也没搞清楚。当然只要是原创有内容的文章,都值得点赞,因为他至少告诉了你他的想法,也许没准就能为你提供一个思路。
热身环节
还是先简单提几个问题,先作为热身和测试你本人自身对 mysql innodb 加锁情况的了解程度。部分不知道没关系,最后面我会简单说明,但本次文章的核心不在这。
1、RR 隔离级别有哪些行锁呢?
2、RR 隔离级别 insert 一条数据(只有主键和普通索引)会加锁吗?加什么锁呢?那 RC 隔离级别呢?
3、RR 隔离级别 sql: delete from test_t where a = 1 ; 你能分析出加锁情况吗?( a 是二级索引 )
4、意向锁是什么?
5、插入意向锁又是什么?
6、死锁的根本原因是为啥呢?
7、间隙锁解决什么呢?
8、RC 隔离级别有间隙锁吗?
环境准备
此问题是我们线上出现的问题,为保护公司隐私,这里就模仿死锁的产生。
msyql 版本:5.7.30
存储引擎:InnoDB
隔离级别:READ COMMITTED
建表sql:
-- id 为自增主键,age 是唯一索引。
CREATE TABLE `testlock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`age` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入三条数据。
INSERT INTO `testlock` (`age`) VALUES (8),(60),(100);
死锁案例1(就是我遇到的死锁情况)
时刻 |
session1 |
session2 |
T1 |
begin; DELETE FROM testlock WHERE age = 60; |
|
T2 | begin; DELETE FROM testlock WHERE age = 60; ( blocked ) |
|
T3 | insert into testlock(age) VALUES(60); |
|
T4 | Deadlock found when trying to get lock; try restarting transaction ( mysql检测到了死锁, session 2 被回滚了) |
死锁原因
step 1:
session1 开启事物,删除一条 age = 60 的数据。我们是读已提隔离级别,且又是唯一索引,所以,我们能够分清楚的知道会在此条数据上加上一个 X 记录锁;
step 2:
session2 开启事物,也删除一条 age = 60 的数据。此时我们能够很容易的知道,这条语句也需要对此记录加一个 X 记录锁,X 记录锁和 X 记录锁是不兼容的,所以 session2 此时阻塞;
step 3:
session1 的 insert 数据时,检测到了唯一冲突(即使这条数据被标记了delete),这时 insert 操作就会给此记录加上 S的 next key lock(即临键锁)。由于对同一行数据局上加锁是需要排队的,所以即将要加的 S next key lock 锁就排在了 session2 的 X 锁后面,所以这里就出现了对同一个字段加锁顺序产生了了环状,所以mysql 检测到了死锁。由于回滚 session2 成本最低,所以 session2 被回滚了。
死锁验证
死锁日志
我们执行 show engine innodb status; mysql 就会把最近的一次死锁日志给打印出来。
可以看到完全符合我们的结论。
这里多说一句,最后面的 lock mode S wating ,这个不是要加记录锁,这里的 S 表示 S 的 next key lock.
友情提示:
了解死锁日志问题,可以看一下:
http://blog.itpub.net/22664653/viewspace-2145133/
死锁案例2(作为扩展)
时刻 |
session1 |
session2 |
session3 |
T1 |
begin; DELETE from testlock where age = 60; |
||
T2 |
begin; insert into testlock(age) VALUES(60); ( blocked ) |
||
T3 |
begin; insert into testlock(age) VALUES(60); ( blocked ) |
||
T4 |
commit; | ||
T5 |
Deadlock found |
success |
死锁分析
T1: session1 加 X NOT_GAP lock,成功.【因为删除加 X 记录锁】
T2: session2 加 S 临键锁,等待.【(8,60) 这个间隙是加入锁成功的,在对 60 这个记录加 S 锁时被 session1 阻塞。】
T3: session3 加 S 临键锁,等待.【(8,60) 这个间隙是加入锁成功的,这是因为和 session2 的间隙锁是兼容的。同样在对 60 这个记录加 S 锁时被 session1 阻塞。】
T4: session1 提交事务,这时由于没有了60这一行数据, sesson2 和 session3 加锁范围扩大,都会加一个 (8,100) 的间隙锁。再由于
session2 和 session3 都是插入,插入会生成插入意向锁,由于插入意向锁和间隙锁冲突,所以 session2 的插入意向锁等待 session3 间隙锁,session3 的插入意向锁等待 session2 间隙锁。所以产生了环状加锁,即死锁。
T5: mysql 能自动检测死锁,所以会回滚其中一个事物。
死锁验证
死锁日志
我们这次就讨论这两个死锁案例完,只要能把这2个死锁案例给完全弄懂,那其他的死锁情况就不是问题了。
但是,我相信,你即使看到了我的分析,你如果再稍加思考,可能不明白的东西更多了,没事儿,接下来就对你的疑惑也是有些网上技术博主的疑惑进行解答,同时也是对网上的那些错误内容进行纠正。(至于错误内容你可以搜索关键词:“唯一索引插入产生死锁” 和 “唯一索引删除再插入产生死锁”,对他们的文章仔细看一看,然后你在结合本文内容进行对比,然后你在自己动手还自造场景观察分析。)
疑惑解答
对于死锁案例1的 step1 和 step2 我相信你是看得懂的,针对 step3 你可能存在的疑问或者你想知道的疑问进行解答:
1、没有数据冲突的情况下, insert 数据是加 X 锁吗?
是的。
演示:
2、insert 数据遇到冲突为什么要加 S 锁,为什么不能对记录加 X 锁呢?
因为 insert 语句也是要进行当前读来校验数据唯一性的,因为读是读嘛,自然不需要加 X 锁。
时刻 |
session1 |
session2 |
T1 |
begin; SELECT * from testlock lock in share mode; |
|
T2 | begin; insert into testlock(age) VALUES(60); Duplicate entry '60' for key 'idx_age', Time: 0.001000s |
针对上面这个操作,我们都知道,
T1:session1 会对 8 60 100 这三条记录加 S 记录锁
T2: session2 insert 是需要进行当前读的,要检验数据唯一性,因为 session1 加的是 S 锁,不影响 session2 读取,读取出来校验唯一性时出现冲突,因此失败。试想,如果 session2 要对记录加 X 锁的话,由于与 S 记录锁冲突,所以 session2 就不能快速检测到冲突。
演示:
(对于这个演示,你只需要知道 insert 遇到唯一冲突时,加的是 S 锁即可)
3、在 insert 有唯一键冲突时加的是临键锁吗?
是的。
其实这个问题,你如果认真看疑问2的演示的话,里面是有说明的。
时刻 |
session1 |
session2 |
T1 |
begin; insert into testlock(age) VALUES(85); 会对此条记录加 X 记录锁 |
|
T2 |
begin; insert into testlock(age) VALUES(85); (blocked) 检测到有重复的数据,会加 S 临键锁。 此时,这个会话会加(60,85)的间隙锁,当要对 85 加锁的时候,被 session1 给阻塞了。 |
|
T3 |
insert into testlock(age) VALUES(86); 成功 注意:这个地方,我插入的是 86. |
|
T4 |
insert into testlock(age) VALUES(84); 此条sql成功的同时检测到了死锁,session2 回滚事务. 注意:这个地方,我插入的是 84. |
Deadlock found when trying to get lock; try restarting transaction. |
演示:
分析:
T1: session1 加 X NOT_GAP lock,成功.
T2: session2 加 S 锁,等待。【解释:我们都知道加锁是一行行加锁的,在 (60,85) 这个间隙是加入成功的,对 85 这行记录加锁时等待.】
T3:
case1:session1 insert 86 成功
case2:session1 insert 59 成功
case3:session1 insert 85 失败,唯一键冲突
case4:session1 insert 84 成功,但 session2 不在阻塞了,因为这里 mysql 检测到了死锁,session2 回滚事务。死锁原因:session2 要对 60 这行数据加 S 锁时要等待 session1 ,现在 session1 要插入 84 时,被 session2 的间隙锁 (60,85) 给阻塞了。(插入意向锁和间隙锁不兼容)
4、insert 数据遇到冲突后,为什么非要加 S 的临键锁扩大加锁范围,不能就只加入一个 S 记录锁吗?
不能,那是为了防止并发插入相同数据时,都能插入成功,出现唯一索引上有相同的数据。
此疑问,我就拿案例2进行分析,这个疑问也是众多博主产生的疑问。为了方便你观察,我把案例2的操作步骤粘贴过来。
时刻 |
session1 |
session2 |
session3 |
T1 |
begin; DELETE from testlock where age = 60; |
||
T2 |
begin; insert into testlock(age) VALUES(60); ( blocked ) |
||
T3 |
begin; insert into testlock(age) VALUES(60); ( blocked ) |
||
T4 |
commit; | ||
T5 |
Deadlock found |
success |
反向分析:
T1: session1 加 X NOT_GAP lock,成功.【因为删除加 X 记录锁】
T2: session2 加 S 记录锁 等待
T3: session3 加 S 记录锁 等待
T4: session1 提交事务,此时 session2 和 session3 都会加 S 记录锁成功,由于都是插入,又都会加入插入意向锁,插入意向锁和 S 记录锁是兼容的,所以 session2 和 session3 都插入成功了。此时出现了2行一样的数据,产生了bug。由此,你看当有唯一索引时,insert 数据如果有冲突, 还必须要加临键锁。
好,现在你基本上能够当有唯一索引情况下,遇到 insert 操作产生死锁是啥问题了吧!我也把延伸问题给你讲了一个遍,不仅告诉了你为什么,还进行了实际演示,希望你下来后能够抽时间演示一遍,能让你理解的更透彻和记的更清楚。
问题回顾
(本问只是简单说下,大部分问题都能很轻松的查找到资料)
1、RR 隔离级别有哪些行锁呢?
记录锁(RK)、间隙锁(GK)、插入意向锁(IK)、Next-Key(NK) 等。
2、RR 隔离级别 insert 一条数据(只有主键和普通索引)会加锁吗?加什么锁呢?那 RC 隔离级别呢?
insert 数据的时候会加入插入意向锁;遇到主键冲突时,会加入临键锁;插入成功,会加入 X 记录锁.
另外,你如果有兴趣,可以了解一下 insert 的隐式锁。
3、RR 隔离级别 sql: delete from test_t where a = 1 ; 你能分析出加锁情况吗?( a 是二级索引 )
一般来说,如果我们开启了RR 隔离级别,也是会开启间隙锁机制的。
首先,a 是二级索引,肯定会有间隙锁的存在。假设表字段只有 (id,a),数据有(1,1),( 2,1), (3,5), (4,6)。那么在二级索引树上加锁情况就是 (-∞ , 1] 和 (1 , 3) ;在主键索引树上加锁情况就是:
[1] 和 [2].
4、意向锁是什么?
可以理解为意向锁是为了表示这个表上有正在操作的事物打上的一个标记。意向锁分为排他意向锁和共享意向锁,排他意向锁表示这个表的某个数据上有排他锁,共享意向锁表示这个表的某个数据上有共享锁。
同时意向锁和所有的行锁都是兼容的,意向锁只和表锁不兼容(S 和 S 还是兼容的)。这是因为,当要加入表锁的时候,不需要遍历这个表所有的记录上是否有锁,只需要判断这个意向锁标记即可。
5、插入意向锁又是什么?
插入意向锁是一种特殊的间隙锁,它是为了提高并发的存在。插入意向锁和行锁是兼容的,插入意向锁和插入意向锁也是兼容的。
6、死锁的根本原因是为啥呢?
死锁的根本原因就是因为多个事物的加锁顺序出现了环状。
7、间隙锁解决什么呢?
间隙锁主要是为了防止幻读。但读了本篇文章你应该了解到了,insert 数据存在冲突时(即使冲突的数据打上了删除标记),不管RC 还是 RR隔离级别,此时也会有间隙锁,即间隙锁还主要解决唯一索引的冲突问题。
8、RC 隔离级别有间隙锁吗?
就是问题7。
任务作业
在死锁案例2分析时,我说了加锁范围会扩大到 (8,100) ,针对这个,我并没有演示,希望你能够自己演示一下,推翻我的结论。
(完)
感谢您的阅读!如果你觉得本篇文章对你提供了帮助,点个赞,就是对大师弟最大的鼓励与支持!