本篇文章主要是处理日常工作中面临的Mysql死锁问题以及如何避免产生死锁的指南。
这里只讨论Innodb引擎,在Innodb下表有两种大类型锁,表锁和行锁。参考自Mysql锁介绍
其中按照锁的性质分类又可分为排他锁和共享锁。
具体sql加锁分析可参考该文章,讲的很全面,而且比较正确。(我本来想写很全的,但是看到了这篇文章,觉得我写多少都没有意义了,人家真的写的很棒,建议如果想要对锁机制有深入了解,一定要通读一下)
可以简单理解为行锁本身,记录锁是直接作用于索引的,这点上面也提到过。
Mysql为了解决RC模式下的幻读问题,引进了间隙锁的概念,比方说一张表存在以下数据,其中在key上有索引存在
id | key | name |
---|---|---|
1 | 2 | xx |
2 | 8 | x1 |
3 | 9 | x2 |
4 | 15 | x3 |
则在事务A使用任一锁形式时(update xx set name = ‘aa’ where key = 9),会锁住索引上的一段范围,也就是8-9,9-15这两个区间。
在此期间,其他事务则不能再写该区间内的数据,此措施可以有效降低幻读风险(无法完全避免),但是相当于扩大了锁粒度,会造成更严重的写冲突和增大死锁可能性。
个人建议,使用RC隔离级别,因为幻读给我们带来的影响可以忽略不计。
(在RC隔离级别下,唯一索引在写入操作时仍然使用了间隙锁来保障唯一性,但是其他情况下不会再存在间隙锁了)
Next-key lock是间隙锁 + 记录锁的组合,Mysql会对名字的索引数据直接加上记录锁,而对于索引两侧的范围则使用间隙锁。
但是当使用执行计划使用到了唯一索引进行扫描时,间隙锁则不会生效,只会给索引加上记录锁。
上面有提到,当进行任何加锁行为时(insert、update)都会对命中索引本身以及两侧的间隙都加锁,这样会导致锁冲突急剧上升,并发能力下降。比方说有两个写请求A、B过来,A写入数据 key = 8,B写入数据 key = 9,这时候,B只能等A把间隙锁释放掉自己才能写入。
所以Mysql为了优化写入的并发能力,又制造了插入意向锁,当insert写入数据时,又原先的Next-key 变成了 插入意向锁 + 记录锁。
意向锁是比排他锁(X锁)兼容度更高的锁,可以理解为共享锁。同时插入意向锁又是一种特殊的间隙锁存在
当请求A和B在同一个间隙内插入数据时,会同时申请到插入意向锁,由于是共享锁,所以A和B不冲突,都能进行写入,大大提高了并发写入能力。但是要注意插入意向锁之间不阻塞,但是会被间隙锁阻塞
这是一种很边缘但是又很常用的锁,所以在这里写一下。我们开发人员在使用数据库自增主键当唯一Id使用时,难道没有好奇过Mysql是如何实现自增id不会冲突的吗?这背后就是AUTO-INC锁在起作用。
简单来说,任一一个insert操作时,都要先申请AUTO-INC锁(表级锁),所以就能保障获取到的Id一定是唯一的。为了提高性能,insert语句操作完以后就会释放AUTO-INC锁,而不是等到事务结束后释放。这也意味着如果事务回滚导致insert回滚了,就会造成自增id不连续的现象。
Mysql为了进一步优化性能,其默认的innodb_autoinc_lock_mode(为1)模式下,如果能提前预知插入数据的行数,则直接可以把自增空间预留出来而不是去获取AUTO-INC表锁,能够显著提升并发写入能力。
在8.0版本又做出了进一步改进,彻底放弃了AUTO-INC表锁,完全采用信号量来控制id的分配。
修改隔离级别为RC可以解决绝大多数的死锁场景(因为会干掉很多间隙锁的存在)。
如果不想直接修改数据库的隔离级别,在我们代码中,每个session同样可以临时声明隔离级别,可参考代码如下
SqlSession batchSession = mybatisSqlSession
.openSession(ExecutorType.BATCH, TransactionIsolationLevel.READ_COMMITTED)
警惕过大事务,当单个事务中存在以下场景就需要注意存在死锁风险了
解决方案:
这个语句产生死锁的原因争议很大,为什么争议很大?因为他跟Mysql的版本高度关联,有些版本是无法复现的,有些版本可以复现,而且复现的场景也各有不同。
可以简单概括为,在某些Mysql版本下,insert into … on duplicate key update操作会产生间隙锁,没错,不是插入意向锁而是普通的间隙锁。也有人认为这是Mysql自身的bug(因为理论上,insert操作是不可能产生死锁的)。
各种版本、触发情况分析下我自己头都大了,所以这里就不再一一阐述了,各位知道某些场景下存在问题就好,或者升级8.0一劳永逸(学习了Mysql知识这么久,发现8.0真是好东西,不愧是大版本更新,几乎在各个方面都有优化改进)。
其实发生死锁了,问题不是很大,顶到天就是会造成一些数据不一致的现象发生(因为会导致一个事务强制回滚,影响业务处理流程),Mysql有自身的死锁发现机制,并且这个配置是默认开启的,不会有人关上的。
只是对于我们技术人员而言,写出死锁就是挺扎眼的行为,会被人吐槽。
先是简单的,发生死锁后有相关异常打印的,看看能不能从代码层面把怀疑sql给圈出来。
如果是那种分析很困难的场景(发生死锁异常的地方不是根因,只是被别的sql牵连的,这种情况是分析不出来的),那就要去捞数据库相关死锁日志了。
公司里有DBA的找DBA帮忙,没有DBA的去开启死锁日志的相关配置(innodb_print_all_deadlocks),在死锁日志中有比较详细的死锁相关事务、sql等信息。
这个就不用解释了,Mysql这么多年了,你碰到的死锁问题都可能被讨论很久了