在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言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,假设 UPDATE test_table SET `name`="zhangsan" WHERE class_id = 10 AND teacher_id = 8; 如果没有 Index Merge 优化的时候,MySQL 查询数据的步骤如下:
从这个过程中,不难看出,MySQL 只使用到了一个索引,至于为什么不使用多个索引,简单来说就是因为多个索引在多棵树上,强行使用反而降低性能。 再来看看引入了 Index Merge 优化后,MySQL 查询数据的步骤如下:
这里可以看出,有了 Index Merge 之后,MySQL 将一条 SQL 语句拆分成了两个查询步骤,分别使用两个索引,再用交集操作优化性能。 死锁分析分析完了 Index Merge 的步骤,我们再回过头想一下为什么会出现死锁呢? 还记得上面说的 Index Merge 将一条 SQL 查询拆分成了两个步骤吗,问题就出现在这里。我们知道 上表数据满足我们文章开头说的特点,根据 假设有如下两条 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 的 方案二 建立联合索引,比如这里可以将 方案三 强制走单个索引,在表名后添加 方案四 关闭 Index Merge 优化:
场景复现数据准备 为了方便测试,这里提供一个 SQL 脚本,将其用 Navicat 导入后即可得到需要的测试数据: 下载地址:https://cdn.juzibiji.top/file/index_merge_student.sql 导入之后,我们会得到如下格式的 10000 条测试数据: 测试代码 由于篇幅限制,这里仅给出代码 Gist 链接:https://gist.github.com/juzi214032/17c0f7a51bd8d1c0ab39fa203f930c60 上述代码主要是开启 100 个线程执行我们的数据修改 SQL 语句,来模拟线上并发情况,在运行几秒钟后,我们会得到下面这样一个报错:
这代表已经产生了死锁异常 死锁分析 上面我们用代码已经构造出了一个死锁,接下来我们进入 MySQL 看看死锁日志,在 MySQL 中执行如下命令即可查看死锁日志: SHOW ENGINE INNODB STATUS; 在日志中,我们找到 通过第 29 行可以看到,事务 1 执行的 SQL 的条件是 接下来用同样的方法分析事务 2,可知事务 2 持有了 3 把锁,分别是主键 id 为1317、1417、1517 的数据行,等待的是 1616 。 看到这里我们就已经发现了,事务 1 持有 1616 等待 1517,事务 2 持有1517 等待 1616,所以形成了一个死锁。此时 MySQL 的处理方法是回滚持有锁最少的事务,并且 JDBC 会抛出我们前面的 MySQLTransactionRollbackException 回滚异常。 总结这个死锁在排查的时候其实非常不好排查,如果你不知道 MySQL 的 Index Merge,那么在排查的时候其实是毫无头绪的,因为呈现在你面前的就只有一条非常简单的 SQL,就算看死锁日志,也是一样的不明所以。 所以处理这类问题,更多的还是考验你的知识储备量和经验,只要遇到过一次,后面在写 SQL 的时候多加注意就好了! 到此这篇关于MySQL线上死锁分析实战的文章就介绍到这了,更多相关MySQL线上死锁分析内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论