死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为”Deadlockfoundwhentryingtogetlock...”。
举例来说A事务持有X1锁,申请X2锁,B事务持有X2锁,申请X1锁。A和B事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。
如上图,是右侧的四辆汽车资源请求产生了回路现象,即死循环,导致了死锁。
从死锁的定义来看,MySQL出现死锁的几个要素为:
a.两个或者两个以上事务
b.每个事务都已经持有锁并且申请新的锁
c.锁资源同时只能被同一个事务持有或者不兼容
d.事务之间因为持有锁和申请锁导致彼此循环等待
说明:后续内容实验环境为5.7版本,隔离级别为RR(可重复读)
为了分析死锁,我们有必要对InnoDB的锁类型有一个了解。
MySQLInnoDB引擎实现了标准的行级别锁:共享锁(Slock)和排他锁(Xlock)
如果事务T1持有行r的S锁,那么另一个事务T2请求r的锁时,会做如下处理:
如果T1持有r的X锁,那么T2请求r的X、S锁都不能被立即允许,T2必须等待T1释放X锁才可以,因为X锁与任何的锁都不兼容。共享锁和排他锁的兼容性如下所示:
间隙锁锁住一个间隙以防止插入。假设索引列有2,4,8三个值,如果对4加锁,那么也会同时对(2,4)和(4,8)这两个间隙加锁。其他事务无法插入索引值在这两个间隙之间的记录。但是,间隙锁有个例外:
next-keylock实际上就是行锁+这条记录前面的gaplock的组合。假设有索引值10,11,13和20,那么可能的next-keylock包括:
(负无穷,10]
(10,11]
(11,13]
(13,20]
(20,正无穷)
在RR隔离级别下,InnoDB使用next-keylock主要是防止幻读问题产生。
InnoDB为了支持多粒度的加锁,允许行锁和表锁同时存在。为了支持在不同粒度上的加锁操作,InnoDB支持了额外的一种锁方式,称之为意向锁(IntentionLock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。意向锁分为两种:
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。表级意向锁与行级锁的兼容性如下所示:
插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同(如事务A插入3,事务B插入4),那么就可以同时插入。
横向是已持有锁,纵向是正在请求的锁:
一个温馨小提示:xmen平台支持查看死锁主库的死锁日志,访问方式如下:
在进行具体案例分析之前,咱们先了解下如何去读懂死锁日志,尽可能地使用死锁日志里面的信息来帮助我们来解决死锁问题。
后面测试用例的数据库场景如下:
MySQL5.7事务隔离级别为RR
表结构和数据如下:
通过执行showengineinnodbstatus可以查看到最近一次死锁的日志。
***(1)TRANSACTION:
TRANSACTION2322,ACTIVE6secstartingindexread
事务号为2322,活跃6秒,startingindexread表示事务状态为根据索引读取数据。常见的其他状态有:
mysqltablesinuse1说明当前的事务使用一个表。
locked1表示表上有一个表锁,对于DML语句为LOCK_IX
LOCKWAIT2lockstruct(s),heapsize1136,1rowlock(s)
LOCKWAIT表示正在等待锁,2lockstruct(s)表示trx->trx_locks锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。本用例中2locks表示IX锁和lock_modeX(Next-keylock)
1rowlock(s)表示当前事务持有的行记录锁/gap锁的个数。
MySQLthreadid37,OSthreadhandle140445500716800,queryid1234127.0.0.1rootupdating
MySQLthreadid37表示执行该事务的线程ID为37(即showprocesslist;展示的ID)
deletefromstudentwherestuno=5表示事务1正在执行的sql,比较难受的事情是showengineinnodbstatus是查看不到完整的sql的,通常显示当前正在等待锁的sql。
***(1)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw**.**studenttrxid2322lock_modeXwaiting
RECORDLOCKS表示记录锁,此条内容表示事务1正在等待表student上的idx_stuno的X锁,本案例中其实是Next-KeyLock。
事务2的log和上面分析类似:
***(2)HOLDSTHELOCK(S):
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw**.**studenttrxid2321lock_modeX
显示事务2的insertintostudent(stuno,score)values(2,10)持有了a=5的LockmodeX
|LOCK_gap,不过我们从日志里面看不到事务2执行的deletefromstudentwherestuno=5;
这点也是造成DBA仅仅根据日志难以分析死锁的问题的根本原因。
***(2)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid11pageno5nbits72indexidx_stunooftablecw**.**studenttrxid2321lock_modeXlocksgapbeforerecinsertintentionwaiting
表示事务2的insert语句正在等待插入意向锁lock_modeXlocksgapbeforerecinsertintentionwaiting(LOCK_X+LOCK_REC_gap)
重点说明下delete不存在的记录是要加上gap锁,事务日志中显示lock_modeXlocksgapbeforerec.
记录不存在,导致T2先持有了(lock_modeXlocksgapbeforerec)锁住
(2,20,1,1,’ratail’,1,0)-(3,30,1,’retail’,1,0)的区间,防止符合条件的记录插入。
总结来说,就是T1(insert)等待T2(delete),T2(insert)等待T1(delete)故而循环等待,出现死锁。
表结构和数据如下所示:
1.事务T2insertintot7(id,a)values(26,10)语句insert成功,持有a=10的排他行锁(X
locksrecbutnogap)
2.事务T1insertintot7(id,a)values(30,10),因为T2的第一条insert已经插入a=10的记录,
事务T1inserta=10则发生唯一键冲突,需要申请对冲突的唯一索引加上SNext-keyLock
(即lockmodeSwaiting)这是一个间隙锁会申请锁住(,10],(10,20]之间的gap区域。
3.事务T2insertintot7(id,a)values(40,9)该语句插入的a=9的值在事务T1申请的gap锁
4,10之间,故需事务T2的第二条insert语句要等待事务T1的S-Next-keyLock锁释放,
在日志中显示lock_modeXlocksgapbeforerecinsertintentionwaiting。
首先要理解的是对同一个字段申请加锁是需要排队的。
其次表tx中索引idx_c1为非唯一普通索引。
(1).T2执行selectforupdate操作持有记录id=30的主键行锁:PRIMARYoftabletest.txtrxid2077lock_modeXlocksrecbutnotgap。
(2).T1语句update通过普通索引idx_c1更新c2,先获取idx_c1c1=5的X锁lock_modeXlocksrecbutnotgap,然后去申请对应主键id=30的行锁,但是T2已经持有主键的行数,于是T1等待。
(3).T2执行根据主键id=30删除记录,需要申请id=30的行锁以及c1=5的索引行锁。但是T1尚及持有该锁,故会出现indexidx_c1oftabletest.txtrxid2077lock_modeXlocksrecbutnotgapwaiting.
T2(delete)等待T1(update),T1(update)等待T2(selectforupdate)循环等待,造成死锁。
可以看到两个事务update不存在的记录,先后获得间隙锁(gap锁),gap锁之间是兼容的所以在update环节不会阻塞。两者都持有gap锁,然后去竞争插入意向锁。当存在其他会话持有gap锁的时候,当前会话申请不了插入意向锁,导致死锁。
1.合理的设计索引,区分度高的列放到组合索引前面,使业务SQL尽可能通过索引定位更少的行,减少
锁竞争。
3.避免大事务,尽量将大事务拆成多个小事务来处理,小事务发生锁冲突的几率也更小。
4.以固定的顺序访问表和行。比如两个更新数据的事务,事务A更新数据的顺序为1,2;事
务B更新数据的顺序为2,1。这样更可能会造成死锁。
5.在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如select…for
update语句,如果是在事务里(运行了starttransaction或设置了autocommit等于0),
那么就会锁定所查找到的记录。
6.尽量按主键/索引去查找记录,范围查找增加了锁冲突的可能性,也不要利用数据库做一些
额外额度计算工作。比如有的程序会用到“select…where…orderbyrand();”
这样的语句,由于类似这样的语句用不到索引,因此将导致整个表的数据都被锁住。
7.优化SQL和表设计,减少同时占用太多资源的情况。比如说,减少连接的表,将复杂SQL