1. 问题:
项目中出现如下报错:
org.springframework.dao.DeadlockLoserDataAccessException: com.xxxMapper.insert (batch index #1) failed.
Cause: java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction;
Deadlock found when trying to get lock; try restarting transaction;
nested exception is java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction
java.sql.BatchUpdateException:Deadlock found when trying to get lock;try restarting transaction
此场景在多线程批量同时对两张表进入插入操作,一张A表,一张B表,A表是原始数据表,B表是处理数据表,插入B表时,每批次插入数据量为50,产生死锁导致数据不一致问题。A表与B表数据不一致,并且比对时以A表为准,所以B表的数据永远有问题,不会及时更新。
2. 问题原因:
2.1 事务的四大特性(ACID)
-
原子性(atomicity):
事务是一个原子操作,要么全部执行成功,要么全部执行失败。 事务的原子性确保一组逻辑操作,要么全部完成,要么完全不起作用。 -
一致性(consistency):
执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。 -
隔离性(isolation):
事务的隔离性是指在并发执行的多个事务中,每个事务的执行互不影响,每个事务都有自己独立的空间进行操作。事务隔离级别越高,数据冲突的可能性就越小,但并发性能也会受到一定的影响。 -
持久性(durability):
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障,应用重启,也不应该对其有任何影响。
2.2 数据库的四种事务隔离级别
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。在事务的并发操作中可能会出现脏读,不可重复读,幻读。
- 读未提交(Read uncommitted):
一个事务读到了另一个事务还没有提交的数据。
但是会产生脏读。 - 读已提交(Read committed):
一个事务要等另一个事务提交后才能读取数据。
但是会产生不可重复读。不可重复读说的是某一条数据发生了改变。 - 可重复读(Repeatable read)
同一事务下,事务在执行期间,多次读取同一数据时,能够保证读取到的数据是一致的。
但是会产生幻读。幻读与不可重复读不同,它说的是多出来了数据。 - 串行化(Serializable)
它是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率最低,比较耗费数据库性能,一般不推荐使用。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能出现 | 可能出现 | 可能出现 |
读已提交 | 不会出现 | 可能出现 | 可能出现 |
可重复读 | 不会出现 | 不会出现 | 可能出现 |
串行化 | 不会出现 | 不会出现 | 不会出现 |
2.3 产生死锁的原因
2.3.1 日志查看
发生死锁时,查看最后一次死锁的日志。
show engine innodb status;
2.3.2 数据库的锁
数据库InnoDB 中的行锁有多种类型:
-
记录锁(RECORD LOCK)
对索引记录加锁。 -
间隙锁(GAP LOCK,也叫范围锁)
对索引记录的所在间隙加锁,在 RR 隔离级别下,用于解决幻读的问题(实际上在 RC 隔离级别下,也会产生间隙锁)。
S 间隙锁和 X 间隙锁是兼容的,不同的事务可以在同一个间隙加锁。
-
NEXT-KEY 锁
相当于 RECORD LOCK + GAP LOCK。 -
插入意向锁(INSERT INTENTION LOCK)
GAP 锁的一种,在执行 INSERT 前,如果待插入记录的下一条记录上被加了 GAP 锁,则 INSERT 语句被阻塞,且生成一个插入意向锁。
仅会被 GAP 锁阻塞。
- 隐式锁
新插入的记录,不生成锁结构,但由于事务 ID 的存在,相当于加了隐式锁;别的事务要对这条记录加锁前,先帮助其生成一个锁结构,然后再进入等待状态。
2.3.2 产生死锁的原因
INSERT 语句加锁类型:
- 被 GAP 锁阻塞时,生成一个插入意向锁。
- 遇到重复键冲突时
主键冲突,产生 S 型记录锁(RR 和 RR 隔离级别,实际上在 INSERT 阶段时还是会请求 GAP 锁)。
唯一键冲突,产生 S 型 NEXT-KEY 锁(RR 和 RR 隔离级别)。
注意:INSERT 语句正常执行时,不会生成锁结构。
INSERT … ON DUPLICATE KEY UPDATE 和 REPLACE 如果遇到重复键冲突:
如果是主键冲突,加 X 型记录锁(RR 和 RR 隔离级别,实际上在 INSERT 阶段时还是会请求 GAP 锁)。
如果是唯一键冲突,加 X 型 NEXT-KEY 锁(RR 和 RR 隔离级别)。
2.3.3 情况1
INSERT语句
T1 时刻
session1 插入记录成功,此时对应的索引记录被隐式锁保护,未生成锁结构。
T2 时刻
session2 插入记录检测到插入值和 session1 唯一键冲突。
session2 帮助 session1 对 a=35 的记录产生了一个显式的锁结构。
session2 自身产生 S 型的 NEXT-KEY LOCK,请求范围为 (30,35],但是其只能获取到 (30,35) 的 GAP LOCK,而被 session1 的 a=35 的记录锁阻塞。
T3 时刻
session1 插入 a=33,被 session2 (30,35)间隙锁阻塞。
闭环锁等待,死锁条件达成:
session1 持有 session2 需要的 a=35 记录锁,且请求 session2 持有的 (30,35) GAP 锁。
session2 持有 session1 需要的 (30,35) GAP 锁,且请求 session1 持有的记录锁。
此情况的解决方案:
在一个事务中的 INSERT 按照主键或唯一键的顺序增序插入,即 session1 可以先插入 a=33 的记录,再插入 a=35 的记录,可一定程度避免受到 GAP 锁的影响。
一个事务中只插入一行记录,且尽快提交。
2.3.4 情况2
REPLACE语句,可参考此处
此情况的解决方案:
在唯一键冲突时,INSERT、INSERT … ON DUPLICATE KEY UPDATE 的加锁范围要比 REPLACE 加锁范围小,在该场景下,可使用 INSERT … ON DUPLICATE KEY UPDATE 代替 REPLACE 来避免死锁,有兴趣的可以自己测试下。
3. 解决方案:
注意点:
在 REPEATABLE-READ 级别,事务持有的 每个锁 在整个事务期间一直被持有。
在 READ-COMMITED 级别,事务里面特定语句结束之后,不匹配该sql语句扫描条件的锁,会被释放。
建议:
- 以固定的顺序访问表和行
- 大事务拆分成小事务。大事务更倾向于死锁,如果业务允许,将大事务拆小。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
- 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
- 为表添加合理的索引(如果不走索引,将会为表的每一行记录添加上锁,増加死锁的概率)。
- INSERT … ON DUPLICATE KEY UPDATE 比 REPLACE 产生死锁的几率小且更安全高效。
以上的小建议都在一定程度上减少和避免的死锁的发生,但是还可能会发生,因此,在业务端做好容错处理也是重要的。比如说本篇博客中的A表与B表可以校验两者的数据一致性,因为高并发会时时刷新数据,因此即使是死锁造成的短暂的数据不一致,在下一次刷新时,也会刷新为最新的数据。
参考:
官方文档:
https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
https://blog.csdn.net/qq271859852/article/details/79284740
https://www.panziye.com/java/4659.html
https://baijiahao.baidu.com/s?id=1781188447015451234&wfr=spider&for=pc
有用:
https://zhuanlan.zhihu.com/p/624468049
https://blog.csdn.net/songjiweiliu/article/details/131136171
https://zhuanlan.zhihu.com/p/92959304
有些东西:
https://zhuanlan.zhihu.com/p/528365818
https://zhuanlan.zhihu.com/p/654416860
https://blog.csdn.net/minghao0508/article/details/129093202
https://juejin.cn/post/6844903854165721101#heading-0