一次 MySQL 线上死锁分析实战
关键词:MySQL Index Merge
前言
MySQL 的锁机制相信大家在学习 MySQL 的时候都有简单的了解过,那既然有锁就必定绕不开死锁这个问题。其实 MySQL 在大部分场景下是不会存在死锁问题的(比如并发量不高,SQL 写得不至于太拉胯的情况),但是在高并发的业务场景下,一不注意就会产生死锁,而这个死锁分析起来也比较麻烦。
前段时间在公司实习的时候就遇到了一个比较奇怪的死锁,之前一直没来得及好好整理,最近有空复现了一下,算是积累一点经验。
业务场景
简单说一下业务背景,公司做的是电商直播,我负责的是主播端相关的业务。而这个死锁就出现在主播后台对商品信息进行更新的时候。
我们的一个商品会有两个关联的 ID,通过其中任何一个 ID 都无法确定唯一一件商品(也就是说这个 ID 和商品是一对多的关系),只能同时查询两个 ID,才能确定一件商品。所以在更新商品信息的时候,需要在 where 条件中同时指定两个 ID,下面是死锁 SQL 的结构(已脱敏):
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 10 AND teacher_id = 8;
这个 SQL 非常简单,根据两个等值条件,对一个字段进行更新。
不知道你看到这个 SQL 会不会懵逼,按常理来说,应该是一个事务里有多条 SQL 才会有可能出现死锁,这一条 SQL 怎么可能出现死锁呢?
是的,我当时也有这样的疑惑,甚至怀疑是不是报警系统瞎报(最后证明不是…),当时是真的摸不着头脑。并且因为数据库权限的原因,想看死锁日志都看不到,又是临近下班的时候,找 DBA 能麻烦死,所以就直接搜索引擎走起了……(关键词:update 死锁 单条 sql),最后查出来是由于 MySQL 的索引合并优化导致的,即 Index Merge,下面会进行详细讲解并复现一下死锁场景。
索引合并
Index Merge 是 MySQL 在 5.0 的时候引入的一项优化功能,主要是用于优化一条 SQL 使用多个索引的情况。
我们来看刚刚的 SQL,假设 class_id 和 teacher_id 分别是两个普通索引:
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 10 AND teacher_id = 8;
如果没有 Index Merge 优化的时候,MySQL 查询数据的步骤如下:
- 根据 class_id 或 teacher_id (具体使用哪个索引由优化器根据实际数据情况自行判断,这里假设使用 
class_id的索引)在二级索引上查询到对应数据的主键 ID - 根据查询到的主键 ID 进行回标查询(即查询聚簇索引),得到相应的数据行
 - 从数据行中获取 
teacher_id,判断其是否等于 8,满足条件则返回 
从这个过程中,不难看出,MySQL 只使用到了一个索引,至于为什么不使用多个索引,简单来说就是因为多个索引在多棵树上,强行使用反而降低性能。
再来看看引入了 Index Merge 优化后,MySQL 查询数据的步骤如下:
- 根据 
class_id查询到相应的主键,再根据主键回表查询到对应的数据行(记为结果集 A) - 根据 
teacher_id查询到相应的主键,再根据主键回表查询到对应的数据行(记为结果集 B) - 将结果集 A 和结果集 B 执行交集操作,获得最终满足条件的结果集
 
这里可以看出,有了 Index Merge 之后,MySQL 将一条 SQL 语句拆分成了两个查询步骤,分别使用两个索引,再用交集操作优化性能。
死锁分析
分析完了 Index Merge 的步骤,我们再回过头想一下为什么会出现死锁呢?
还记得上面说的 Index Merge 将一条 SQL 查询拆分成了两个步骤吗,问题就出现在这里。我们知道 UPDATE 语句是会加上一个行级排他锁的,在分析加锁步骤之前,我们假设有如下一个数据表:

上表数据满足我们文章开头说的特点,根据 class_id 和 teacher_id  单个字段均无法唯一确定一条数据,只能联合两个字段,才能确定一条数据,并且设定 class_id 和 teacher_id  分别为两个普通索引。
假设有如下两条 SQL 语句并发执行,它们的参数完全不同,直觉告诉我们应该不会出现死锁,但直觉往往是错误的:
// 线程 A 执行
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 2 AND teacher_id = 1;
// 线程 B 执行
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 1 AND teacher_id = 2;
那么在 Index Merge 的优化下,并发执行如上 SQL 的时候,MySQL 的加锁步骤如下:

最终,两个事务互相等待,形成死锁
解决方案
因为这个死锁本质上还是由于 Index Merge 这个优化导致的,所以要解决这个场景的死锁问题,本质上只要让 MySQL 不走 Index Merge 优化即可。
方案一
手动将一条 SQL 拆分成多条 SQL,在逻辑层做交集操作,阻止 MySQL 的憨憨优化行为,比如这里我们可以先根据 class_id 查询到相应主键,再根据 teacher_id 查询相应主键,最后根据交集后的主键查询数据。
方案二
建立联合索引,比如这里可以将 class_id 和 teacher_id 建立一个联合索引,MySQL 就不会走 Index Merge 了
方案三
强制走单个索引,在表名后添加 for index(class_id) 可以指定该语句仅走 class_id 索引
方案四
关闭 Index Merge 优化:
- 永久关闭:
SET [GLOBAL|SESSION] optimizer_switch='index_merge=off'; - 临时关闭:
UPDATE /*+ NO_INDEX_MERGE(test_table) */ test_table SETname="zhangsan" WHERE class_id = 10 AND teacher_id = 8; 
场景复现
数据准备
为了方便测试,这里提供一个 SQL 脚本,将其用 Navicat 导入后即可得到需要的测试数据:
下载地址:https://cdn.juzibiji.top/file/index_merge_student.sql
导入之后,我们会得到如下格式的 10000 条测试数据:

测试代码
由于篇幅限制,这里仅给出代码 Gist 链接:https://gist.github.com/juzi214032/17c0f7a51bd8d1c0ab39fa203f930c60

上述代码主要是开启 100 个线程执行我们的数据修改 SQL 语句,来模拟线上并发情况,在运行几秒钟后,我们会得到下面这样一个报错:
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
这代表已经产生了死锁异常
死锁分析
上面我们用代码已经构造出了一个死锁,接下来我们进入 MySQL 看看死锁日志,在 MySQL 中执行如下命令即可查看死锁日志:
SHOW ENGINE INNODB STATUS;

在日志中,我们找到 LATEST DETECTED DEADLOCK 这一行,这里开始便是我们上次产生的死锁,接下来我们开始分析。
通过第 29 行可以看到,事务 1 执行的 SQL 的条件是 class_id = 6 和 teacher_id = 16 ,它目前持有了一个行锁,第 34~39 行是该行数据,34 行是主键的十六进制表示,我们转换为 10 进制即为 1616。同样的,看 45 行,其等待拿锁的是主键 id 1517 的数据。

接下来用同样的方法分析事务 2,可知事务 2 持有了 3 把锁,分别是主键 id 为1317、1417、1517 的数据行,等待的是 1616 。
看到这里我们就已经发现了,事务 1 持有 1616 等待 1517,事务 2 持有1517 等待 1616,所以形成了一个死锁。此时 MySQL 的处理方法是回滚持有锁最少的事务,并且 JDBC 会抛出我们前面的 MySQLTransactionRollbackException 回滚异常。
总结
这个死锁在排查的时候其实非常不好排查,如果你不知道 MySQL 的 Index Merge,那么在排查的时候其实是毫无头绪的,因为呈现在你面前的就只有一条非常简单的 SQL,就算看死锁日志,也是一样的不明所以。
所以处理这类问题,更多的还是考验你的知识储备量和经验,只要遇到过一次,后面在写 SQL 的时候多加注意就好了!
一次 MySQL 线上死锁分析实战的更多相关文章
- mysql innodb引擎  一次线上死锁分析排查步骤
		
我们的线上erp系统一天使用人员反映部分数据死活保存不上而且页面操作很慢.开始以为操作数据量大的原因, 后来查看了我们线上的glowroot系统,发现slowtrace中有超长时间的访问,点开查看详情 ...
 - 【MySQL 线上 BUG 分析】之 多表同字段异常:Column ‘xxx’ in field list is ambiguous
		
一.生产出错! 今天早上11点左右,我在工作休息之余,撸了一下猫.突然,工作群响了,老大在里面说:APP出错了! 妈啊,这太吓人了,因为只是说了出错,但是没说错误的信息.所以我赶紧到APP上看看. 这 ...
 - 线上BUG:MySQL死锁分析实战
		
原文链接:线上BUG:MySQL死锁分析实战 1 线上告警 我们不需要关注截图中得其他信息,只要能看到打印得org.springframework.dao.DeadlockLoserDataAcces ...
 - MySQL慢日志线上问题分析及功能优化
		
本文来源于数据库内核专栏. MySQL慢日志(slow log)是MySQL DBA及其他开发.运维人员需经常关注的一类信息.使用慢日志可找出执行时间较长或未走索引等SQL语句,为进行系统调优提供依据 ...
 - 线上bug分析
		
昨天下午大神把组内几十号人召集在一起开Online bug分析大会,主要是针对近期线上事故从事故原因和解决方案两个维度来分析. 对金融软件来说,每一次的线上事故都有可能给公司带来重大的损失,少扣了用户 ...
 - 记一次linux通过jstack定位CPU使用过高问题或排查线上死锁问题
		
一.java定位进程 在服务器中终端输入命令:top 可以看到进程ID,为5421的cpu这列100多了. 记下这个数字:5421 二.定位问题进程对应的线程 然后在服务器中终端输入命令:top -H ...
 - MySQL死锁系列-线上死锁问题排查思路
		
前言 MySQL 死锁异常是我们经常会遇到的线上异常类别,一旦线上业务日间复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常.这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,并且分 ...
 - 一次MySQL线上慢查询分析及索引使用
		
本文由作者郑智辉授权网易云社区发布. 0.前言 本文通过分析线上MySQL慢查询日志,定位出现问题的SQL,进行业务场景分析,结合索引的相关使用进行数据库优化.在两次处理问题过程中,进行的思考. 1. ...
 - Docker + node(koa) + nginx + mysql 线上环境部署
		
在上一篇 Docker + node(koa) + nginx + mysql 开发环境搭建,我们进行了本地开发环境搭建 现在我们就来开始线上环境部署 如果本地环境搭建没有什么问题,那么线上部署的配置 ...
 
随机推荐
- KVM之XFS磁盘扩容
			
1.前言 根据目前我们使用的需求,以前规划的100G磁盘空间不够,这里将演示XFS文件系统的扩容,因为我使用的是KVM所以也会演示KVM的扩容方式. 2.KVM磁盘扩容 扩容前一定要先备份,或者做快照 ...
 - 动态代理+静态代理+cglib代理 详解
			
代理定义:代理(Proxy):是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 动态代理+静态 ...
 - EF6.2加载速度慢的解决方案
			
最近的项目中一直有反馈,EF在第一次启动之后调用的话,加载速度很慢,在网上搜索了一下,基本就是三种解决方案. 在程序启动的时候将映射视图缓存下来. 使用Ngen生成EF的本地镜像. IIS8内置功能 ...
 - RAID 技术全解
			
图文并茂 RAID 技术全解 – RAID0.RAID1.RAID5.RAID100-- RAID 技术相信大家都有接触过,尤其是服务器运维人员,RAID 概念很多,有时候会概念混淆.这篇文章为网络转 ...
 - 2019-2020 ACM-ICPC Brazil Subregional Programming Contest   Problem M Maratona Brasileira de Popcorn  (二分)
			
题意:有\(n\)袋爆米花,某个队伍有\(c\)个队员,每个队员每秒做多可以吃\(t\)粒爆米花,但一袋爆米花只能由一个队员吃完,并且一个队员只能吃连续的一袋或几袋,不能隔着吃某一袋,求将所有爆米花吃 ...
 - 002、Python中json字符串与字典转换
			
1.测试用例文件TestCase.xlsx 2.编写Python文件进行读取 #!/usr/bin/env python # -*- coding:utf-8 -*- import time impo ...
 - 【luogu AT3957】[AGC023F] 01 on Tree
			
01 on Tree 题目链接:luogu AT3957 题目大意 有一棵根为 \(1\) 的树,每个节点有个值 \(0\) 或 \(1\). 然后每次你可以把一个没有父亲的点删除,然后把值放进一个数 ...
 - 大数据开发-Spark Join原理详解
			
数据分析中将两个数据集进行 Join 操作是很常见的场景.在 Spark 的物理计划阶段,Spark 的 Join Selection 类会根 据 Join hints 策略.Join 表的大小. J ...
 - 国产网络测试仪MiniSMB - 双击就可以直接编辑数据报文字段(如IP地址)
			
国产网络测试仪MiniSMB(www.minismb.com)是复刻smartbits的IP网络性能测试工具,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此以太 ...
 - Operating System:管程相关概念
			
管程 (Moniters,也称为监视器)一.管程的概念是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源.这些共享资源一般是硬件设备或一群变量.管程实现了在一个时间点, ...