项目中出现如下报错:
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表的数据永远有问题,不会及时更新。
原子性(atomicity):
事务是一个原子操作,要么全部执行成功,要么全部执行失败。 事务的原子性确保一组逻辑操作,要么全部完成,要么完全不起作用。
一致性(consistency):
执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。
隔离性(isolation):
事务的隔离性是指在并发执行的多个事务中,每个事务的执行互不影响,每个事务都有自己独立的空间进行操作。事务隔离级别越高,数据冲突的可能性就越小,但并发性能也会受到一定的影响。
持久性(durability):
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障,应用重启,也不应该对其有任何影响。
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。在事务的并发操作中可能会出现脏读,不可重复读,幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能出现 | 可能出现 | 可能出现 |
读已提交 | 不会出现 | 可能出现 | 可能出现 |
可重复读 | 不会出现 | 不会出现 | 可能出现 |
串行化 | 不会出现 | 不会出现 | 不会出现 |
发生死锁时,查看最后一次死锁的日志。
show engine innodb status;
数据库InnoDB 中的行锁有多种类型:
记录锁(RECORD LOCK)
对索引记录加锁。
间隙锁(GAP LOCK,也叫范围锁)
对索引记录的所在间隙加锁,在 RR 隔离级别下,用于解决幻读的问题(实际上在 RC 隔离级别下,也会产生间隙锁)。
S 间隙锁和 X 间隙锁是兼容的,不同的事务可以在同一个间隙加锁。
NEXT-KEY 锁
相当于 RECORD LOCK + GAP LOCK。
插入意向锁(INSERT INTENTION LOCK)
GAP 锁的一种,在执行 INSERT 前,如果待插入记录的下一条记录上被加了 GAP 锁,则 INSERT 语句被阻塞,且生成一个插入意向锁。
仅会被 GAP 锁阻塞。
INSERT 语句加锁类型:
INSERT … ON DUPLICATE KEY UPDATE 和 REPLACE 如果遇到重复键冲突:
如果是主键冲突,加 X 型记录锁(RR 和 RR 隔离级别,实际上在 INSERT 阶段时还是会请求 GAP 锁)。
如果是唯一键冲突,加 X 型 NEXT-KEY 锁(RR 和 RR 隔离级别)。
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 锁的影响。
一个事务中只插入一行记录,且尽快提交。
REPLACE语句,可参考此处
此情况的解决方案:
在唯一键冲突时,INSERT、INSERT … ON DUPLICATE KEY UPDATE 的加锁范围要比 REPLACE 加锁范围小,在该场景下,可使用 INSERT … ON DUPLICATE KEY UPDATE 代替 REPLACE 来避免死锁,有兴趣的可以自己测试下。
注意点:
在 REPEATABLE-READ 级别,事务持有的 每个锁 在整个事务期间一直被持有。
在 READ-COMMITED 级别,事务里面特定语句结束之后,不匹配该sql语句扫描条件的锁,会被释放。
建议:
以上的小建议都在一定程度上减少和避免的死锁的发生,但是还可能会发生,因此,在业务端做好容错处理也是重要的。比如说本篇博客中的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