背景之前遇到的数据库死锁都是批量更新时加锁顺序不一致导致的死锁,但是上周遇到了一个比较难理解的死锁。借此机会重新学习一下mysql的死锁知识和常见的死锁场景。经过多方面的调查和同事的讨论,终于找到了造成这个死锁问题的原因,收获颇丰。虽然我们是后端程序员,不需要像DBA那样深入分析锁相关的源码,但是如果能够掌握基本的死锁排查方法,对我们的日常开发是大有裨益的。PS:本文不会介绍死锁的基础知识。关于mysql的加锁原理,可以参考本文参考资料中提供的链接。死锁的原因首先介绍数据库和表。由于涉及到公司内部的真实数据,下面做了模拟,不影响具体分析。我们使用的是5.5版本的mysql数据库,事务隔离级别默认为RR(Repeatable-Read),使用的是innodb引擎。假设有一张测试表:CREATETABLE`test`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`a`int(11)unsignedDEFAULTNULL,PRIMARYKEY(`id`),UNIQUEKEY`a`(`a`))ENGINE=InnoDBAUTO_INCREMENT=100DEFAULTCHARSET=utf8;表的结构很简单,一个主键id,另一个***索引a。表中数据如下:mysql>select*fromtest;+----+------+|id|a|+----+------+|1|1||2|2||4|4|+----+------+3rowsinset(0.00sec)死锁操作如下:Steptransaction1transaction21begin2deletefromtestwherea=2;3begin4deletefromtestwherea=2;(事务1卡住)5提示死锁:ERROR1213(40001):Deadlockfoundwhentryingtogetlock;尝试重启事务insertintotest(id,a)values(10,2);然后我们可以通过SHOWENGINEINNODBSTATUS查看死锁日志;----------------------最新检测到死锁------------------------17021913:31:31***(1)交易:交易2A8BD,ACTIVE11secstartingindexreadmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize376,1rowlock(s)MySQLthreadid448218,OSthreadhandle0x2abe5fb5d700,queryid18923238renjun.fangcloud.net121.41.41.92rootupdatingdeletefromtestwherea=2***(1)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BDlock_modeXwaitingRecordlock,heapno3PHYSICALRECORD:n_fields2;compactformat;infobits320:len4;hex00000002;asc;;1:len4;hex00000002;asc;;***(2)TRANSACTION:TRANSACTION2A8BC,ACTIVE18secinsertingmysqltablesinuse1,locked14lockstruct(s),heapsize1248,3rowlock(s),undologentries2MySQLthreadid448217,OSthreadhandle0x2abe5fd65700,queryid18923239renjun.fangcloud.net121.41.41.92rootupdateinsertintotest(id,a)values(10,2)***(2)HOLDSTHELOCK(S):RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BClock_modeXlocksrecbutnotgapRecordlock,heapno3PHYSICALRECORD:n_fields2;compactformat;infobits320:len4;hex00000001;as4;as4;hex00000002;asc;;***(2)等待这个锁被授予:RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BClockmodeSwaitingRecordlock,heapno3PHYSICALRECORD:n_fields2;compactformat;infobits320:len4;hex00000002;asc;;1:len4;hex00000002;asc;第一步是读取死锁日志。死锁日志通常分为两部分。上半部分显示事务1正在等待什么锁:17021913:31:31***(1)TRANSACTION:TRANSACTION2A8BD,ACTIVE11secstartingindexreadmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize376,1rowlock(s)MySQLthreadid448218,OSthreadhandle0x2abe5fbjunnet18d700,32queryid141.41.92rootupdatingdeletefromtestwherea=2***(1)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BDlock_modeXwaitingRecordlock,heapno3PHYSICALRECORD:n_fields2;紧凑格式;infobits320:len4;hex00000002;asc;;1:len4;hex00000002;asc;;从日志中可以看出事务1当前正在执行deletefromtestwherea=2,而这条语句是在申请索引a的X锁,所以提示lock_modeXwaiting。然后日志下半部分显示事务2当前持有的锁和正在等待的锁:***(2)TRANSACTION:TRANSACTION2A8BC,ACTIVE18secinsertingmysqltablesinuse1,locked14lockstruct(s),heapsize1248,3rowlock(s),undologentries2MySQLthreadid448217,OSthreadhandle0x2abe5fd65700,2queryid1fangcloud.net121.41.41.92rootupdateinsertintotest(id,a)values(10,2)***(2)HOLDSTHELOCK(S):RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BClock_modeXlocksrecbutnotgapRecordlock,heapfieldECORDHYSICALRformats2:compfieldECORDHYSICALR格式;infobits320:len4;hex00000002;asc;;1:len4;hex00000002;asc;;***(2)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid0pageno923nbits80index`a`oftable`oauthdemo`.`test`trxid2A8BClockmodeSwaitingRecordlock,heapno3PHYSICALRECORD:n_fields2;compactformat;infobits320:len4;hex00000002;asc;;1:len4;hex00000002;asc;;从日志的HOLDSTHELOCKS(S)块可以看出,事务2持有索引a的X锁,是记录锁(RecordLock)。该锁是由第2步中事务2执行的delete语句请求的,由于是RR隔离模式下基于***索引(其中a=2)的等价查询,所以会申请记录锁,而不是a下一键锁。从日志的WAITINGFORTHISLOCKTOBEGRANTED块可以看出,事务2正在申请一个S锁,这是一个共享锁。insertintotest(id,a)values(10,2)语句请求锁。一般情况下insert语句会申请排他锁,也就是X锁,但是这里出现了S锁。这是因为a字段是独占索引,所以insert语句在插入前会进行重复键检查。为了让这个检查成功,需要申请一个S锁,防止其他事务修改a字段。那么为什么这个S锁会失效呢?这是申请需要排队的同一个字段的锁。S锁前面有未申请的X锁,所以S锁必须等待,于是形成循环等待,发生死锁。通过阅读死锁日志,我们可以清楚的知道这两个事务形成了什么样的循环等待。进一步分析,我们可以逆向推断循环等待的原因,也就是死锁的原因。死锁形成流程图为了让大家更好的理解死锁形成的原因,我们将死锁形成的过程以表格的形式进行说明:StepTransaction1Transaction21begin2deletefromtestwherea=2;执行成功,事务2持有a=2下的X锁,类型为记录锁。3begin4deletefromtestwherea=2;事务1想在a=2下申请X锁,但是由于事务2已经申请了X锁,两个X锁互斥,所以X锁申请进入锁请求队列。5发生死锁,事务1的权重较小,所以选择回滚(成为牺牲品)。插入测试(id,a)值(10,2);由于a字段已经建立了***索引,所以需要申请S锁来查重键。由于插入的a的值还是2,所以排在X锁后面。但是之前的X锁申请只能在事务2commit或者rollback之后才能成功。此时形成循环等待,发生死锁。在排查死锁的过程中,有同事也发现上面的场景会造成另一个死锁,无法手动重现,只能在高并发场景下重现。这个死锁对应的日志这里就不贴了。与之前死锁的核心区别在于事务2的锁等待由S锁变为X锁,即lock_modeXlocksgapbeforerecinsertintentionwaiting。我们还是用表格来详细描述死锁的过程:StepsTransaction1Transaction21begin2deletefromtestwherea=2;执行成功,事务2占用a=2下的X锁,类型为记录锁。3begin4[InsertPhase1]insertintotest(id,a)values(10,2);事务2申请S锁进行重复键校验。检查成功。5从测试中删除a=2;事务1想在a=2下申请X锁,但是由于事务2已经申请了X锁,两个X锁互斥,所以X锁申请进入锁请求队列。6发生死锁,事务1的权重较小,所以选择回滚(成为牺牲品)。【插入的第2阶段】insertintotest(id,a)values(10,2);事务2开始插入数据,S锁升级为X锁,类型为insertintention。同理,X锁进入队列排队,形成循环等待,死锁发生。总结和排查死锁,首先需要根据死锁日志分析循环等待场景,然后根据每个事务执行的SQL分析锁的类型和顺序,逆向推断如何形成循环等待,从而你可以找到死锁的原因原因就没有了。PS:以上分析是根据经验推断的。希望其他朋友指出错误和不足之处。谢谢你!
