MySQL InnoDB引擎下,Insert和Delete并发执行时产生死锁是非常常见的场景,这类死锁的本质是Gap锁和插入意向锁的冲突导致的,理解两种锁的特性就能快速定位问题根源。

相关锁的基础概念
Gap锁
Gap锁是InnoDB在可重复读隔离级别下特有的锁类型,用来锁住索引记录之间的间隙,防止其他事务在这个间隙中插入新的记录,避免出现幻读问题。Delete语句执行时,如果命中了范围条件,就会对符合条件的索引记录以及记录之间的间隙加Gap锁。
插入意向锁
插入意向锁是Insert语句执行时生成的一种间隙锁,属于特殊的意向锁,当Insert要插入的间隙已经被其他事务加了Gap锁时,当前事务会生成一个插入意向锁并进入等待状态,直到持有Gap锁的事务提交释放锁之后才能继续插入。
死锁产生的完整过程还原
我们创建一个测试表来模拟死锁场景,表结构如下:
CREATE TABLE test_lock (
id INT PRIMARY KEY,
num INT,
INDEX idx_num (num)
) ENGINE=InnoDB;
-- 插入初始数据
INSERT INTO test_lock VALUES (1, 10), (2, 20), (3, 30);
假设现在有两个事务T1和T2,按照以下顺序执行:
| 执行顺序 | 事务T1 | 事务T2 | 锁状态说明 |
|---|---|---|---|
| 1 | START TRANSACTION; | START TRANSACTION; | 两个事务开启 |
| 2 | DELETE FROM test_lock WHERE num > 15 AND num < 25; | T1对num在20所在的间隙加Gap锁,锁住(10,20)和(20,30)之间的间隙 | |
| 3 | INSERT INTO test_lock VALUES (4, 18); | T2要插入的18在T1锁住的(10,20)间隙中,T2生成插入意向锁并等待T1释放Gap锁 | |
| 4 | INSERT INTO test_lock VALUES (5, 22); | T1要插入的22在T2等待的(20,30)间隙中,T1生成插入意向锁并等待T2释放插入意向锁,此时形成死锁 |
最终MySQL会检测到死锁,回滚其中一个事务,另一个事务执行成功。
死锁排查方法
当生产环境出现这类死锁时,可以通过以下步骤排查:
- 查看MySQL的死锁日志,执行
SHOW ENGINE INNODB STATUS;命令,在输出的日志中找到LATEST DETECTED DEADLOCK部分,里面会记录死锁涉及的事务、执行的SQL语句以及持有的锁和等待的锁信息。 - 根据日志中的锁信息,确认是否是Gap锁和插入意向锁的冲突,定位对应的表和SQL语句。
- 结合业务逻辑,分析事务的执行顺序,还原死锁产生的场景。
规避方案
针对这类Insert和Delete并发的死锁问题,可以采用以下方案规避:
- 缩短事务的执行时间,尽量让事务尽早提交,减少锁的持有时间,降低锁冲突的概率。
- 如果业务允许,可以将隔离级别调整为读已提交,读已提交隔离级别下不会加Gap锁,从根源上避免这类冲突。
- 调整业务逻辑,避免Delete范围操作和Insert操作并发执行操作同一段索引间隙,比如将Delete操作改为基于主键的删除,减少Gap锁的范围。
- 给Insert语句的插入字段添加合理的索引,避免无索引导致的全表Gap锁。
总结
Insert和Delete并发产生的死锁核心是Gap锁和插入意向锁的冲突,理解两种锁的加锁规则就能快速定位问题。实际开发中可以通过查看死锁日志、调整事务逻辑、优化隔离级别等方式规避这类问题,保障数据库并发操作的稳定性。