总结死锁需满足以下条件:
- 2个或者2个以上的并发事务操作
- 并发事务之间存在锁冲突
- 锁冲突关系成环形
GAP锁和Insert的隐式锁,最容易导致死锁,以下分析从这俩典型场景开始。
1. 表结构
建立以下表作为场景验证,id为主键,使用InnoDB,版本是5.7+,隔离级别RR。
CREATE TABLE `trigger` (`id` char(50) NOT NULL,`name` varchar(128) DEFAULT NULL,`cron` varchar(50) DEFAULT '0',`timeout` int(10) DEFAULT '0',`status` int(1) DEFAULT '0',`job_id` char(50) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `index_id` (`id`) USING BTREE,KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2. 死锁场景一
原因分析:行记录不存在时,加记录X锁变成加GAP X锁,GAP X锁可共存,插入意向锁与GAP X冲突导致死锁。
t1时刻:select for update where id=3加锁过程:默认加NextKey锁,但id=3不存在,退化为GAP锁
t2时刻:T2事务同样获取到GAP锁,因为GAP锁是共存锁
t3时刻:INSERT加锁过程:因为其他事务有GAP锁,需要加插入意向锁,等待T2释放
t4时刻:因为其他事务有GAP锁,需要加插入意向锁,等待T1释放,死锁
t3时的锁信息如下(select * from information_schema.innodb_locks
):
GAP X锁,锁的范围是(下一个存在的索引值,上一个存在的索引值) 注意是开区间,id=3在这个范围内,所以当加插入意向锁时会冲突。
锁等待信息如下:
tips:因为做了多次复现操作,事务ID不一定完全一致,能说明问题即可。
show engine innodb status
查看死锁信息。
T1事务在等待插入意向锁,T2拥有一个GAP X锁,同样在等待插入意向锁,检测到死锁,选择了T2进行回滚。
3.死锁场景二
原因分析:插入数据唯一索引冲突时,先获取GAP S锁,GAP S锁可共存,但与记录X锁冲突,插入意向锁与GAP S锁也会冲突,最终导致死锁。
t1时刻:T1 INSERT实际上未加锁,因为无任何冲突
t2时刻:T2出现INSERT唯一索引冲突,会给T1增加一个记录X锁,自身获取GAP S锁时,等待
t3时刻:T3同上获取GAP S锁时阻塞
t4时刻:T1操作回滚,释放记录X锁,T2和T3得到GAP S锁,接着获取插入意向锁,但与其他事务GAP S锁冲突,死锁。
t3时的锁信息如下(select * from information_schema.innodb_locks
):
锁等待信息如下
死锁信息如下
只看到T2和T3,因为T1已经回滚结束。T2和T3都在等待获取插入意向锁,死锁状态。
补充一点innodb status的信息说明:
死锁信息显示
- 记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
- 间隙锁(LOCK_GAP): lock_mode X locks gap before rec
- Next-key 锁(LOCK_ORNIDARY): lock_mode X
- 插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention
💡 有一个例外:如果在 supremum record 上加锁,locks gap before rec
会省略掉,间隙锁会显示成 lock_mode X
,插入意向锁会显示成 lock_mode X insert intention
💡 infimum和supremum是系统生成的纪录,分别为最小和最大纪录值,infimum的下一条是用户纪录中键值最小的纪录,supremum的上一条是用户纪录中键值最大的纪录,通过next_record字段来相连
参考:
MySQL 中的 INSERT 是怎么加锁的?
MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking
MySQL :: MySQL 8.0 Reference Manual :: 15.7.3 Locks Set by Different SQL Statements in InnoDB