当前位置: 首页 > Linux

MySQL死锁原因及解决方法

时间:2023-04-06 22:46:54 Linux

一、Mysql锁类型及锁分析1、锁类型介绍:MySQL有页级、表级、行级三种锁级别。表级锁:开销小,加锁速度快;没有死锁;锁粒度大,锁冲突概率最高,并发度最低。行级锁:开销大,加锁慢;可能会出现死锁;锁粒度最小,锁冲突概率最低,并发度最高。页锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;加锁粒度介于表锁和行锁之间,并发通用算法:下一个KeyLocks锁,同时加锁记录(数据),并在记录前面加GapGap锁,不加锁记录,只记录上一个GapRecordlocklock(lockdata,notlockGap),所以其实Next-KeyLocks=Gaplock+Recordlocklock2.死锁的原因及例子1.原因:所谓死锁:是指两个或多个进程等待的现象由于在执行过程中对资源的竞争而相互竞争。如果没有外力,他们将无法前进。此时,称为系统处于死锁状态或系统出现死锁。这些始终相互等待的进程称为死锁进程。表级锁不会造成死锁。所以死锁的解决方法主要针对最常用的InnoDB。死锁的关键是两个(或多个)Session加锁的顺序不一致。那么对应的解决死锁问题的关键就是:让不同的session依次加锁2.生成例子:案例1需求:将投资资金分成几份,随机分配给借款人。起初,业务程序的思路是这样的:投资人投资后,金额随机分成几份,然后从借款人表中随机抽取几份,然后借款人表中的余额为通过selectforupdate一一更新。例如两个用户同时投资,用户A的金额随机分成2份分配给借款人1,用户2B的金额随机分成2份分配给借款人2和1。加锁的顺序不同,死锁当然很严重。很快就会出现。这个问题的改进很简单,一次性锁定所有分配的借款人即可。select*fromxxxwhereidin(xx,xx,xx)forupdatemysql中的列表值会自动从小到大排序,锁也是从小到大一个一个加。例如(下面的sessionid为主键):Session1:mysql>select*fromt3whereidin(8,9)forupdate;+----+--------+------+----------------------+|编号|课程|姓名|ctime|+----+--------+------+-------------------+|8|西澳|f|2016-03-0211:36:30||9|剑侠|f|2016-03-0111:36:30|+----+--------+------+--------------------+集合中的行(0.04秒)会话2:从t3中选择*,其中id在(10,8,5)中进行更新;lockwaiting...其实此时id=10的记录并没有加锁,但是id=5的记录已经加锁了加锁了,这里id=8的锁等待,请阅读你不相信我。Session3:mysql>select*fromt3whereid=5forupdate;锁定等待Session4:mysql>select*fromt3whereid=10forupdate;+----+--------+------+--------------------+|编号|课程|姓名|ctime|+----+--------+------+--------------------+|10|杰比|克|2016-03-1011:45:05|+----+--------+------+--------------------+rowinset(0.00sec)in在其他session中,id=5不能加锁,id=10可以加锁。案例2开发中经常会做这种判断需求:根据字段值(带索引)查询,不存在则插入;否则,更新它。以id为主键为例,当前没有id=22的行Session1:select*fromt3whereid=22forupdate;Emptyset(0.00sec)session2:select*fromt3whereid=23forupdate;空集(0.00秒)Session1:插入t3值(22,'ac','a',now());锁定等待...Session2:插入t3值(23,'bc','b',现在());ERROR1213(40001):尝试获取锁时发现死锁;尝试重新启动事务锁定现有行(主键)时,mysql只有行锁。当锁定一个不存在的行时(即使条件是主键),mysql会锁定一个范围(用间隙锁)。锁定范围为:(无限小或小于表中锁定id的最大值,无限大或大于表中锁定id的最小值)如:如果表中已有id为(11,12),thenlock(12,infinity)iftheexistingidinthetableis(11,30)Thenlock(11,30)这个死锁的解决方法是:insertintot3(xx,xx)onduplicatekey更新`xx`='XX';使用mysql特定的语法来解决这个问题。因为插入语句是针对主键的,所以无论插入的行是否存在,都只会有行锁Case3mysql>select*fromt3whereid=9forupdate;+----+-------+------+--------------------+|编号|课程|姓名|ctime|+----+-------+------+--------------------+|9|剑侠|f|2016-03-0111:36:30|+----+--------+------+--------------------+集合中的行(0.00秒)Session2:mysql>select*fromt3whereid<20forupdate;锁等待Session1:mysql>insertintot3values(7,'ae','a',now());ERROR1213(40001):尝试获取锁时发现死锁;tryrestarttransaction这个和Case1等类似,只是session1按照常理没有出牌,Session2在等待Session1的id=9的锁,session2持有1到8的锁(注意范围从9到19没有被session2锁定),最后session1在插入新行时必须等待session2,因此发生死锁。这个在业务需求中一般不会出现,因为你锁定了id=9,但是想插入id=7的行,有点跳动,当然要有解决办法,那就是重新组织业务需求,避免这样写。案例4一般情况下,两个session通过一个sql各自持有一个锁,然后访问对方的加锁数据产生死锁。Case5两条单条SQL语句涉及相同的加锁数据,但加锁顺序不同,导致死锁。死锁场景如下:表结构:CREATETABLEdltask(idbigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'autoid',avarchar(30)NOTNULLCOMMENT'uniq.a',bvarchar(30)NOTNULLCOMMENT'uniq.b',cvarchar(30)NOTNULLCOMMENT'uniq.c',xvarchar(30)NOTNULLCOMMENT'data',PRIMARYKEY(id),SETUNIQUEKEYuniq_a_b_c(a,b,c))CHENGINE=innoDBARDEFA=utf8COMMENT='deadlocktest';#a,b,c三列,组合成唯一索引,主键索引就是id列。事务隔离级别:RR(RepeatableRead)每个事务只有一条SQL:deletefromdltaskwherea=?和b=?不是真正意义上的物理删除,而是将记录标记为已删除。(注意:这些标记为已删除的记录会被后台的Purge操作恢复并物理删除,但处于删除状态的记录会在索引中保存一段时间。)在RR隔离级别下,唯一索引满足查询条件,但删除记录,如何加锁?InnoDB这里的处理策略不同于前面两种策略,或者说是前两种策略的结合:对于满足条件的删除记录,InnoDB会给记录加上下一个key锁X(给记录本身加X锁,而在同时锁住记录前的GAP,防止满足条件的新记录的插入。)唯一查询,三种情况,对应三种加锁策略,总结如下:这里,我们看到下一个keylock,是不是很眼熟?对了,上一个死锁中事务1和事务2处于等待状态的锁都是nextkey锁。理解了这三种加锁策略,其实构造了一定的并发场景,死锁的原因就呼之欲出了。但是,还有一个前提策略需要引入,那就是InnoDB内部采用的死锁预防策略。如果找到满足条件的记录,且该记录有效,则对该记录加X锁,NoGaplock(lock_modeXlocksrecbutnotgap);如果找到满足条件的记录,但记录无效(标记为已删除的记录),则对该记录添加下一个键锁(锁定记录本身和记录前的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测试死锁的原因。这种死锁有几个前提条件:删除操作是针对唯一索引上的等价查询的删除;(范围下的删除也会造成死锁,但死锁场景与本文分析的场景相同,有所不同)至少有3个(或更多)并发删除操作;并发删除操作可能删除同一条记录,被删除的记录必须存在;事务隔离级别设置为RepeatableRead,未设置innodb_locks_unsafe_for_binlog参数(该参数默认为FALSE);(ReadCommitted隔离级别,因为不会有Gap锁,没有nextkey,所以不会出现死锁)——使用InnoDB存储引擎;(废话!MyISAM引擎根本就没有行锁)https://blog.csdn.net/tr1912/...