持续创作,加速成长!这是我参与「日新计划 10 月更文挑战」的第29天,点击检查活动概况
前语
最近在看 小林coding 的文章,看到一篇《字节面试:加了什么锁,导致死锁的?》,自己也跟着做了做,标题如下图:
其实基础好的友友们,一眼就能看出会产生死锁,不明白的友友们也不要泄气,听我细细剖析;
试验的 MySQL 版本是 8.0.21,;
假如友友们对 MySQL 的锁不太了解,又没有什么好的资源的话,引荐看看小林写的 MySQL 锁篇;
预备
新建一张 t_student
表,只要 id
字段是主键字段,其他都是一般字段;
CREATE TABLE `t_student` (
`id` int NOT NULL,
`no` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
`score` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
填充相关数据,最终,t_student
表的记载如下所示:
开始
在试验开始前,再次声明一下试验环境:
- MySQL 版本:8.0.21
- 隔离级别:可重复读(RR)
- Navicat:12
启动两个业务,按照标题的 SQL 履行次序,进程如下表格:
可以看到,业务 A 和 业务 B 都在履行 insert 句子后,都陷入了等候状况(前提没有打开死锁检测),也便是产生了死锁,由于都在彼此等候对方开释锁。
原因
为什么会产生死锁?
可以经过下面这条 SQL 句子来检查业务履行 SQL 的进程中,到底加了什么锁:
SELECT * FROM performance_schema.data_locks
接下来,我们就一起来剖析一下每一条 SQL 句子履行之后所加的锁;
Time 1 阶段
Time 1 阶段,业务 A 履行以下句子:
# 业务 A
BEGIN
> OK
> 时刻: 0s
update t_student set score = 100 where id = 25
> Affected rows: 0
> 时刻: 0s
然后履行上述的锁查询句子,检查业务 A 此刻加了什么锁,这儿的话,有针对性的选择了几个输出,
SELECT
`ENGINE`, `ENGINE_TRANSACTION_ID`, `LOCK_TYPE`, `LOCK_MODE`, `LOCK_STATUS`, `LOCK_DATA`
FROM
performance_schema.data_locks
从上图可以看到,共加了两个锁,分别是:
- 表锁:X 类型的意向锁;
- 行锁:X 类型的空隙锁;
这儿我们要点重视行锁,图中 LOCK_TYPE
中的 RECORD
表明行级锁,而不是记载锁的意思,经过 LOCK_MODE
可以确认是 next-key 锁,仍是空隙锁,仍是记载锁:
- 假如
LOCK_MODE
为 X,阐明是 next-key 锁; - 假如
LOCK_MODE
为 X, REC_NOT_GAP,阐明是记载锁; - 假如
LOCK_MODE
为 X, GAP,阐明是空隙锁;
因而,此刻业务 A 在主键索引(INDEX_NAME : PRIMARY
)上加的是空隙锁,锁规模是 (20, 30)
。
空隙锁的规模
(20, 30)
,是怎样确定的?
其实这是可以根据经历揣度出来的,假如 LOCK_MODE
是 next-key 锁或许空隙锁,那么 LOCK_DATA
就表明锁的规模最右值,此次的业务 A 的 LOCK_DATA
是 30。
然后锁规模的最左值是 t_student
表中 id 为 30 的上一条记载的 id 值,即 20。因而,空隙锁的规模 (20, 30)
。
这儿的话,我们可以再经过两个小试验来验证一下:
-
在当时
t_student
表中的第一条记载前进行更新:update t_student set score = 21 where id = 10; > Affected rows: 0 > 时刻: 0s
经过对当时记载的锁剖析,可以发现,加的依旧是 X 型空隙锁,一起,
Lock_DATA
的值为15,即空隙锁的规模是(10,15)
: -
在当时
t_student
表中的最终一条记载后进行更新:update t_student set score = 21 where id = 99 > Affected rows: 0 > 时刻: 0s
经过对当时记载的锁剖析,可以发现,只加了一个 X 锁,
LOCK_DATA
的值为supremum pseudo-record
,相当于比索引中所有值都大,但却不存在索引中,也可以视为最终一行之后的空隙锁;For the last interval, the next-key lock locks the gap above the largest value in the index and the “supremum”pseudo-record having a value higher than any value actually in the index. The supremum is not a real index record, so, in effect, this next-key lock locks only the gap following the largest index value.
经过上述的两个小试验,不难发现,一般情况下,LOCK_DATA
的值代表的是右边的规模鸿沟值;
Time 2 阶段
Time 2 阶段,业务 B 履行以下句子:
# 业务 B
BEGIN
> OK
> 时刻: 0s
update t_student set score = 100 where id = 26
> Affected rows: 0
> 时刻: 0s
在履行锁查询句子,来看看业务 B 此刻加了什么锁;
从上图可以看到,行锁是 X 类型的空隙锁,空隙锁的规模是 (20, 30)
。
业务 A 和 业务 B 的空隙锁规模都是相同的,为什么不会抵触?
两个业务的空隙锁之间是彼此兼容的,不会产生抵触。
在 MySQL 官网上还有一段非常要害的描述:
Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.
空隙锁的含义只在于阻止区间被刺进,因而是可以共存的。一个业务获取的空隙锁不会阻止另一个业务获取同一个空隙规模的空隙锁,共享(S型)和排他(X型)的空隙锁是没有差异的,他们彼此不抵触,且功用相同。
Time 3 阶段
Time 3,业务 A 刺进了一条记载:
此刻,业务 A 就陷入了等候状况。
再履行锁查询句子,来看看业务 A 此刻加了什么锁导致了堵塞的产生;
可以看到,业务 A 的状况为等候状况(LOCK_STATUS: WAITING
),由于向业务 B 生成的空隙锁(规模 (20, 30)
)中刺进了一条记载,所以业务 A 的刺进操作生成了一个刺进意向锁(LOCK_MODE:INSERT_INTENTION
)。
刺进意向锁是什么?
注意!刺进意向锁名字里虽然有意向锁这三个字,可是它并不是意向锁,它属于行级锁,是一种特别的空隙锁。
在 MySQL 的官方文档中有以下重要描述:
An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.
这段话表明虽然刺进意向锁是一种特别的空隙锁,但不同于空隙锁的是,该锁只用于并发刺进操作。
假如说空隙锁锁住的是一个区间,那么「刺进意向锁」锁住的便是一个点。因而从这个角度来说,刺进意向锁确实是一种特别的空隙锁。
刺进意向锁与空隙锁的另一个非常重要的差别是:虽然「刺进意向锁」也属于空隙锁,但两个业务却不能在同一时刻内,一个具有空隙锁,另一个具有该空隙区间内的刺进意向锁(当然,刺进意向锁假如不在空隙锁区间内则是可以的)。所以,刺进意向锁和空隙锁之间是抵触的。
别的补充一点,刺进意向锁的生成机遇:
每刺进一条新记载,都需要看一下待刺进记载的下一条记载上是否现已被加了空隙锁,假如已加空隙锁,那 Insert 句子会被堵塞,并生成一个刺进意向锁 。
Time 4 阶段
Time 4,业务 B 刺进了一条记载:
insert into t_student(id, no, name, age,score) value (26, 'S0026', 'ace', 28, 90)
> 1213 - Deadlock found when trying to get lock; try restarting transaction
> 时刻: 0.008s
这儿的话,由于做过优化,所以产生死锁时,直接被检测了出来,并且主动回滚了,有些可能是堵塞等候,进入了死锁状况;
经过锁剖析也可以得知,业务 B 由于回滚,现已开释了空隙锁,之前业务 A 在 Time 3 阶段堵塞等候的 insert 句子也可以成功履行了:
所以为什么会产生死锁呢?
本次案例中,业务 A 和业务 B 在履行完后 update 句子后都持有规模为(20, 30)的空隙锁,而接下来的刺进操作为了获取到刺进意向锁,都在等候对方业务的空隙锁开释,于是就造成了循环等候,满足了死锁的四个条件:互斥、占有且等候、不行强占用、循环等候,因而产生了死锁。
总结
两个业务即便生成的空隙锁的规模是相同的,也不会产生抵触,由于空隙锁意图是为了避免其他业务刺进数据,因而空隙锁与空隙锁之间是彼此兼容的。
在履行刺进句子时,假如刺进的记载在其他业务持有空隙锁规模内,刺进句子就会被堵塞,由于刺进句子在碰到空隙锁时,会生成一个刺进意向锁,然后刺进意向锁和空隙锁之间是互斥的联系。
假如两个业务分别向对方持有的空隙锁规模内刺进一条记载,而刺进操作为了获取到刺进意向锁,都在等候对方业务的空隙锁开释,于是就造成了循环等候,满足了死锁的四个条件:互斥、占有且等候、不行强占用、循环等候,因而产生了死锁。