唯一索引插入引发的死锁问题

MySQL 8.4.0

RR隔离级别

场景复现

分析下面SQL死锁的场景

对于switch表,有主键索引id和唯一索引(uid、type)。

 CREATE TABLE `switch` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for each switch',
  `uid` int NOT NULL COMMENT 'User ID associated with the switch',
  `type` tinyint NOT NULL COMMENT 'Type of the switch',
  `val` tinyint(1) NOT NULL COMMENT 'Value of the switch, TRUE for on and FALSE for off',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uid` (`uid`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Table to store switch information for different users'

业务中有这么一个需求,以事务的方式批量插入固定两个开关;

注意,虽然是批量插入,但顺序是固定的!!!

INSERT INTO switch (uid, type, val) VALUES  (30001,3,1) ON DUPLICATE KEY UPDATE val = VALUES(val);
INSERT INTO switch (uid, type, val) VALUES  (30001,23,0) ON DUPLICATE KEY UPDATE val = VALUES(val);

然后业务上线后一段时间,告警发现事务出现了死锁Deadlock found when trying to get lock; try restarting transaction。

在这里插入图片描述

死锁排查

排除乱序插入;当时以为是事务中批量插入的顺序不一致,但重新梳理了下业务,能确定事务插入顺序都是固定的【23、3】;

INSERT INTO switch (uid, type, val) VALUES  (30001,23,0) ON DUPLICATE KEY UPDATE val = VALUES(val);
INSERT INTO switch (uid, type, val) VALUES  (30001,3,0) ON DUPLICATE KEY UPDATE val = VALUES(val);

唯一索引顺序不一致:后面想到使用了唯一索引,唯一索引的顺序是按照uid、type递增排序的,但我插入的时候是先插入type=23、再插入type=3的记录,因此怀疑是插入的时候,唯一索引上加了间隙锁,锁住了(0~23)的间隙,两个事务都保持间隙锁不释放,就产生了死锁。

定位到问题后,尝试把插入顺序调整为唯一索引的顺序,再尝试,就没有死锁问题了。
在这里插入图片描述

锁分析

导出死锁日志:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-07-13 03:56:02 281472325639936
*** (1) TRANSACTION:
TRANSACTION 6770, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 281472766701312, query id 100 localhost root update
INSERT INTO switch (uid, type, val) VALUES  (30001,23,0) ON DUPLICATE KEY UPDATE val = VALUES(val)

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6770 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6770 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (2) TRANSACTION:
TRANSACTION 6769, ACTIVE 9 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 2
MySQL thread id 10, OS thread handle 281472765583104, query id 101 localhost root update
INSERT INTO switch (uid, type, val) VALUES  (30001,3,0) ON DUPLICATE KEY UPDATE val = VALUES(val)

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6769 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6769 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

使用GPT分析,

  1. 死锁的原因
    • 两个事务都试图在mydatabase.switch表的uid索引上插入或更新具有相同uid(在这里是u1,即十六进制的80007531)的记录。
    • 事务6770(Thread ID 9)正在等待一个它当前也持有的锁(这是一个死锁的经典标志)。
    • 事务6769(Thread ID 10)正在等待一个间隙锁(gap lock)以插入一个新的记录,但同时它也持有一个对已有记录的记录锁(record lock)。
  2. 详细分析
    • 事务6770正在尝试插入一个记录(或更新一个已存在的记录),其uidu1,并且它正在等待一个X(排他)锁来允许这个操作。然而,它已经在等待该锁,因为它也被事务6769持有。
    • 事务6769也在尝试插入或更新同一个uidu1的记录。它已经持有一个对u1记录的X锁,但它现在还需要一个间隙锁(gap lock)来确保在u1之前或之后的间隙中没有其他记录被插入,这样它就可以安全地插入一个新的uidu1的记录(即使它实际上并不打算插入一个新的uid,但由于使用了INSERT ... ON DUPLICATE KEY UPDATE语法,MySQL仍然需要确保间隙的锁)。
    • 因为两个事务都在等待对方释放锁,所以形成了一个死锁。

解决方案

那为什么让插入顺序和唯一索引的顺序一致后,就没有死锁产生了呢?

那是因为,如果插入操作不会导致间隙中出现新的索引值,MySQL可以优化并使用行级锁,而不会产生额外的间隙锁。

继续分析上面解决方案中事务的死锁报告:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-07-13 04:10:58 281472325639936
*** (1) TRANSACTION:
TRANSACTION 6835, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 281472766701312, query id 126 localhost root update
INSERT INTO switch (uid, type, val) VALUES  (30001,23,0)

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6835 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6835 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (2) TRANSACTION:
TRANSACTION 6834, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 2
MySQL thread id 10, OS thread handle 281472765583104, query id 127 localhost root update
INSERT INTO switch (uid, type, val) VALUES  (30001,3,0)

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6834 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 5 n bits 72 index uid of table `mydatabase`.`switch` trx id 6834 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80007531; asc   u1;;
 1: len 1; hex 97; asc  ;;
 2: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

从您提供的死锁报告来看,虽然两个事务(Transaction 6835 和 Transaction 6834)都在尝试操作相同的uidu1,即十六进制的80007531)在mydatabase.switch表的uid索引上,但是实际上并没有形成经典的死锁情况。

这里的关键点在于:

  1. Transaction 6835(Thread ID 9)正在等待一个S(共享)锁,但它已经显示为waiting状态,这意味着它并没有实际持有任何锁,而只是在等待。在MySQL的InnoDB存储引擎中,共享锁通常允许多个事务同时读取同一行,但不允许其他事务修改它。然而,由于Transaction 6835处于等待状态,它实际上并没有持有任何锁。
  2. Transaction 6834(Thread ID 10)已经持有一个X(排他)锁在uidu1的记录上,并正在等待一个间隙锁(gap lock)以允许它插入一个可能的新记录(尽管实际上由于ON DUPLICATE KEY UPDATE,它可能只是更新现有的记录)。间隙锁用于锁定一个范围,但不包括记录本身,以确保没有其他事务在该范围内插入新记录。

由于Transaction 6835没有实际持有任何锁,它只是在等待一个它还没有获得的锁,因此它不能与Transaction 6834形成死锁。死锁通常发生在两个或更多的事务互相等待对方释放资源时。

可以发现,事务6835因为是顺序插入type=3、23的记录,并不需要间隙锁,所以不会和事务6834持有的X锁形成循环等待关系。

总结

在批量插入的过程中,尽量保证插入顺序与索引的顺序一致,不仅是主键索引,也包括唯一索引。

  • 14
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高冷小伙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值