6个MySQL死锁案例原因分析及死锁预防策略,分享给大家。Mysql锁类型及锁分析MySQL有三种锁级别:页级、表级、行级。**表级锁:**开销小,加锁速度快;没有死锁;锁粒度大,锁冲突概率最高,并发度最低。**行级锁:**开销大,加锁慢;会有死锁;加锁粒度最小,锁冲突概率最低,并发度也最高。页锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁粒度介于表锁和行锁之间,并发算法:下一个KeyLocks锁,同时锁住记录(数据),锁住记录前面的Gap?Gap锁,不锁记录,只记录前一个GapRecordlock锁(锁数据,不锁Gap)所以其实,Next-KeyLocks=Gap锁+Recordlock锁死锁的原因和例子所谓死锁:是指两个或多个进程在执行过程中因争夺资源而相互等待的现象。如果没有外力,他们将无法前进。这时候,系统就被称为死锁状态或者系统出现了死锁,而这些一直在等待对方的进程就称为死锁进程。表级锁不会造成死锁。所以死锁的解决方法主要针对最常用的InnoDB。死锁的关键是两个(或多个)Session加锁的顺序不一致。那么对应的解决死锁问题的关键就是:让不同的session被锁住,从而产生例题1需求:将投资资金分成几份,随机分配给借款人。起初,业务程序的思路是这样的:投资人投资后,金额随机分成几份,然后从借款人表中随机抽取几份,然后借款人表中的余额为通过selectforupdate一一更新。例如两个用户同时投资,用户A的金额随机分成2份分配给借款人1和借款人2,用户B的金额随机分成2份分配给借款人2和1.由于加锁顺序不同,当然很快就会出现死锁。这个问题的改进很简单,一次性锁定所有分配的借款人即可。select*fromxxxwhereidin(xx,xx,xx)forupdatelistvaluesmysqlinin会自动从小到大排序,锁从小到大一个一个加。例如(以下sessionid为主键):Session1:mysql>select*fromt3whereidin(8,9)forupdate;+----+------+------+-------------------+|id|课程|名称|ctime|+----+--------+------+--------------------+|8|WA|f|2016-03-0211:36:30||9|JX|f|2016-03-0111:36:30|+----+--------+-----+--------------------+rowsinset(0.04sec)Session2:select*fromt3whereidin(10,8,5)forupdate;lockwaiting...其实id=10的记录此时并没有被锁定,而是id=5的记录已经被锁定,锁在等待id=8的记录。不信请看:Session3:mysql>select*fromt3whereid=5forupdate;锁定等待Session4:mysql>select*fromt3whereid=10forupdate;+----+--------+------+-------------------+|id|course|name|ctime|+----+------+-----+------------------+|10|JB|g|2016-03-1011:45:05|+----+---------+------+------------------+rowinset(0.00sec)在其他session中,id=5不能加锁,id=10可以加锁。案例2开发中经常会做这种判断需求:根据字段值(带索引)查询,不存在则插入;否则更新它。以id为主键为例,当前没有id=22的行以id为主键,当前没有id=22的行Session1:select*fromt3whereid=22forupdate;Emptyset(0.00sec)session2:select*fromt3whereid=23forupdate;Emptyset(0.00sec)Session1:insertintot3values(22,'ac','a',now());锁定等待...Session2:insertintot3values(23,'bc','b',now());ERROR1213(40001):尝试获取锁时发现死锁;尝试重新启动事务锁定现有行(主键)时,mysql只有行锁。当锁定一个不存在的行时(即使条件是主键),mysql会锁定一个范围(用间隙锁)。锁定范围为:无穷小或小于表中锁定id的最大值,无穷大或大于表中锁定id的最小值,如:如果表中已经存在id(11,12),然后锁定(12,无穷大)。如果表中已有id为(11,30),则锁定(11,30)。这个死锁的解决方案是:insertintot3(xx,xx)onduplicatekeyupdate?xx='XX';使用mysql特定的语法来解决这个问题。因为插入语句是针对主键的,所以无论插入的行是否存在,都只会有一个行锁。案例3mysql>select*fromt3whereid=9forupdate;+----+--------+------+-------------------+|id|course|name|ctime|+----+--------+------+---------------------+|9|JX|f|2016-03-0111:36:30|+----+------+------+-------------------+rowinset(0.00sec)Session2:mysql>select*fromt3whereid<20forupdate;LockwaitingSession1:mysql>insertintot3values(7,'ae','a',now());ERROR1213(40001):Deadlockfoundwhentryingtogetlock;tryrestartingtransaction这个和Case1等类似,但是session1不按常理卡是played,Session2在等待Session1的id=9的锁,session2持有1到8的锁(注意9到19的范围没有被session2锁),最后session1不得不插入新行等待session2,所以出现死锁。这个在业务需求中一般不会出现,因为你锁定了id=9,但是想插入id=7的行,有点跳动,当然要有解决办法,那就是重新组织业务需求,避免这样写。案例4一般情况下,两个session通过一个sql各自持有一个锁,然后访问对方的加锁数据产生死锁。Case5两条单条SQL语句涉及相同的加锁数据,但加锁顺序不同,导致死锁。Case6死锁场景如下:CREATETABEdltask(idbigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'autoid',avarchar(30)NOTNULLCOMMENT'uniq.a',bvarchar(30)NOTNULLCOMMENT'uniq.b',cvarchar(30)NOTNULLCOMMENT'uniq.c',xvarchar(30)NOTNULLCOMMENT'data',PRIMARYKEY(id),UNIQUEKEYuniq_a_b_c(a,b,c))ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='deadlocktest';a、b、c三列组合成唯一索引,主键索引为id列。事务隔离级别:RR(RepeatableRead)每个事务只有一条SQL:deletefromdltaskwherea=?相反,该记录被标记为已删除。(注意:这些标记为已删除的记录会被后台的Purge操作恢复并物理删除,但处于删除状态的记录会在索引中保存一段时间。)在RR隔离级别下,唯一索引满足查询条件,但是是删除记录,如何加锁?InnoDB这里的处理策略不同于前面两种策略,或者说是前两种策略的结合:对于满足条件的删除记录,InnoDB会在记录上加下一个键锁X(给记录本身加上X锁,以及同时对记录前的GAP进行加锁,防止插入满足条件的新记录。)唯一查询,三种情况,对应三种加锁策略,总结如下:这里,我们看到下一个key加锁,是不是熟悉的?对了,上一个死锁中事务1和事务2处于等待状态的锁都是nextkey锁。理解了这三种加锁策略,其实构造了一定的并发场景,死锁的原因就呼之欲出了。但是,还有一个前提策略需要引入,那就是InnoDB内部采用的死锁预防策略。找到一条满足条件的记录,且该记录有效,则对该记录加X锁,NoGap锁(lock_modeXlocksrecbutnotgap);找到满足条件的记录,但该记录无效(标记为已删除记录),然后锁定该记录Addnextkeylock(同时锁定记录本身和记录之前的Gap:lock_modeX);如果没有找到满足条件的记录,则对第一条不满足条件的记录加Gap锁,保证不插入满足条件的记录(locksgapbeforerec);死锁预防策略在InnoDB引擎内部(或者说所有数据库内部),存在多种锁:事务锁(行锁、表锁)、Mutex(保护内部共享变量操作)、RWLock(也称为Latch,保护内部页面阅读和修改)。InnoDB的每一页都是16K。读取页面时,需要给页面加S锁。更新页面时,需要给页面加X锁。不管怎样,当一个页面被操作时,该页面就会被锁定。添加页锁后,页中存储的索引记录不会被并发修改。因此,为了修改一条记录,InnoDB内部是如何处理的:根据给定的查询条件,找到对应记录所在的页面;给页面加X锁(RWLock),然后在页面中查找满足条件的记录;在有锁的情况下,对满足条件的记录加事务锁(行锁:根据记录是否满足查询条件,记录是否被删除,对应上述三种加锁策略之一);死锁预防策略:相对于事务锁,页锁是短期持有的锁,而事务锁(行锁、表锁)是长期持有的锁。因此,为了防止页锁和事务锁之间产生死锁。InnoDB有一个死锁预防策略:持有事务锁(行锁、表锁),可以等待获取页锁;但是反过来,持有页锁,就等不及持有事务锁了。根据防死锁策略,持有页锁加行锁时,如果行锁需要等待。然后释放页锁,然后等待行锁。此时获取行锁没有任何锁保护,所以加行锁后,记录可能已经被并发修改了。所以这时候应该加回页锁,重新判断记录的状态,在页锁的保护下重新给记录加锁。如果此时没有并发修改记录,第二次加锁可以很快完成,因为已经持有了相同模式的锁。但是,如果记录已经被并发修改过,则可能会导致本文前面提到的死锁问题。以上InnoDB的死锁预防处理逻辑,对应的函数是row0sel.c::row_search_for_mysql()。有兴趣的朋友,可以跟踪调试这个函数的处理流程。很复杂,但是浓缩了InnoDB的精髓。分析死锁的原因做了这么多准备工作。在准备好Delete操作的三种加锁逻辑和InnoDB的死锁预防策略等知识后,再回过头来分析本文开头提到的死锁问题,你就能开始念来了,事半功倍。首先假设dltask中只有一条记录:(1,'a','b','c','data')。三个并发事务同时执行如下SQL:deletefromdltaskwherea='a'andb='b'andc='c';并产生如下并发执行逻辑,导致死锁:上面分析的并发过程,在死锁日志中充分说明了死锁的原因。实际上,根据事务1的第6步和事务0的第3/4步之间的顺序,死锁日志中还可能出现另一种情况,即等待事务1的锁方式为X锁+记录上的Gap没有锁定(lock_modeX锁定rec但不是间隙等待)。这第二种情况也是“润杰”给出的死锁用例中用MySQL5.6.15测试死锁的原因。产生这种死锁的几个前提条件:delete操作针对的是对唯一索引的等价查询的删除;(范围下删除也会造成死锁,但死锁场景与本文分析的场景相同,有所不同)至少有3个(或更多)并发删除操作;并发删除操作可能删除同一条记录,被删除的记录必须存在;事务隔离级别设置为RepeatableRead,未设置innodb_locks_unsafe_for_binlog参数(该参数默认为FALSE);(ReadCommitted隔离级别,由于不会有Gap锁,也没有nextkey,所以不会出现死锁)使用InnoDB存储引擎;(废话!MyISAM引擎根本就没有行锁)作者:JavaDryStore链接:https://juejin.cn/post/6950979697400348686来源:掘金版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。
