MySQL死锁场景及解法

在MySQL中,当一个事务在满足某个索引条件的情况下插入数据时,会生成一个"gap锁",也被称为间隙锁。Gap锁用于防止其他事务在这个索引范围内插入数据,从而保证数据的唯一性和一致性。更具体来说,当一个事务要插入一条记录时,MySQL会在满足索引条件的位置上创建一个记录锁,同时在插入记录之前,会在新记录的前后空隙位置上创建一个间隙锁(gap锁)。这样做的目的是防止其他事务在相同的索引范围内插入新记录,从而确保数据的一致性。Gap锁的存在是为了保证数据完整性和一致性,但同时也可能增加了死锁的可能性。在设计数据库应用程序时,需要注意合理规划事务的逻辑和持续时间,以及考虑合适的锁策略,以最小化死锁的风险。 在死锁情况下,常见的是两个事务同时在插入数据时持有了间隙锁,但是彼此又都在等待对方持有的间隙锁,导致了死锁的发生。

1、RR级别下,先delete,再insert,导致死锁

www.jianshu.com/p/f8495015d... 流程如下:

事务一delete,加插入意向锁,

事务二delete,加插入意向锁,应为是共享锁可以加成功, 事务一insert,需要得事务二释放插入意向锁, 事务二insert,需要等事务一释放插入意向锁,死锁了。 原因:操作的都是不存在的数据,delete的时候加了插入意向锁吧。

解决方案 1)修改隔离级别为提交读RC; 2)修改业务代码逻辑,删除记录之前,先select,确认该记录存在,再执行delete删除该记录。

2、唯一索引约束死锁问题在从库的解决方案 -replace语句

前言:大部分并行复制死锁的问题是出现在唯一键上,和隔离级别以及是否开启WRITESET不是强相关

首先看下replace语句会加什么样的锁,了解死锁产生原因~

java 复制代码
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`a` varchar(10) NOT NULL,
`b` varchar(10) NOT NULL,
`c` varchar(10) DEFAULT NULL,
`d` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `u_k` (`a`,`b`),
KEY `i_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

//表已有数据
select * from test;
+----+-----+-----+------+------+
| id | a | b | c | d |
+----+-----+-----+------+------+
| 10 | a10 | b10 | c10 | d10 |
| 20 | a20 | b20 | c20 | d20 |
| 30 | a30 | b30 | c30 | d30 |
| 40 | a40 | b40 | c40 | d40 |
| 50 | a50 | b50 | c50 | d50 |
+----+-----+-----+------+------+

//replace语句
replace into test values(NULL,'a30','b30','c30','d30');//id会在server层生成

"show engine innodb status\G"看到的加锁情况:

SQL 复制代码
---TRANSACTION 5568158, ACTIVE 10 sec
4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 91634, OS thread handle 139866244536064, query id 38506448 localhost root
TABLE LOCK table `kk`.`test` trx id 5568158 lock mode IX
RECORD LOCKS space id 33 page no 3 n bits 80 index PRIMARY of table `kk`.`test` trx id 5568158 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 7; compact format; info bits 0
0: len 4; hex 8000001e; asc ;;
1: len 6; hex 00000054f69e; asc T ;;
2: len 3; hex 613330; asc a30;;
3: len 3; hex 623330; asc b30;;
4: len 7; hex 5a000000510149; asc Z Q I;;
5: len 3; hex 633330; asc c30;;
6: len 3; hex 643330; asc d30;;
RECORD LOCKS space id 33 page no 4 n bits 72 index u_k of table `kk`.`test` trx id 5568158 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 3; hex 613330; asc a30;;
1: len 3; hex 623330; asc b30;;
RECORD LOCKS space id 33 page no 4 n bits 72 index u_k of table `kk`.`test` trx id 5568158 lock_mode X
Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 3; hex 613330; asc a30;;
1: len 3; hex 623330; asc b30;;
Record lock, heap no 5 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 3; hex 613430; asc a40;;
1: len 3; hex 623430; asc b40;;

由于这个语句触发了唯一键冲突,在这个情况下会被拆分成delete+insert语句: delete from test where id=30 and a='a30' and b='b30' and c='c30' and d='d30';

insert into test values(30,'a30','b30','c30','d30'); 从输出可以看出,其获得了

  1. 1个table IX锁
  2. 1个主键的record锁(delete带来的)
  3. 1个唯一键上的record锁(delete带来的)
  4. 1个a='a30',b='b30'的next key锁(insert需要判重带来的)
  5. 1个a='a40',b='b40'的next key锁(insert需要判重带来的)

假如这个时候也有一个session使用replace语句,也想去更新这个unique key。那么就会发生如下的问题。

  • session1 执行完第3步,获得了该unique key的record x锁
  • session2 也执行到第3步(第2步因为server生成的id不会冲突,所以可以继续),想获得该unique key的record x锁,但是无法获得进入等待状态
  • session1继续执行到第4步时,想获得该unique key的next key锁,但是该锁和session 2等待的类型不兼容
  • 最终形成一个session1等待session2完成,session2等待session1完成。

解法探索

这个问题解法非常复杂,业界讨论了很多,具体看该bug。主要着眼点还是考虑在做唯一性检查的时候进行修正。有考虑提前释放next key lock的;有考虑使用latch进行加锁(因为latch持有时间短,且内部有通过顺序来避免死锁),但是在实现上都需要非常严格的评估以及一些潜在的极端case场景,所以官方迟迟没有给予修复。

因此我们一方面保持对官方的关注,另一方面,能否解决在从库上遇到的这个问题。解决在从库上遇到的这个问题对我们的意义也许更大。

  1. 责任问题。主库上遇到死锁,业务可以感知到并进行重试,但是从库上遇到死锁就是数据库研发中心的责任。
  2. 从库卡住后,会造成不可控的延迟,而我们多数业务对延迟很敏感。我们很多业务在遇到延迟时,都需要手动摘流量处理(响应时间在分钟级别,特别是非上班时间可能要10分钟以上),而这时已经造成了影响。
  3. 从库卡住之后的解决办法不够平滑。在遇到卡死情况下,只能通过kill -9杀掉进程才可以解决。这样一方面恢复时间比较长,一方面恢复后数据是冷数据,对业务也是干扰。
  4. 容量风险。有一定概率遇到多数从库均卡住,如果下掉流量,可能压垮剩余的数据库。

从库解决方案

由于加上next key锁是为了判重,那么在从主库上复制过来的数据,无论采用writeset还是commit_order模式,均不可能重复。所以在从库的SQL线程回放时,可以去掉这块检查,也就避免了next key锁,最终解决了从库上的这个问题。

代码

只要加三行即可解决问题

总结

理论分析和实验表明,修复后,在不改变任何配置的情况下,从库上不会出现死锁问题。

参考文章

约束实现 mysql.taobao.org/monthly/202...

www.163.com/dy/article/...

主从回放死锁bug

zhuanlan.zhihu.com/p/196769001

业界对该问题的讨论

bugs.mysql.com/bug.php?id=...

相关推荐
AskHarries3 分钟前
Java字节码增强库ByteBuddy
java·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞3 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。4 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*5 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu5 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s5 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子5 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算