记一次Index-Merge造成的死锁

起因

前一段时间一个没有多少量的项目突然线上出错报警,第一时间查到异常日志

报错信息比较明显,数据库产生死锁。

分析

分析代码之前让我们来复习一下什么是死锁以及产生死锁的原因是什么

死锁产生原因是什么❓

当两个及以上的事务,都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源。

举个我们最常见的例子,A 事务持有X ,申请Y,B 事务持有Y锁,申请X锁。A和B 事务持有锁并且申请对方持有的锁,这样就会造成死锁。

翻译成代码:

// 在隔离级别RR,ID为主键索引的情况下。 (画外音:不谈隔离级别与索引情况下分析加锁都是耍流氓)                                

session1:update name = “a” where id =1;update name = “b” where id =4; session2:   update name = “a” where id = 4;update name = “b” where id = 1;

在并发的情况下,假设请求顺序是这样的1. session1先拿id=1的行锁2. session2拿id=4的行锁3. session1请求id=4的行锁(等待session2释放)4. session2请求id=1的行锁(等待session1释放)5. 循环等待,造成死锁

了解了死锁产生的基本原因之后,让我们去看下源码,看是不是有类似这样的代码逻辑。

但是奇怪的是,我们翻了源码(这里不把源码放出来了),但是源码并没有类似这样的逻辑,更神奇的是代码里根本就没有@Transaction注解,也就意味着没有应用到事务,也就是说单表语句造成了死锁❓

现在看来问题比较诡异,单条语句造成了死锁。接下来我们去跟DBA要一下死锁日志

根据死锁日志,发现确实仅仅是因为 

update g_growth_free_activity_product    SET buy_count = 1079    where product_id = 79550 and activity_id = 2062 and deleted = 0

这条语句产生了死锁。

转机

之后就是各种google,百度的时候了,终于我们发现了一些和我们比较像的案例,https://blog.csdn.net/zheng0518/article/details/54695605 ,链接里的例子和我们的现象比较接近,文章里更是贴出了MySQL官方bug的地址https://bugs.mysql.com/bug.php?id=77209

上面的图就bug中描述的内容,大意是update时使用index merge增加了死锁风险。

我们需要先去看看【index-merge】是什么。
我们翻一下官方文档:https://dev.mysql.com/doc/refman/8.0/en/index-merge-optimization.html,文档里有多种情况的介绍,不赘述。
翻译一下大概就是对单个表的多个索引分别进行扫描并将结果交并集处理。

那我们再来看业务SQL,针对
update g_growth_free_activity_product    SET buy_count = 1079    where product_id = 79550 and activity_id = 2062 and deleted = 0
如果有index merge,意味着【product_id】和【activity_id】是索引列。(deleted字段应该没人加索引吧)
我们去看下表中的索引结构:

【product_id】和【activity_id】确实都是普通二级索引。
虽然都是单列索引,但是我们还不能确定优化器在执行SQL的时候一定会选择【index merge】,还需要查看下执行计划。
为了保证我们的数据和线上一致,我们把线上数据拉了下来,并创建了一个test表,表结构相同,索引结构相同,把数据导进去。并查询到发生死锁时的请求日志

我们通过执行计划能看到,update时确实使用了 【index-merge】进行优化,extra列显示的是使用了【交集】类型。

到这时候,所有的条件就都能对的上了,但是是否真的是因为这个原因发生死锁我们还需要还原一下案发现场,尝试在neibu环境进行复现。
我们在上面已经建好的test表上做测试。

10个线程并发执行更新,查看结果。

确实是很容易就发生了死锁。
到这里我们问题就已经基本定位了:由于索引设置不合理的缘故,where条件两个单列普通二级索引在查询的时候MySQL进行了index-merge优化,引发死锁。

分析

找到原因之后,我们再来分下下使用index-merge为什么会发生死锁。

我们先拿到死锁的数据。
查询【activity_id=2062】,对应MySQL主键记录是在 【88-234】
查询【product_id=79550】,对应MySQL主键记是【218,186】
查询【product_id=79466】,对应MySQL主键记是【219,183】我们根据执行计划与加锁流程拆分下成如下几个过程

再结合的死锁日志,我们分析下加锁流程

session1等待【activity_id】的锁,session2等待的是主键锁,产生循环等待,发生死锁。

解决方案

最后说一下解决方案,建议使用方案2,本身就是因为索引使用不合理导致,优化之后再也没有死锁的问题了。

1、关闭【index merge】
2、建立联合索引
3、优化代码
4、强制走单列索引

最后提几个关于MySQL建议阅读的文档:
官方文档:https://dev.mysql.com/doc/
淘宝数据库内核月报:http://mysql.taobao.org/monthly/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章