使用MySQL的过程中,大概率上都会遇到死锁问题,这实在是个令人头痛的问题。今天小编就来对死锁进行一个详细地分析,并结合常见的死锁案例进行探讨,同时,给出一些如何去尽可能避免死锁的建议,希望能给朋友们一些帮助和启发。
什么是死锁
死锁是并发系统中一个常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。
当两个及以上的事务,都在等待对方释放已经持有的锁,或因为加锁顺序不一致造成循环等待锁资源的时候,就会出现“死锁”。
常见的报错信息为Deadlockfoundwhentryingtogetlock...。
举例来说A事务持有X1锁,申请X2锁,B事务持有X2锁,申请X1锁。A和B事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。
如上图,是右侧的四辆汽车资源请求产生了回路现象,即死循环,导致了死锁。
从死锁的定义来看,MySQL出现死锁的几个要素为:
两个或者两个以上事务
每个事务都已经持有锁并且申请新的锁
锁资源同时只能被同一个事务持有或者不兼容
事务之间因为持有锁和申请锁导致彼此循环等待
InnoDB锁类型
为了分析死锁,我们有必要对InnoDB的锁类型有一个了解。
MySQLInnoDB引擎实现了标准的行级别锁:共享锁(Slock)和排他锁(Xlock)
不同事务可以同时对同一行记录加S锁。
如果一个事务对某一行记录加X锁,其他事务就不能加S锁或者X锁,从而导致锁等待。
如果事务T1持有行r的S锁,那么另一个事务T2请求r的锁时,会做如下处理:
T2请求S锁立即被允许,结果T1T2都持有r行的S锁
T2请求X锁不能被立即允许
如果T1持有r的X锁,那么T2请求r的X、S锁都不能被立即允许,T2必须等待T1释放X锁才可以,因为X锁与任何的锁都不兼容。
共享锁和排他锁的兼容性如下所示:
间隙锁(gaplock)
间隙锁锁住一个间隙以防止插入。假设索引列有2,4,8三个值,如果对4加锁,那么同时也会对(2,4)和(4,8)这两个间隙加锁。其他事务无法插入索引值在这两个间隙之间的记录。但是,间隙锁有个例外:
如果索引列是唯一索引,那么只会锁住这条记录(只加行锁),而不会锁住间隙。
对于联合索引且是唯一索引,如果where条件只包括联合索引的一部分,那么依然会加间隙锁。
next-keylock
next-keylock实际上就是行锁+这条记录前面的gaplock的组合。假设有索引值10,11,1和20,那么可能的next-keylock包括:
(负无穷,10],(10,11],(11,1],(1,20],(20,正无穷)
在RR隔离级别下,InnoDB使用next-keylock主要是防止幻读问题产生。
意向锁(Intentionlock)
InnoDB为了支持多粒度的加锁,允许行锁和表锁同时存在。为了支持在不同粒度上的加锁操作,InnoDB支持了额外的一种锁方式,称之为意向锁(IntentionLock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。意向锁分为两种:
意向共享锁(IS):事务有意向对表中的某些行加共享锁
意向排他锁(IX):事务有意向对表中的某些行加排他锁
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。表级意向锁与行级锁的兼容性如下所示:
插入意向锁(InsertIntentionlock)
插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同(如事务A插入,事务B插入4),那么就可以同时插入。
锁模式兼容矩阵
横向是已持有锁,纵向是正在请求的锁:
阅读死锁日志
在进行具体案例分析之前,咱们先了解下如何去读懂死锁日志,尽可能地使用死锁日志里面的信息来帮助我们来解决死锁问题。
后面测试用例的数据库场景如下:MySQL5.7事务隔离级别为RR。
表结构和数据如下:
测试用例如下:
通过执行showengineinnodbstatus可以查看到最近一次死锁的日志。
日志分析如下:
(1)TRANSACTION:TRANSACTION,ACTIVE6secstartingindexread
事务号为,活跃6秒,startingindexread表示事务状态为根据索引读取数据。常见的其他状态有:
mysqltablesinuse1说明当前的事务使用一个表。
locked1表示表上有一个表锁,对于DML语句为LOCK_IX
LOCKWAIT2lockstruct(s),heapsize,1rowlock(s)
LOCKWAIT表示正在等待锁,2lockstruct(s)表示trx-trx_locks锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。本用例中2locks表示IX锁和lock_modeX(Next-keylock)
1rowlock(s)表示当前事务持有的行记录锁/gap锁的个数。
MySQLthreadid7,OSthreadhandle,queryid.0.0.1rootupdating
MySQLthreadid7表示执行该事务的线程ID为7(即showprocesslist;展示的ID)
deletefromstudentwherestuno=5表示事务1正在执行的sql,比较难受的事情是showengineinnodbstatus是查看不到完整的sql的,通常显示当前正在等待锁的sql。
(1)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw.studenttrxidlock_modeXwaiting
RECORDLOCKS表示记录锁,此条内容表示事务1正在等待表student上的idx_stuno的X锁,本案例中其实是Next-KeyLock。
事务2的log和上面分析类似:
(2)HOLDSTHELOCK(S):
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw.studenttrxidlock_modeX
显示事务2的insertintostudent(stuno,score)values(2,10)持有了a=5的LockmodeX
LOCK_gap,不过我们从日志里面看不到事务2执行的deletefromstudentwherestuno=5;
这点也是造成DBA仅仅根据日志难以分析死锁的问题的根本原因。
(2)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw\***\*.\*\***studenttrxidlock_modeXlocksgapbeforerecinsertintentionwaiting
表示事务2的insert语句正在等待插入意向锁lock_modeXlocksgapbeforerecinsertintentionwaiting(LOCK_X+LOCK_REC_gap)
经典案例分析
案例一:事务并发insert唯一键冲突
表结构和数据如下所示:
测试用例如下:
日志分析如下:
事务T2insertintot7(id,a)values(26,10)语句insert成功,持有a=10的排他行锁(Xlocksrecbutnogap)
事务T1insertintot7(id,a)values(0,10),因为T2的第一条insert已经插入a=10的记录,事务T1inserta=10则发生唯一键冲突,需要申请对冲突的唯一索引加上SNext-keyLock(即lockmodeSwaiting)这是一个间隙锁会申请锁住(,10],(10,20]之间的gap区域。
事务T2insertintot7(id,a)values(40,9)该语句插入的a=9的值在事务T1申请的gap锁4-10之间,故需事务T2的第二条insert语句要等待事务T1的S-Next-keyLock锁释放,在日志中显示lock_modeXlocksgapbeforerecinsertintentionwaiting。
案例二:先update再insert的并发死锁问题
表结构如下,无数据:
测试用例如下:
死锁分析:
可以看到两个事务update不存在的记录,先后获得间隙锁(gap锁),gap锁之间是兼容的所以在update环节不会阻塞。两者都持有gap锁,然后去竞争插入意向锁。当存在其他会话持有gap锁的时候,当前会话申请不了插入意向锁,导致死锁。
如何尽可能避免死锁
合理的设计索引,区分度高的列放到组合索引前面,使业务SQL尽可能通过索引定位更少的行,减少锁竞争。
调整业务逻辑SQL执行顺序,避免update/delete长时间持有锁的SQL在事务前面。
避免大事务,尽量将大事务拆成多个小事务来处理,小事务发生锁冲突的几率也更小。
以固定的顺序访问表和行。比如两个更新数据的事务,事务A更新数据的顺序为1,2;事务B更新数据的顺序为2,1。这样可能会造成死锁。
在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如select…forupdate语句,如果是在事务里(运行了starttransaction或设置了auto