[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 KEY ind_a_b (a,b)

  ) ENGINE=INNODB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;

  2.2 场景模拟及锁分析

  注:事务隔离级别为RC。

  2.2.1 场景验证一

  事务1插入后回滚,事务2,3一个插入成功,一个产生死锁。

  Session ASession BSessionC备注

  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、共享锁、排它锁、意向锁的兼容矩阵如下

  XIXSIS

  冲突

  冲突

  冲突

  冲突

  IX

  冲突

  兼容

  冲突

  兼容

  冲突

  冲突

  兼容

  兼容

  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 ASession BSession 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表信息:

  在这里插入图片描述

  3.2 事务优先级

  MySQL选择了回滚事务2(trx_id 为31205716),对于MySQL来说,凡事都会考虑一个代价,在解除死锁方面,会选择回滚事务产生影响最小的一个进行回滚。

  这里就要提一下两个概念了,一个是事务的权重(trx_weight),另外一个是事务的调度权重(trx_schedule_weight)。

  对于事务的权重来说,与回滚事务的选择有关。具体与事务undo版本链的长度有关,回滚的undo记录越多,产生的影响就会越大,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锁造成死锁。

  缺点:

  优点:

  五、总结

  通过对本次死锁的学习,从死锁复现,事务的回滚与提交,MySQL事务优先级,事务锁的分析等等中,更需要自己在处理问题中要:

  如果您觉得博主的文章还不错或者有帮助的话,期待您的关注,如果三连点赞评论收藏就更好啦!谢谢各位大佬给予的支持!

最后修改:2024 年 08 月 16 日
如果觉得我的文章对你有用,请随意赞赏