当前位置: 首页 > 科技观察

记得一个神奇的MySQL死锁排查

时间:2023-03-21 19:46:17 科技观察

背景说起Mysql死锁,之前写过一篇关于Mysql锁的基础介绍。对一些基本的Mysql锁或者死锁有一个简单的了解。你可以阅读这篇文章解释了为什么开发人员需要了解数据库锁。有了上面的经验,本以为死锁就可以轻松处理了,没想到又是一个阳光明媚的下午,又报了死锁,不过这次并没有想象中的那么简单。问题之初,一天下午,系统突然报警,抛出异常:仔细一看,好像是因为死锁回滚,写了事务回滚异常。原来是死锁问题。我明白了,于是开始积极排查这个问题。首先在数据库中搜索InnodbStatus,InnodbStatus中会记录上次的死锁信息,输入如下命令:SHOWENGINEINNODBSTATUS死锁信息如下,简单处理sql信息:----------------------最新检测到死锁------------------------2019-02-2215:10:560x7eec2f468700***(1)TRANSACTION:TRANSACTION2660206487,ACTIVE0secstartingindexreadmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize1136,1rowlock(s)MySQLthreadid31261312,OSthreadhandle139554322093824,queryid1162497575010.23.134.92erp_crm__6f73updating/*id:3637ba36*/UPDATEtenant_configSETopen_card_point=0wheretenant_id=123***(1)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid1322pageno534nbits960indexuidx_tenantoftable`erp_crm_member_plan`.`tenant_config`trxid2660206487lock_modeXlocksrecbutnotgapwaiting***(2)TRANSACTION:TRANSACTION2660206486,ACTIVE0secstartingindexreadmysqltablesinuse1,locked13lockstruct(s),heapsize1136,2rowlock(s)MySQLthreadid31261311,OSthreadhandle139552870532864,queryid1162497575810.23.134.92erp_crm__6f73updating/*id:3637ba36*/UPDATEtenant_configSETopen_card_point=0wheretenant_id=123***(2)HOLDSTHELOCK(S):RECORDLOCKSspaceid1322pageno534nbits960indexuidx_tenantoftable`erp_crm_member_plan`.`tenant_config`trxid2660206486lockmodeS***(2)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid1322pageno534nbits960indexuidx_tenantoftable`erp_crm_member_plan`.`tenant_config`trxid2660206486lock_modeXlocksrecbutnotgapwaiting***WEROLLBACKTRANSACTION(1)-----------我给大家简单分析一下这个死锁日志。事务1执行Update语句时,需要获取uidx_tenant索引和where条件上的X锁(行锁)。事务2执行同样的Update语句,也想获取uidx_tenant上的X锁(行锁),然后发生死锁,事务1回滚。那时,我很困惑。我回忆了一下产生死锁的必要条件:,互斥。2.请求和保持条件。3.不剥夺条件。4.循环等待。从日志上看,事务1和事务2都在竞争同一行的行锁,这和之前的循环竞争锁有点不一样。不管怎么看,他们都不能满足循环等待的条件。经同事提醒,由于无法从死锁日志中排查,只能从业务代码和业务日志中查看。这段代码的逻辑如下:saveConfig冲突,更新记录context:{},config:{}",poiContext,tenantConfig);returntenantConfigMapper.updateTenantConfig(poiContext.getTenantId(),tenantConfig);}}这段代码的意思是保存一个配置文件,如果发生了如果存在唯一索引冲突,则会更新。当然这里写的可能不是很规范。其实你可以使用insertinto...onduplicatekeyupdate来达到同样的效果,但是即使你使用这个,实际上也会出现死锁。同事看完代码,把当时的业务日志发给我。可以看到这里同时出现了3条log,说明是updated语句发生了唯一索引冲突,然后发生了死锁。至此,答案终于有了一点端倪。这时候看我们的表结构如下(简化):CREATETABLE`tenant_config`(`id`bigint(21)NOTNULLAUTO_INCREMENT,`tenant_id`int(11)NOTNULL,`open_card_point`int(11)DEFAULTNULL,PRIMARYKEY(`id`),UNIQUEKEY`uidx_tenant`(`tenant_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4ROW_FORMAT=COMPACT我们的tenant_id作为唯一索引,我们的insert和updatewhere条件都是基于唯一索引。UPDATEtenant_configSETopen_card_point=0wheretenant_id=123到了这里,感觉跟插入时锁定唯一索引有关系。接下来,我们将在下一步进行深入分析。深入分析上面我们说进入update语句的事务有3个。为了简化说明,我们只需要两个事务同时进入更新语句即可。下表展示了我们的整个发生过程:提示:S锁是共享锁,X锁是互斥锁。一般来说,X锁和S锁和X锁是互斥的,S锁和S锁是不互斥的。从上面的流程我们可以看出,这个死锁的key需要获取S锁。为什么再次插入时需要获取S锁?因为我们需要检测唯一索引?如果要在RR隔离级别下读,就是当前读,那么其实还需要加S锁。这里发现uniquekey已经存在了。这时update的执行就会被两个事务的S锁阻塞,从而形成上面的循环等待状态。Tips:在MVCC中,currentread和snapshotread的区别:currentread每次都需要加锁(可以使用sharedlock或者mutexlock)来获取最新的数据,而snapshotread读取的是这个事务一开始的snapshot通过undolog实现。这就是整个僵局的原因。还有一种情况会发生这种死锁,那就是三个插入操作同时发生。如果先插入的事务被回滚,另外两个事务也会出现。这种僵局。解决方案这里的核心问题是摆脱S锁。这里提供三种方案供参考:将RR隔离级别降低为RC隔离级别。这里RC隔离级别会使用快照读,这样就不会加S锁。重新插入时,使用select*forupdate,加X锁,这样就不会加S锁了。可以提前加分布式锁,可以用Redis,或者ZK等,分布式锁可以参考我的这篇文章。谈论分布式锁的第一种方法不太现实,毕竟隔离级别不能轻易修改。第三种方法比较麻烦。所以第二种方法是我们绝对确定的。总结说了这么多,我们来做一个小总结。在排查死锁等问题时,有时光看死锁日志有时并不能解决问题。需要结合整个业务日志、代码、表结构进行分析,才能得到正确的结果。当然上面还有一些数据库锁的基础知识。不明白的可以查看我的另一篇文章Whydevelopersneedtounderstanddatabaselocks。***本文收录于JGrowing-CaseStudy社区共建的综合优秀Java学习路线。如果你想参与开源项目的维护,可以一起共建。github地址是:https://github.com/javagrowing/JGrowing请给个小star。