谁还没阅历过死锁呢
大家好,我是小林。
说个很早之前自己遇到过数据库死锁疑问。
有个业务关键逻辑就是新增订单、修正订单、查问订单等操作。而后由于订单是不能重复的,所以事先在新增订单的时刻做了幂等性校验,做法就是在新增订单记载之前,先经过select ... for update 语句查问订单能否存在,假设不存在才拔出订单记载。
而正是由于这样的操作,当业务量很大的时刻,就或者会出现死锁。
接上去跟大家聊下为什么会出现死锁,以及怎样防止死锁。
死锁的出现
本次案例经常使用存储引擎 Innodb,隔离级别无法重复读(RR)。
接上去,我用实战的形式来带大家看看死锁是怎样出现的。
我建了一张订单表,其中 id 字段为主键索引,order_no 字段个别索引,也就是非惟一索引:
`id``order_no``create_date`datetime
而后,先 t_order 表里如今曾经有了 6 条记载:
假定这时有两事务,一个事务要拔出订单 1007 ,另外一个事务要拔出订单1008,由于须要对订单做幂等性校验,所以两个事务先要查问该订单能否存在,不存在才拔出记载,环节如下:
可以看到,两个事务都堕入了等候形态(前提没有关上死锁检测),也就是出现了死锁,由于都在相互等候对方监禁锁。
这里在查问记载能否存在的时刻,经常使用了 select ... for update语句,目标为了防止事务口头的环节中,有其余事务拔出了记载,而出现幻读的疑问。
假设没有经常使用 select ... for update 语句,而经常使用了单纯的 select语句,假设是两个订单号一样的恳求同时出去,就会出现两个重复的订单,有或者出现幻读,如下图:
为什么会发生死锁?
可重复读隔离级别下,是存在幻读的疑问。
Innodb 引擎为了处置「可重复读」隔离级别下的幻读疑问,就引出了 next-key 锁,它是记载锁和间隙锁的组合。
个别的 select 语句是不会对记载加锁的,由于它是经过 MVCC 的机制成功的快照读,假设要在查问时对记载加行锁,可以经常使用上方这两个形式:
//对读取的记载加共享锁//对读取的记载加排他锁
行锁的监禁机遇是在事务提交(commit)后,锁就会被监禁,并不是一条语句口头完就监禁行锁。
比如,上方事务 A 查问语句会锁住(2, +∞]范畴的记载,然前时期假设有其余事务在这个锁住的范畴拔出数据就会被阻塞。
next-key 锁的加锁规定其实挺复杂的,在一些场景下会退步成记载锁或间隙锁,我之前也写一篇加锁规定,具体可以看这篇「我做了一天的试验!」
须要留意的是,next-key lock 锁的是索引,而不是数据自身,所以假设 update 语句的 where条件没有用到索引列,那么就会全表扫描,在一行行扫描的环节中,不只给行加上了行锁,还给行两头的空隙也加上了间隙锁,相当于锁住整个表,而后直到事务完结才会监禁锁。
所以在线上千万不要口头没有带索引条件的 update语句,不然会形成业务停滞,我有个读者就由于干了这个事情,而后被老板教育了一波,具体可以看这篇「完蛋,公司被一条 update 语句干趴了!」
回到前面死锁的例子,在口头上方这条语句的时刻:
由于 order_no 不是惟一索引,所以行锁的类型是间隙锁,于是间隙锁的范畴是(1006, +∞)。那么,当事务 B 往间隙锁里拔出 id = 1008的记载就会被锁住。
由于当咱们口头以下拔出语句时,会在拔出间隙上再次失掉拔出动向锁。
拔出动向锁与间隙锁是抵触的,所以当其它事务持有该间隙的间隙锁时,须要等候其它事务监禁间隙锁之后,才干失掉到拔出动向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中select ... for update 语句并不会相互影响。
案例中的事务 A 和事务 B 在口头完后 select ... for update语句后都持有范畴为(1006,+∞)的间隙锁,而接上去的拔出操作为了失掉到拔出动向锁,都在等候对方事务的间隙锁监禁,于是就形成了循环等候,造成死锁。
如何防止死锁?
死锁的四个必要条件:互斥、占有且等候、无法侵占用、循环等候。只需系统出现死锁,这些条件肯定成立,然而只需破坏恣意一个条件就死锁就不会成立。
在数据库层面,有两种战略经过「冲破循环等候条件」来解除死锁形态:
当出现超时后,就出现上方这个揭示:
当检测到死锁后,就会出现上方这个揭示:
上方这个两种战略是「当有死锁出现时」的防止形式。
咱们可以回归业务的角度来预防死锁,对订单做幂等性校验的目标是为了保障不会出现重复的订单,那咱们可以间接将 order_no字段设置为惟一索引列,应用它的惟一上去保障订单表不会出现重复的订单,不过有一点不好的中央就是在咱们拔出一个曾经存在的订单记载时就会抛出意外。