mysql 并发insert导致的死锁

477 阅读2分钟

发现问题

最近线上有个服务发现有不少死锁日志,Deadlock found when trying to get lock; try restarting transaction,查看了下相关日志,是两个请求同时插入相同的数据导致的,insert操作会产生死锁确实挺奇怪的,因此这里需要研究下。

分析

我这里把每一步的步骤都列出来,读者可以比较容易的复现。下面的隔离级别都是RR,这里我用的是mysql8.0,但是5.7的版本也是ok的。

构造数据

对线上的表进行脱敏后,可以简化为如下的结构:

CREATE TABLE `t_test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `num` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_num` (`num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

非常简单的一个表,一个自增主键,一个是唯一键的字段。

给表插入一些数据:

insert into t_test(num)values(1);
insert into t_test(num)values(2);
insert into t_test(num)values(3);
insert into t_test(num)values(4);
insert into t_test(num)values(5);

现在表的数据如下:

+----+-----+
| id | num |
+----+-----+
|  1 |   1 |
|  2 |   2 |
|  3 |   3 |
|  4 |   4 |
|  5 |   5 |
+----+-----+

问题复现

image.png 按照上面表格的顺序即可进行并发insert死锁的复现。

加锁分析

下面所有的加锁情况都可以通过select * from performance_schema.data_locks\G来观测,这条命令我试了下只在8.0版本可用🥲。

  • t2时,事务a给记录8添加了隐式锁,所谓的隐式锁就是不加锁,只是在记录上添加了一个事务id,这样之后的事务若与这条记录冲突,可以对锁进行升级。网上很多资料写insert时加了插入意向锁,其实是不太准确的,mysql只有在有冲突的时候才会去尝试加这个锁,没有冲突就加隐式锁。
  • t3时,事务b发现记录8已经加了隐式锁,因此事务b把原先事务a加的隐式锁升级成X型record锁(不是事务a升级的),然后给自己加一个S型的next key锁。由于记录8已经加了X record锁,因此这个锁需要等待事务a的锁的释放。
  • ⚠️这里有个很重要的一点,t3时事务b的next key锁是由两部分组成的,即gap锁+record锁,gap锁的范围是(5,8),record锁是8,等待住是因为record锁hold住了,但是gap锁此时是加成功的。
  • t4时,事务a插入记录7,一开始是只想加隐式锁,但是事务b拥有(5,8)的gap锁,因此事务a就要拿到insert intention锁(插入意向锁),而insert intention锁与gap锁是冲突的,需要等待事务b释放锁,因此就造成了死锁。

解决问题

解决这个问题很简单,我们之前是先插入8再插入7的,只要改成先插入7再插入8就行,下面是分析:

image.png

Reference