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

教你如何分析MySQL死锁问题

时间:2023-03-16 18:11:48 科技观察

前言前几天跟朋友分析了一个死锁问题,才有了这篇图文并茂的详细博文,哈哈~发生了死锁,如何排查解决它?本文将和大家讨论这个问题准备数据环境模拟死锁事件分析死锁日志分析死锁结果环境准备数据库隔离级别:mysql>select@@tx_isolation;+------------------+|@@tx_isolation|+----------------+|可重复读|+----------------+1rowinset,1warning(0.00sec)autocommitoff:mysql>setautocommit=0;QueryOK,0rowsaffected(0.00sec)mysql>select@@autocommit;+-------------+|@@autocommit|+------------+|0|+------------+1rowinset(0.00sec)tablestructure://idisanauto-自增主键,name为非唯一索引,balance公共字段CREATETABLE`account`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(255)DEFAULTNULL,`balance`int(11)DEFAULTNULL,PRIMARYKEY(`id`),KEY`idx_name`(`name`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8;表中数据:模拟并发,开启两个终端模拟事务并发,执行顺序和实验现象如下:1)事务A执行更新操作,更新成功mysql>updateaccountsetbalance=1000wherename='Wei';QueryOK,1rowaffected(0.01sec)2)事务B执行更新操作,更新成功mysql>updateaccountsetbalance=1000wherename='Eason';QueryOK,1rowaffected(0.01sec))3)事务A执行插入操作,卡死~我的ql>insertintoaccountvalues(null,'Jay',100);这时候可以使用select*frominformation_schema.innodb_locks;查看锁情况:4)事务B执行插入操作,插入成功,事务A的插入由阻塞变为死锁errormysql>insertintoaccountvalues(null,'Yan',100);QueryOK,1rowaffected(0.01sec)锁介绍在分析死锁日志之前,先来做一个锁介绍,哈哈~主要介绍兼容性和锁模式类型锁:共享锁InnoDB用排他锁实现了标准的行级锁,包括两种类型:共享锁(简称称为s锁)和独占锁(称为x锁)。共享锁(S锁):允许持有锁的事务读取一行。独占锁(X锁):允许持有锁的事务更新或删除一行。如果事务T1持有行r的s锁,当另一个事务T2请求r的锁时,它会做如下的事情:T2请求s锁并立即被允许,结果T1和T2都持有s锁行r和T2请求x锁不能立即被授予如果T1持有r的x锁,那么T2对r的x和s锁的请求不能立即被授予。T2必须等待T1释放x锁,因为X锁与任何锁都不兼容。意向锁意向共享锁(IS锁):事务想获得表中某行的共享锁意向锁(IX锁):事务想获得表中某行的独占锁例如:事务1在表上1加了S锁后,事务2如果要改变某行记录,需要加IX锁。因为不兼容,需要等待S锁释放;如果事务1给表1加了一个IS锁,那么事务2加的IX锁和IS锁兼容的话就可以操作,实现了更细粒度的加锁。InnoDB存储引擎中锁的兼容性如下:记录锁(RecordLocks)记录锁是最简单的行锁,只锁一行。例如:SELECTc1FROMtWHEREc1=10FORUPDATE记录锁总是加在索引上。即使一个表没有索引,InnoDB也会隐式地创建一个索引,并使用这个索引来实现记录锁。会阻塞其他事务插入、更新、删除记录锁的事务数据(关键字:lock_modeXlocksrecbutnotgap),记录如下:infobits00:len4;hex8000000a;asc;;1:len6;hex00000000274f;asc'O;;2:len7;hexb60000019d0110;asc;;GapLocksGapLocks是加在两个索引之间的锁,或者在第一个索引之前加一个gap,或在最后一个索引之后。gaplock锁定的是一个区间,而不仅仅是这个区间内的每一条数据。间隙锁只是阻止其他事务插入间隙,不会阻止其他事务获取同一间隙上的间隙锁,所以gapxlock和gapslock的作用是一样的。间隔锁的事务数据(关键词:gapbeforerec),记录如下:RECORDLOCKSspaceid177pageno4nbits80indexidx_nameoftable`test2`.`account`trxid38049lock_modeXlocksgapbeforerecRecordlock,heapno6PHYSICALRECORD:n_fields2;compactformat;infobits00:len3;hex576569;ascWei;020he;;18:len4;Next-KeyLocksNext-keylock是recordlock和gaplock的结合,指的是加在一条记录上的锁和这条记录前面的gap。插入意向锁(InsertIntention)插入意向锁是在插入一行记录之前设置的间隙锁。这个锁释放一个插入模式的信号,即当多个事务插入同一个索引间隙时,如果没有插入到间隙中的同一个位置,则不需要互相等待。假设有索引值4和7,几个不同的事务准备插入5和6,每个锁在获得插入行的排它锁之前用插入意向锁锁住4和7之间的间隙,但是确实notblock对方不会因为插入的行而冲突。事务数据类似于下面:RECORDLOCKSspaceid31pageno3nbits72index`PRIMARY`oftable`test`.`child`trxid8731lock_modeXlocksgapbeforerecinsertintentionwaitingRecordlock,heapno3PHYSICALRECORD:n_fields3;compactformat;infobits00:len4;hex80000066;ascf;;1:len6;hex000000002215;asc";;2:len7;hex9000000172011c;ascr;;...Lockmodecompatibilitymatrix(横的是持有的锁,竖的是正在申请的锁):如何读取死锁日志showengineinnodbstatus可以使用showengineinnodbstatus查看最新的死锁Log哈~,执行后死锁日志如下:0x243c***(1)TRANSACTION:TRANSACTION38048,ACTIVE92secinsertingmysqltablesinuse1,locked1LOCKWAIT4lockstruct(s),heapsize1136,4rowlock(s),undologentries2MySQLthreadid53,OSinsthreadhandle2300,queryid2362localvalueertupacdateinuse1:',100)***(1)WAITINGFORTISLOCKTOBEGRANTED:RECORDLOCKSspaceid177pageno4nbits80indexidx_nameoftable`test2`.`account`trxid38048lock_modeXlocksgapbeforerecinsertintentionwaitingRecord锁,heapno6PHYSICALRECORD:n_fields2;compactformat;infobits00:len3;hex576569;ascWei;;1:len4;hex80000002;asc;;***(2)TRANSACTION:TRANSACTION38049,ACTIVE72secinserting,threaddeclaredinsideInnoDB5000mysqltablesinuse1,locked15lockstruct(s),heapsize1136,4rowlock(s),undologentries2MySQLthreadid52,OSthreadhandle9276,queryid2363localhost::1rootupdateinsertintoaccountvalues(null,'Yan',100)***(2)HOLDSTHELOCK(S):RECORDLOCKSspaceid177pageno4nbits80indexidx_nameoftable`test2`.`account`trxid38049lock_modeXlocksgapbeforerecRecordlock,heapno6PHYSICALRECORD:n_fields2;compactformat;infobits00:len3;hex576569;ascWei;;1:len4;hex80000002;asc;;***(2)WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid177pageno4nbits80indexidx_nameoftable`test2`.`account`trxid38049lock_modeXinsertintentionwaitingRecordlock,heapno1PHYSICALRECORD:n_fields1;compactformat;infobits00:len8;hex73757072656d756d;ascsupremum;;***WEROLLBACKTRANSACTION(1)我们如何分析上面的死锁日志?Part11)找到关键字TRANSACTION,事务380482)查看正在执行的SQLinsertintoaccountvalues(null,'Jay',100)3)等待锁释放(WAITINGFORTHISLOCKTOBEGRANTED),插入意向排他锁(lockmodeXlocksgapbeforerecinsertintentionwaiting),普通索引(idxname),物理记录(PHYSICALRECORD),间隙间隔(unknown,Wei);第二部分1)找到关键字TRANSACTION,事务380492)查看正在执行的SQLinsertintoaccountvalues(null,'Yan',100)3)持有锁(HOLDSTHELOCK),间隙锁(lockmodeXlocksgapbeforerec),普通索引(indexidxname),physicalrecord(物理记录),intervals(unknown,wei);4)正在等待锁释放(waitingforthislocktobegranted),插入意向锁(lockmodeXinsertintentionwaiting),on普通索引(indexidxname),物理记录(physicalrecord),间隙间隔(unknown,+∞);5)Transaction1rollsback(我们回滚事务1);检查日志结果查看日志得到:事务A在等待insert意向排他锁(事务A是日志的事务1,根据insert语句查座),是在事务B的怀抱中~事务B持有间隙锁,正在等待插入意向排他锁。可能有些朋友会有疑惑。事务A持有什么样的锁?日志分不清要拿什么样的insert意向排他锁呢?事务B具体采取了什么间隙锁?为什么还要带插入意向锁呢?死锁循环是如何形成的?目前日志没有显示死循环的构成?下节详细分析一波一波~死锁分析死锁死循环四要素互斥条件:指一个进程独占使用已分配的资源,即某个资源只被占用在一段时间内通过一个过程。如果此时有其他进程在请求资源,请求者只能等到占用资源的进程用完释放。请求和持有条件:表示一个进程至少保留了一个资源,但是又提出了新的资源请求,并且该资源已经被其他进程占用。此时,请求进程被阻塞,但它仍然持有它已经获得的其他资源。非剥夺条件:指进程已经获取的资源,在用完之前不能剥夺,用完只能自己释放。循环等待条件:当死锁发生时,必然有一个进程——一个资源环链,即进程集{P0,P1,P2,...,Pn}中的P0正在等待P1占用的资源;P1在等待P2占用的资源,...,Pn在等待P0已经占用的资源。事务A持有什么锁?想拿什么样的insertintent排它锁?为了记录方便,例子中用W代表Wei,J代表Jay,E代表Eason~我们先分析一下事务A中update语句的锁情况~updateaccountsetbalance=1000wherename='Wei';间隙锁:Update语句会对非唯一索引的name加上左区间的间隙锁和右区间的间隙锁(因为当前记录的表中只有一个name='wei',所以中间没有gaplock~),也就是(E,W)和(W,+∞)为什么会有gaplock?因为这是RR的数据库隔离级别,用来解决幻读问题的~recordlock因为name是索引,所以update语句肯定会加上W的recordlockNext-KeylockNext-Keylock=recordlock+gaplock,所以update语句会有(E,W]Next-Keylock上面说过,事务A执行update语句后,会持有一把锁:Next-keyLock:(E,W]GapLock:(W,+∞)我们再来分析一下事务A中insert语句的加锁情况gaplock(E,W)插入意向锁(InsertIntention)插入意向锁是在一行记录操作前设置的一个gaplock。这个锁释放一个insertmode的信号,即事务A需要insert意向锁(E,W),因此事务A的update语句和insert语句执行完后,是一个连续的(E,W]的Next-Key锁和(W,+∞)的Gap锁,想获取(E,W)的插入意向的独占锁,等待锁匹配死锁日志。哈哈~事务B有什么间隙锁?为什么还要带插入意向锁呢?同样我们来分析一波事务B,update语句的锁分析:updateaccountsetbalance=1000wherename='Eason';gaplock:updatestatement会在非唯一索引的name上加上左区间的gaplock和右区间的gaplock(因为当前表只有一条name='Eason'的记录,所以中间没有间隙锁~),也就是(-∞,E)和(E,W)记录锁,因为name是索引quote,所以update语句肯定会加上E的recordlockNext-KeylockNext-Keylock=recordlock+gaplock,所以update语句会有(-∞,E]的Next-Keylock总结综上所述,事务B执行更新语句后,会持有锁:Next-keyLock:(-∞,E]GapLock:(E,W)下面分析一下insert语句的加锁情况一波Binsertintoaccountvalues(null,'Yan',100);间隙锁:因为Yan(Y在W之后),所以需要请求间隙锁加(W,+∞)插入意向锁(InsertIntention)插入意向锁是在插入一行记录之前设置了一个间隙锁,这个锁释放一个插入模式的信号,即事务A需要插入一个意向锁(W,+∞)。因此,在执行事务B的update语句和insert语句,持有(-∞,E]的Next-Key锁,(E,W)的Gap锁,如果想得到(W,+∞)的gap锁定,即插入意向排他锁,加锁情况也和死锁日志一致还原死锁真相接下来让我们一起还原死锁真相~哈哈~执行完UpdateWei语句后,事务A持有Next-keyLockof(E,W],和(W,+∞)GapLock的next-key锁,插入成功~事务B执行UpdateEason语句,持有(-∞,E]的Next-KeyLock,以及(E,W)的GapLock,插入成功~当事务A执行InsertJay的语句时,因为需要(E,W)的插入意向锁,但是(E,W)在事务B的怀抱,所以卡住了~当事务B执行InsertYan的语句时,因为需要(W,+∞)的插入意向锁,但是(W,+∞)在事务A的怀抱中,所以它也卡住了事务A持有(W,+∞)的GapLock,等待(E,W)的插入意向锁,事务B持有(E,W)的Gap锁,等待插入意向锁of(W,+∞)Lock,这样就形成了死锁闭环~(间隙锁和插入意向锁会发生冲突,可以看锁引入的锁模式兼容矩阵~)事务A和B形成死锁关闭后循环,因为Innodb的底层机制,会让其中一个事务放弃资源,另一个事务执行成功,这就是为什么最后看到事务B插入成功,但是事务插入A显示Deadlockfound~总结最后,当我们遇到死锁问题时,我们应该如何分析呢?模拟死锁场景showengineinnodbstatus;查看死锁日志找出死锁SQLSQLSQL锁分析,可以去官网看看,分析死锁日志(持有什么锁,等待什么锁)熟悉锁模式兼容性矩阵,InnoDB存储引擎中锁的兼容性矩阵。