手把手教你分析解决MySQL死锁问题

在生产环境中如果出现MySQL死锁问题该如何排查和解决呢,本文将模拟真实死锁场景进行排查,最后总结下实际开发中如何尽量避免死锁发生。
一、准备好相关数据和环境
当前自己的数据版本是8.0.22
mysql> select @@version;
+-----------+
| @@version |
+-----------+
| 8.0.22 |
+-----------+
1 row in set (0.00 sec)
数据库隔离级别(默认隔离级别)
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
自动提交关闭
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 0 |
+--------------+
1 row in set (0.00 sec)
表结构
这个age为 非唯一索引,这点对下面整个案例非常重要。
-- id是自增主键,age是非唯一索引,name普通字段
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`age` int DEFAULT NULL COMMENT '年龄',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户信息表';
表中暂时先插入两条数据

二、模拟出真实死锁案例
开启两个终端模拟事务并发情况,执行顺序以及实验现象如下:
1)事务A执行更新操作,更新成功
mysql> update user set name = 'wangwu' where age= 20;
Query OK, 1 row affected (0.00 sec)
- 事务B执行更新操作,更新成功
mysql> update user set name = 'zhaoliu' where age= 10;
Query OK, 1 row affected (0.00 sec)
3)事务A执行插入操作,陷入阻塞~
mysql> insert into user values (null, 15, "tianqi");

4)事务B执行插入操作,插入成功,同时事务A的插入由阻塞变为死锁error。
insert into user values (null, 30, "wangba");
Query OK, 1 row affected (0.00 sec)
事务A的插入操作变成报错。

上面四步操作后,我们分别对事务A和事务B进行commit操作。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
我们再来看数据库中表的数据。

我们发现,事务B的所有操作最终都成功了,而事务A的操作因为报错都回滚了。所以事务A的操作都失败。
那既然是死锁,为什么回滚事务A,而不是事务B,是随机的还是有机制在里面?
我们可以理解死锁是数据库对事务的保护机制,一旦发生死锁,MySQL会选择相对小的事务(undo较少的)进行回滚。
三、查看分析死锁日志
可以用 show engine innodb status,查看最近一次死锁日志哈,执行后,死锁日志如下(只展示部分日志):
LATEST DETECTED DEADLOCK
------------------------
2021-12-24 06:02:52 0x7ff7074f8700
*** (1) TRANSACTION:
TRANSACTION 2554368, ACTIVE 22 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
INSERT INTO user VALUES (NULL, 15, "tianqi")
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554368 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554368 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 4; hex 80000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 2554369, ACTIVE 14 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
INSERT INTO user VALUES (NULL, 30, "wangba")
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554369 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 4; hex 80000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554369 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (1)
1、事务A相关日志
1)找到关键词TRANSACTION,事务2554368

2)查看事务1正在执行的sql
insert into user values (null, 15, "tianqi")
- 查看当前事务已占有的锁和等待其它事务释放的锁

2、事务B相关日志
1)找到关键词TRANSACTION,事务2554369

2)查看事务2正在执行的sql
insert into user values (null, 30, "wangba")
- 查看当前事务已占有的锁和等待其它事务释放的锁

3、总结
这里把一些关键的日志截图了下

我们把这张图换一种方式画下

1)从图中可以很明显的看出,事务1和事务2都在等对方的锁释放,所以导致了死锁问题。而且最终是事务1进行了回滚。
2)这个日志提供比较重要的信息就是我们可以看出的是哪两条sql在互相一直等待其它事务的锁释放而产生了死锁,也知道是哪个索引导致产生的死锁,同时也知道最终哪个事务
被回滚了。
3)如果上面的信息还不能帮你定位解决问题,那可以问数据库DB要详细的binlog日志来分析这段时间这两个事务具体执行的所有sql。
四、总结分析案例中产生死锁的原因
这个分析就需要对MySQL中的各种锁机制有所了解,还不清楚的话可以看我之前写的两篇文章,看完你就清楚我下面所写的了。
1、事务A的SQL产生了哪些锁
1) 事务A的update语句产生哪些锁
我们先来看
update user set name = 'wangwu' where age= 20;
记录锁
因为是等值查询,所以这里会在满足age=20的所有数据请求一个记录锁。
间隙锁
因为这里是非唯一索引的等值查询,所以一样会产生间隙锁(如果是唯一索引的等值查询那就不会产生间隙锁,只会有记录锁),因为这里只有2条记录
所以左边为(10,20),右边因为没有记录了,所以请求间隙锁的范围就是(20,+∞),加一起就是(10,20) +(20,+∞)。
Next-Key锁
Next-Key锁=记录锁+间隙锁,所以该Update语句就有了(10,+∞)的 Next-Key锁
事务A的install语句产生哪些锁
INSERT INTO user VALUES (NULL, 15, "tianqi");
间隙锁
因为age 15(在10和20之间),所以需要请求加(10,20)的间隙锁
插入意向锁(Insert Intention)
插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即事务A需要插入意向锁(10,20),这个插入锁的作用就是提高插入效率的,在分析
死锁的时候我们可以不用关心它,只关心上面间隙锁就好了。
2、事务B的SQL产生了哪些锁
事务B的update语句产生哪些锁
我们先来看
update user set name = 'zhaoliu' where age= 10
记录锁
因为是等值查询,所以这里会在满足age=10的所有数据请求一个记录锁。
间隙锁
因为左边没有记录,右边有一个age=20的记录,所以间隙锁的范围是(-∞,10),右边为(10,20),一起就是(-∞,10)+(10,20)。
Next-Key锁
Next-Key锁=记录锁+间隙锁,所以该Update语句就有了(-∞,20)的 Next-Key锁
事务A的install语句产生哪些锁
INSERT INTO user VALUES (NULL, 30, "wangba")
间隙锁
- 因为age 30(左边是20,右边没有值),所以需要请求加(20,+∞)的间隙锁
插入意向锁(Insert Intention)
- (20,+∞)
锁都分析清楚了,接下来再来看下是什么地方导致死锁的呢?

这样以来产生整个死锁的原因也就清楚了,不过这里再补充两点
1)MySQL的间隙锁虽然有左开右闭的原则,但是其实这个并不完全正确,因为它有可能是左闭右开,也可能是左开右开,它会跟你插入主键值位置有关,具体的可以看我之前写的
一篇文章里面有完整示例MySQL记录锁、间隙锁、临键锁小案例演示。所以这里间隙锁写的都是左开右开的范围,可能临界点有点模糊,但不影响分析这个案例的死锁问题。
2)通过事务A和事务B的update语句,我们可以发现其实它们都持有间隙锁(10,20)的这段范围,说明间隙锁范围是可以相互兼容的,意思就是只要你的10不在我(10,+∞)的间隙锁
范围内,那就可以产生部分重合的间隙锁,也就是这里的(10,20)。
五、实际开发中如何尽量避免死锁发生
一般来讲在实际开发中,很少会发生死锁的情况,尤其是在业务并发量不是很大的情况下。在并发很大的情况下可能会存在偶尔产生死锁。
不过呢,在自己实际开发中,有遇到过请求一个接口出现100%概率死锁的情况。
当时的场景其实很简单。一段业务代码中,有去走Dubbo调其它接口服务,这就存在了两个事务,结果各自事务提交的时候,都需要等待对方的锁释放,就导致每次都发生死锁超时。
这其实是一种代码不规范而导致死锁的发生。这里也总结下如何尽量避免死锁发生。
1)不同的应用访问同一组表时,应尽量约定以相同的顺序访问各表。对一个表而言,应尽量以固定的顺序存取表中的行。这点真的很重要,它可以明显的减少死锁的发生。
举例:好比有a,b两张表,如果事务1先a后b,事务2先b后a,那就可能存在相互等待产生死锁。那如果事务1和事务2都先a后b,那事务1先拿到a的锁,事务2再去拿a的锁,如果
锁冲突那就会等待事务1释放锁,那自然事务2就不会拿到b的锁,那就不会堵塞事务1拿到b的锁,这样就避免死锁了。
2)在主键等值更新的时候,尽量先查询看数据库中有没有满足条件的数据,如果不存在就不用更新,存在才更新。为什么要这么做呢,因为如果去更新一条数据库不存在的数据,
一样会产生间隙锁。
举例:如果表中只有id=1和id=5的数据,那么如果你更新id=3的sql,因为这条记录表中不存在,那就会产生一个(1,5)的间隙锁,但其实这个锁就是多余的,因为你去更新一个
数据都不存在的数据没有任何意义。
3)尽量使用主键更新数据,因为主键是唯一索引,在等值查询能查到数据的情况下只会产生行锁,不会产生间隙锁,这样产生死锁的概率就减少了。当然如果是范围查询,
一样会产生间隙锁。
4)避免长事务,小事务发送锁冲突的几率也小。这点应该很好理解。
5)在允许幻读和不可重复度的情况下,尽量使用RC的隔离级别,避免gap lock造成的死锁,因为产生死锁经常都跟间隙锁有关,间隙锁的存在本身也是在RR隔离级别来
解决幻读的一种措施。
感谢
这篇文章给自己提供了很好的思路,这篇文章也基本上按照这个思路往下写的
声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!
手把手教你分析解决MySQL死锁问题的更多相关文章
- 通俗易懂地解决中文乱码问题(2) --- 分析解决Mysql插入移动端表情符报错 ‘incorrect string value: '\xF0...
原文:[原创]通俗易懂地解决中文乱码问题(2) --- 分析解决Mysql插入移动端表情符报错 'incorrect string value: '\xF0... 这篇blog重点在解决问题,如果你对 ...
- 手把手教你分析Mysql死锁问题
前言 前几天跟一位朋友分析了一个死锁问题,所以有了这篇图文详细的博文,哈哈~ 发生死锁了,如何排查和解决呢?本文将跟你一起探讨这个问题 准备好数据环境 模拟死锁案发 分析死锁日志 分析死锁结果 环境准 ...
- 手把手教你分析MySQL查询性能瓶颈,包教包会
当一条SQL执行较慢,需要分析性能瓶颈,到底慢在哪? 我们一般会使用Explain查看其执行计划,从执行计划中得知这条SQL有没有使用索引?使用了哪个索引? 但是执行计划显示内容不够详细,如果显示用到 ...
- 【原创】通俗易懂地解决中文乱码问题(2) --- 分析解决Mysql插入移动端表情符报错 ‘incorrect string value: '\xF0...
这篇blog重点在解决问题,如果你对字符编码并不是特别了解,建议先看看 < [原创]通俗易懂地解决中文乱码问题(1) --- 跨平台乱码 >. 当然,如果只是针对解决这个Mysql插入报错 ...
- 使用jstack分析解决进程死锁问题
项目启动后不久就会出现死锁的现象,一直不知道什么原因造成的,后来经过大神的指点,解决了这个问题. 流程如下: 1.环境jdk1.6以上: 2.linux下使用ps aux|grep tomcat 命令 ...
- 手把手教你彻底理解MySQL的explain关键字
数据库是程序员必备的一项基本技能,基本每次面试必问.对于刚出校门的程序员,你只要学会如何使用就行了,但越往后工作越发现,仅仅会写sql语句是万万不行的.写出的sql,如果性能不好,达不到要求,可能会阻 ...
- 手把手教你 Docker搭建mysql并配置远程访问
一.使用docker部署mysql 1.在docker中搜索要安装的mysql docker search mysql (这步其实可以跳过O(∩_∩)O哈哈~) 2.拉取mysql镜像 docker ...
- MySQL死锁问题分析及解决方法实例详解(转)
出处:http://www.jb51.net/article/51508.htm MySQL死锁问题是很多程序员在项目开发中常遇到的问题,现就MySQL死锁及解决方法详解如下: 1.MySQL常用 ...
- MySQL 死锁问题分析
转载: MySQL 死锁问题分析 线上某服务时不时报出如下异常(大约一天二十多次):"Deadlock found when trying to get lock;". Oh, M ...
随机推荐
- 【WP】攻防世界-杂项-Misc
长期更新一波 攻防世界 的杂项题解 这东西主要靠积累吧 攻防世界:https://adworld.xctf.org.cn 因为攻防世界的题目顺序经常变化,我也不改序号了,顺着之前写的位置往下写,推荐使 ...
- 对Spring IOC容器的思考
最近在看Spring5的视频教学,学到了IOC容器这块,对IOC有些浅薄的理解,分享一二:有错误之处,还请大佬指出 IOC(Inversion of Control 控制反转),是面向对象编程中的一种 ...
- 新建任务(Project)
<Project2016 企业项目管理实践>张会斌 董方好 编著 新建任务,这操作简单得就跟在Excel的单元格里输入个数据一样,不过也不是一点讲究都没有. 首先得选对视图. 不是所有的视 ...
- 如何完成符合ISO 26262要求的基于模型设计(MBD)的测试
背景介绍 随着汽车行业的迅速发展,汽车的复杂程度不断增加,越来越多的汽车电子控制系统具有与安全相关的功能,因此对ECU的安全要求也越来越高.复杂的软件功能,将会带来大量的软件风险问题,如何保证软件的安 ...
- CF1093B Letters Rearranging 题解
Content 有 \(t\) 次询问,每次询问给定一个字符串 \(s\).定义一个"好的字符串"为不是回文串的字符串.对于每一次询问,求出任意一个重新排列能够得到的"好 ...
- CF1547A Shortest Path with Obstacle 题解
Content 给定两个在二维平面上的网格 \(A(x_A,y_A)\) 和 \(B(x_B,y_B)\),另外,还有一个不可通过的网格 \(F(x_F,y_F)\).你需要求出在不经过 \(F\) ...
- CF1514A Perfectly Imperfect Array 题解
Content 给定一个长度为 \(n\) 的序列,问是否存在一个非空子序列,使得这个子序列所有元素的积不是完全平方数. 数据范围:\(t\) 组数据,\(1\leqslant t\leqslant ...
- 第二周Python笔记 数据类型 列表 字典
列表,拉锁式儿合并. [ [a,b] for a,b in zip(list1,list2)] #最笨的 a=[1,2,3,4,5] b=[2,3,4,5,6] d=[] for i in range ...
- 『与善仁』Appium基础 — 30、操作微信小程序
目录 1.测试微信小程序前提 2.获取微信小程序的进程 3.代码示例 4.补充:(了解) 微信小程序和微信公众号的测试方式基本上是一样的. 微信的小程序越来越多了,随之带来的问题是:小程序如何做自动化 ...
- 【LeetCode】380. Insert Delete GetRandom O(1) 解题报告(Python)
[LeetCode]380. Insert Delete GetRandom O(1) 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxu ...