[MySQL]-死锁案例-唯一索引上的并发插入
森格 | 2022年12月
本文是对实际work中遇到的死锁问题的复现,其目的是学会去分析死锁日志、还原日志上下文、理解死锁产生原因、MySQL处理机制(回滚事务的选择),最后到死锁的解决方案的提出。
一、死锁是什么
1.1 定义
死锁,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进程称为死锁进程。
1.2 必要条件
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
- 不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放 。
- 循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
二、死锁场景复现
对于死锁的学习,个人认为最好的方式就是自己去验证,通过复现问题分析整个过程,从而提出解决方案。
本案例是由于在唯一索引高并发插入导致的死锁。
2.1 建表
mysql>CREATE TABLE
t
(
id
BIGINT(20) NOT NULL AUTO_INCREMENT,
a
BIGINT(20) NOT NULL,
b
BIGINT(20) NOT NULL,
name
VARCHAR(255) NOT NULL,
PRIMARY KEY (id
),
UNIQUE KEYind_a_b
(a
,b
)
) ENGINE=INNODB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;
结果如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x6BYX05u-1671607367226)(C:\Users\PC\AppData\Roaming\Typora\typora-user-images\image-20221220140332614.png)]
2.2 场景模拟及锁分析
注:事务隔离级别为RC。
2.2.1 场景验证一
事务1插入后回滚,事务2,3一个插入成功,一个产生死锁。
Session A | Session B | SessionC | 备注 | |
---|---|---|---|---|
T1 | set autocommit = 0; | [set autocommit = 0;] | [set autocommit = 0;] | |
T2 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’); Query OK, 1 row affected (0.00 sec) | 执行成功,此A具有隐式锁,因为当前事务A加的锁,不可能产生锁冲突。 | ||
T3 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’);等待 | 产生锁冲突,事务A的锁升为X锁,事务B等待S锁 | ||
T4 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’);等待 | 事务C也等待S锁 | ||
T5 | rollback; | 事务A回滚,X锁释放,事务B和C同时拿到S锁 | ||
T6 | Query OK, 1 row affected (36.41 sec) | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | 事务B,事务C各自持有S锁,当前间隙事务A仍在活跃事务数组m_ids中,所以事务B和C同时想获取插入意向锁X锁,两锁互斥,导致死锁。事务C报死锁错误后释放S锁,另外一个插入成功。 |
2.2.2 补充
1、共享锁、排它锁、意向锁的兼容矩阵如下
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
2、insert的加锁过程
根据官方文档指出
INSERT对插入的行设置独占锁。此锁是索引记录锁,而不是下一个密钥锁(即,没有间隙锁),并且不会阻止其他会话在插入的行之前插入到间隙中。
在插入行之前,将设置一种称为插入意图间隙锁的间隙锁。该锁表示插入的意图,即插入到同一索引间隙中的多个事务如果不在间隙中的同一位置插入,则无需彼此等待。假设存在值为4和7的索引记录。尝试插入值为5和6的单独事务在获得插入行的排他锁之前,都会使用插入意图锁锁定4和7之间的间隙,但不会彼此阻止,因为这些行不冲突。
如果发生重复密钥错误,将设置重复索引记录的共享锁。如果有多个会话试图插入同一行(如果另一个会话已经具有独占锁),则使用共享锁可能会导致死锁。如果另一个会话删除了该行,则可能会发生这种情况。
3、加锁顺序
不知道有没有人对事务B、C的 autocommit 设置有疑问,为什么事务的手动提交的与否都可以看到死锁?
解答:
我们来分析一下,事务A、B、C进行事务等待队列,事务A先对插入记录的X锁(唯一索引),此时事务B、C等待插入记录的S锁(唯一索引);A回滚释放X锁(唯一索引),此时B、C拿到插入记录的S锁,此时两事务都想加IX锁,到这里就会出现死锁了,截止这里我们可以发现,死锁是出现在加唯一索引加锁的过程,还并未到达对主键加锁的阶段,所以事务B、C是否设置自动提交与我们是否能看到死锁并无太大关系。
2.2.3 场景验证二
事务1正常提交,事务2,3插入失败,没有产生死锁。
时间\会话 | Session A | Session B | Session C | 备注 |
---|---|---|---|---|
T1 | set autocommit = 0; | |||
T2 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’); Query OK, 1 row affected (0.00 sec) | 执行成功,此A具有隐式锁,因为当前事务A加的锁,不可能产生锁冲突。 | ||
T3 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’);等待 | 产生锁冲突,事务A的锁升为X锁,事务B等待S锁 | ||
T4 | INSERT INTO t(a,b,NAME) VALUES(7,1,‘name’);等待 | 事务C也等待S锁 | ||
T5 | commit; | 事务A提交,事务B和C获得S锁,当前插入间隙无活跃事务,进行唯一性约束检查 | ||
T6 | ERROR 1062 (23000): Duplicate entry ‘7-1’ for key ‘ind_a_b’ | ERROR 1062 (23000): Duplicate entry ‘7-1’ for key ‘ind_a_b’ | 唯一性约束检查后发现唯一冲突,结束 |
三、事务回滚代价
3.1 查看死锁日志
mysql> show engine innodb status;
3.2 事务信息
在innodb中,有三张表可以帮助我们更好去分析死锁信息:
- information_schema.innodb_trx:事务信息表。
- information_schema.innodb_locks:事务锁的信息表。
- information_schema.innodb_lock_waits:锁等待关系表。
下图是场景复现时所截information_schema.innodb_trx表信息:
3.2 事务优先级
MySQL选择了回滚事务2(trx_id 为31205716),对于MySQL来说,凡事都会考虑一个代价,在解除死锁方面,会选择回滚事务产生影响最小的一个进行回滚。
MySQL8.0.20之前在等待锁的事务优先级排序采取FIFO算法,之后采取CATS算法。该算法通过分配调度权限对等待的事务进行优先级排序,该权重是根据事务阻塞的事务数量计算的。例如,两个事务正在等待同一对象上的锁,那么阻塞最多事务的事务将被分配更大的调度权重,如果权重相等,则优先考虑等待时间最长的事务。
四、解决方案
对于实际场景下的在唯一键值进行高并发插入的死锁,其解决方案之一为:当前事务在执行插入操作之前为其记录申请排它锁
SELECT id FROM t WHERE a=7 AND b=1 FOR UPDATE;
INSERT INTO t(a,b,NAME) VALUES(7,1,'name');
如此,可以防止其他并发事务对该记录加S锁,从而避免拥有S锁再去申请X锁造成死锁。
缺点:
- 增加一条查询语句,在数据库的层面上会增加QPS。
- 但更为重要的是,会使得事务串行等待,增加时间成本。
优点:
- 实现程度上,与其从业务层解决问题,这种方案显得十分简单可行。
- 成本消耗上,从业务层解决这种小概率问题带来的成本增加来说,该方案就显得九两拨千金了。
五、总结
通过对本次死锁的学习,从死锁复现,事务的回滚与提交,MySQL事务优先级,事务锁的分析等等中,更需要自己在处理问题中要:
- 要找到问题的原因所在,解决自己的盲区
- 问题的复现也要做到准确,去寻找技巧
- 问题的解决需要真正做到可行,但同时也需要理清方案的优缺点