Mysql专栏(四) Mysql锁机制

本篇文章主要是处理日常工作中面临的Mysql死锁问题以及如何避免产生死锁的指南。

一 Mysql主要锁类型

这里只讨论Innodb引擎,在Innodb下表有两种大类型锁,表锁和行锁。参考自Mysql锁介绍

  1. 表锁:对整张表加锁,加锁后,其他事务不能再对该表进行操作,并发程度最低,但是不会产生死锁风险,一般出现在修改表结构及元数据时才会产生。
  2. 行锁:在Innodb中,行锁是基于索引实现的(这时会有一个比较重要的概念,如果DML没有命中索引,则锁会升级为表锁),行锁的锁粒度较小,只会持有某些数据行的锁,这为表的并发操作提供了底层支持。比方说Mysql可以允许多个事务并发更新表的不同数据行。但是因为其锁范围可大可小,再加上Mysql间隙锁的存在,如果使用不当也会产生死锁的风险。

其中按照锁的性质分类又可分为排他锁和共享锁。

二 Mysql行锁

具体sql加锁分析可参考该文章,讲的很全面,而且比较正确。(我本来想写很全的,但是看到了这篇文章,觉得我写多少都没有意义了,人家真的写的很棒,建议如果想要对锁机制有深入了解,一定要通读一下

2.1 记录锁

可以简单理解为行锁本身,记录锁是直接作用于索引的,这点上面也提到过。

2.2 间隙锁

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隔离级别下,唯一索引在写入操作时仍然使用了间隙锁来保障唯一性,但是其他情况下不会再存在间隙锁了

3. Next-key lock

Next-key lock是间隙锁 + 记录锁的组合,Mysql会对名字的索引数据直接加上记录锁,而对于索引两侧的范围则使用间隙锁。
但是当使用执行计划使用到了唯一索引进行扫描时,间隙锁则不会生效,只会给索引加上记录锁。

4. 插入意向锁(基于mysql 5.7.10)

上面有提到,当进行任何加锁行为时(insert、update)都会对命中索引本身以及两侧的间隙都加锁,这样会导致锁冲突急剧上升,并发能力下降。比方说有两个写请求A、B过来,A写入数据 key = 8,B写入数据 key = 9,这时候,B只能等A把间隙锁释放掉自己才能写入。
Mysql专栏(四) Mysql锁机制_第1张图片
所以Mysql为了优化写入的并发能力,又制造了插入意向锁,当insert写入数据时,又原先的Next-key 变成了 插入意向锁 + 记录锁。
意向锁是比排他锁(X锁)兼容度更高的锁,可以理解为共享锁。同时插入意向锁又是一种特殊的间隙锁存在

当请求A和B在同一个间隙内插入数据时,会同时申请到插入意向锁,由于是共享锁,所以A和B不冲突,都能进行写入,大大提高了并发写入能力。但是要注意插入意向锁之间不阻塞,但是会被间隙锁阻塞
Mysql专栏(四) Mysql锁机制_第2张图片

5. AUTO-INC 锁(扩展知识)

这是一种很边缘但是又很常用的锁,所以在这里写一下。我们开发人员在使用数据库自增主键当唯一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的分配。

三 死锁问题避免

3.1 声明RC隔离级别

修改隔离级别为RC可以解决绝大多数的死锁场景(因为会干掉很多间隙锁的存在)。
如果不想直接修改数据库的隔离级别,在我们代码中,每个session同样可以临时声明隔离级别,可参考代码如下

SqlSession batchSession = mybatisSqlSession
			.openSession(ExecutorType.BATCH, TransactionIsolationLevel.READ_COMMITTED)

3.2 控制事务粒度

警惕过大事务,当单个事务中存在以下场景就需要注意存在死锁风险了

  1. 存在多条对同一张表且不同查询条件的 update、delete、insert等会产生间隙锁的操作,则一定具有死锁风险。
  2. 存在批量update操作(不同id或者不同条件的update)

解决方案:

  1. 缩小事务范围,尽可能不要在一个事务里处理太多sql。
  2. 在事务外再声明分布式锁,杜绝并发(不建议,因为会严重降低并发能力)
  3. 去除事务,保障数据的最终一致性

3.3 insert into … on duplicate key update 谨慎使用

这个语句产生死锁的原因争议很大,为什么争议很大?因为他跟Mysql的版本高度关联,有些版本是无法复现的,有些版本可以复现,而且复现的场景也各有不同。

可以简单概括为,在某些Mysql版本下,insert into … on duplicate key update操作会产生间隙锁,没错,不是插入意向锁而是普通的间隙锁。也有人认为这是Mysql自身的bug(因为理论上,insert操作是不可能产生死锁的)。

各种版本、触发情况分析下我自己头都大了,所以这里就不再一一阐述了,各位知道某些场景下存在问题就好,或者升级8.0一劳永逸(学习了Mysql知识这么久,发现8.0真是好东西,不愧是大版本更新,几乎在各个方面都有优化改进)。

四 死锁问题排查

其实发生死锁了,问题不是很大,顶到天就是会造成一些数据不一致的现象发生(因为会导致一个事务强制回滚,影响业务处理流程),Mysql有自身的死锁发现机制,并且这个配置是默认开启的,不会有人关上的。

只是对于我们技术人员而言,写出死锁就是挺扎眼的行为,会被人吐槽。

4.1 找出问题sql

先是简单的,发生死锁后有相关异常打印的,看看能不能从代码层面把怀疑sql给圈出来。
如果是那种分析很困难的场景(发生死锁异常的地方不是根因,只是被别的sql牵连的,这种情况是分析不出来的),那就要去捞数据库相关死锁日志了。

公司里有DBA的找DBA帮忙,没有DBA的去开启死锁日志的相关配置(innodb_print_all_deadlocks),在死锁日志中有比较详细的死锁相关事务、sql等信息。

4.2 先把问题sql谷歌

这个就不用解释了,Mysql这么多年了,你碰到的死锁问题都可能被讨论很久了

你可能感兴趣的:(Mysql专栏,mysql,数据库,java)