type
status
date
slug
summary
tags
category
password
1、死锁的发生
MySQL死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些事务都无法继续执行下去。
死锁发生的必要条件:
- 互斥(Mutual Exclusion):资源一次只能被一个事务占用。
- 请求并保持(Hold and Wait):事务在持有资源的同时请求新的资源。
- 不可抢占(No Preemption):已分配给事务的资源不能被其他事务强行夺取,必须由事务主动释放。
- 循环等待(Circular Wait):多个事务形成头尾相接的循环等待关系。
InnoDB 的常见死锁场景:
- 不同事务以不同顺序访问相同的多行数据:
- 事务A先锁定行1,再请求行2
- 事务B先锁定行2,再请求行1
- 两者互相等待形成死锁
- 索引导致的锁升级:当 MySQL 从行锁升级为表锁时可能引发死锁
- GAP 锁冲突:在 REPEATABLE READ 隔离级别下,InnoDB 使用间隙锁防止幻读,两个事务可能在不同间隙上相互阻塞
1.1 不同顺序访问相同的多行数据
例如并发更新不同记录,两个事务以不同顺序访问相同的多行数据。
1.2 GAP 锁冲突
建一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引:
然后,先
t_order
表里现在已经有了 6 条记录:
假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:

可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。
2、为什么会产生死锁
InnoDB 常见的死锁原因:
- 不同事务以不同顺序访问相同的多行数据,每个事务持有某个数据行的锁(该数据行被其他事务访问并尝试获取锁),并尝试获取其他数据行的锁(其他数据行的锁被其他事务持有),导致多个事务互相持有对方需要的锁资源,产生死锁问题。
- 在「可重复读」隔离级别,InnoDB 会使用间隙锁解决幻读问题,两个事务可能在不同间隙上相互阻塞,产生死锁问题。
这里重点介绍间隙锁引发的死锁问题。Innodb 引擎为了解决「可重复读」隔离级别下的幻读问题,引入了间隙锁的概念。间隙锁是属于行锁的一种,行级锁有三种类型:
- Record Lock:记录锁,锁定单条索引项。例如
SELECT * FROM table WHERE id = 1 FOR UPDATE
会对 id=1 的记录加记录锁
- Gap Lock:间隙锁,锁定索引记录之间的间隙,但不包括记录本身,是一个左开右开的区间。例如
SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE
会对 (10, 20) 范围加锁。
- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定索引记录及其前面的间隙,是一个左开右闭的区间。例如
SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE
会对 (10, 20] 范围加锁。
比如,下面事务 A 查询语句会锁住
(2, +∞]
范围的记录,然后期间如果有其他事务在这个锁住的范围插入数据就会被阻塞。
回到前面死锁的例子。

事务 A 在执行下面这条语句的时候:
我们可以通过
select * from performance_schema.data_locks\G;
这条语句,查看事务执行 SQL 过程中加了什么锁。
从上图可以看到,共加了两个锁,分别是:
- 表锁:X 类型的意向锁;
- 行锁:X 类型的间隙锁;
这里我们重点关注行锁,图中 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思,通过 LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁:
- 如果 LOCK_MODE 为
X
,说明是 X 型的 next-key 锁;
- 如果 LOCK_MODE 为
X, REC_NOT_GAP
,说明是 X 型的记录锁;
- 如果 LOCK_MODE 为
X, GAP
,说明是 X 型的间隙锁;
因此,此时事务 A 在二级索引(INDEX_NAME : index_order)上加的是 X 型的 next-key 锁,锁范围是
(1006, +∞]
。next-key 锁的范围 (1006, +∞],是怎么确定的?根据我的经验,如果 LOCK_MODE 是 next-key 锁或者间隙锁,那么 LOCK_DATA 就表示锁的范围最右值,此次的事务 A 的 LOCK_DATA 是 supremum pseudo-record,表示的是 +∞。然后锁范围的最左值是 t_order 表中最后一个记录的 index_order 的值,也就是 1006。因此,next-key 锁的范围 (1006, +∞]。
间隙锁与间隙锁之间是兼容的,所以两个事务中
select ... for update
语句并不会相互阻塞。两个事务都可以成功执行以下 SQL 语句,在执行完之后都持有范围为(1006, +∞]
的 next-key 锁。当事务 B 往事务 A next-key 锁的范围
(1006, +∞]
里插入 id = 1008 的记录时,会在插入间隙上获取插入意向锁,而插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。事务 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.
间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
这里的共同间隙包括两种场景:
- 其一是两个间隙锁的间隙区间完全一样;
- 其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。
但是有一点要注意,next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。
插入意向锁是什么?
注意!插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁。
在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.
这段话表明尽管插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。
插入意向锁的特点:
- 插入意向锁和间隙锁互不兼容:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁。
- 插入意向锁的生成时机:每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),现象就是 Insert 语句会被阻塞。
3、如何解决死锁
- 如果系统已经发生死锁,要解决死锁问题需要破坏上述四个必要条件(互斥、请求与保持、不可抢占、循环等待)中的任意一个,如资源一次性分配、允许抢占、强制释放资源等。在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
- 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数
innodb_lock_wait_timeout
是用来设置超时时间的,默认值时 50 秒。 - 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数
innodb_deadlock_detect
设置为 on,表示开启这个逻辑,默认就开启。
- 如果未发生死锁问题,可以通过算法(如银行家算法)动态判断资源分配是否安全,避免发生死锁问题。
- Author:mcbilla
- URL:http://mcbilla.com/article/bb231f3d-81c2-4e03-9a30-cca23d6da22f
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts