一次 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_idteacher_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 语句是会加上一个行级排他锁的,在分析加锁步骤之前,我们假设有如下一个数据表:

image

上表数据满足我们文章开头说的特点,根据 class_idteacher_id 单个字段均无法唯一确定一条数据,只能联合两个字段,才能确定一条数据,并且设定 class_idteacher_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 的加锁步骤如下:

image

最终,两个事务互相等待,形成死锁

解决方案

因为这个死锁本质上还是由于 Index Merge 这个优化导致的,所以要解决这个场景的死锁问题,本质上只要让 MySQL 不走 Index Merge 优化即可。

方案一

手动将一条 SQL 拆分成多条 SQL,在逻辑层做交集操作,阻止 MySQL 的憨憨优化行为,比如这里我们可以先根据 class_id 查询到相应主键,再根据 teacher_id 查询相应主键,最后根据交集后的主键查询数据。

方案二

建立联合索引,比如这里可以将 class_idteacher_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 条测试数据:

image

测试代码

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

image

上述代码主要是开启 100 个线程执行我们的数据修改 SQL 语句,来模拟线上并发情况,在运行几秒钟后,我们会得到下面这样一个报错:

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

这代表已经产生了死锁异常

死锁分析

上面我们用代码已经构造出了一个死锁,接下来我们进入 MySQL 看看死锁日志,在 MySQL 中执行如下命令即可查看死锁日志:

SHOW ENGINE INNODB STATUS;
image

在日志中,我们找到 LATEST DETECTED DEADLOCK 这一行,这里开始便是我们上次产生的死锁,接下来我们开始分析。

通过第 29 行可以看到,事务 1 执行的 SQL 的条件是 class_id = 6teacher_id = 16,它目前持有了一个行锁,第 34~39 行是该行数据,34 行是主键的十六进制表示,我们转换为 10 进制即为 1616。同样的,看 45 行,其等待拿锁的是主键 id 1517 的数据。

image

接下来用同样的方法分析事务 2,可知事务 2 持有了 3 把锁,分别是主键 id 为1317、1417、1517 的数据行,等待的是 1616

看到这里我们就已经发现了,事务 1 持有 1616 等待 1517,事务 2 持有1517 等待 1616,所以形成了一个死锁。此时 MySQL 的处理方法是回滚持有锁最少的事务,并且 JDBC 会抛出我们前面的 MySQLTransactionRollbackException 回滚异常。

总结

这个死锁在排查的时候其实非常不好排查,如果你不知道 MySQL 的 Index Merge,那么在排查的时候其实是毫无头绪的,因为呈现在你面前的就只有一条非常简单的 SQL,就算看死锁日志,也是一样的不明所以。

所以处理这类问题,更多的还是考验你的知识储备量和经验,只要遇到过一次,后面在写 SQL 的时候多加注意就好了!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容