高并发下数据库插入操作死锁问题

发布时间:2023年12月28日

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)

  1. 原子性(atomicity):
    事务是一个原子操作,要么全部执行成功,要么全部执行失败。 事务的原子性确保一组逻辑操作,要么全部完成,要么完全不起作用。

  2. 一致性(consistency):
    执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。

  3. 隔离性(isolation):
    事务的隔离性是指在并发执行的多个事务中,每个事务的执行互不影响,每个事务都有自己独立的空间进行操作。事务隔离级别越高,数据冲突的可能性就越小,但并发性能也会受到一定的影响。

  4. 持久性(durability):
    一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障,应用重启,也不应该对其有任何影响。

2.2 数据库的四种事务隔离级别

数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。在事务的并发操作中可能会出现脏读,不可重复读,幻读。

  1. 读未提交(Read uncommitted):
    一个事务读到了另一个事务还没有提交的数据。
    但是会产生脏读。
  2. 读已提交(Read committed):
    一个事务要等另一个事务提交后才能读取数据。
    但是会产生不可重复读。不可重复读说的是某一条数据发生了改变。
  3. 可重复读(Repeatable read)
    同一事务下,事务在执行期间,多次读取同一数据时,能够保证读取到的数据是一致的。
    但是会产生幻读。幻读与不可重复读不同,它说的是多出来了数据。
  4. 串行化(Serializable)
    它是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率最低,比较耗费数据库性能,一般不推荐使用。
隔离级别脏读不可重复读幻读
读未提交可能出现可能出现可能出现
读已提交不会出现可能出现可能出现
可重复读不会出现不会出现可能出现
串行化不会出现不会出现不会出现

2.3 产生死锁的原因

2.3.1 日志查看

发生死锁时,查看最后一次死锁的日志。

show engine innodb status;
2.3.2 数据库的锁

数据库InnoDB 中的行锁有多种类型:

  1. 记录锁(RECORD LOCK)
    对索引记录加锁。

  2. 间隙锁(GAP LOCK,也叫范围锁)
    对索引记录的所在间隙加锁,在 RR 隔离级别下,用于解决幻读的问题(实际上在 RC 隔离级别下,也会产生间隙锁)。

S 间隙锁和 X 间隙锁是兼容的,不同的事务可以在同一个间隙加锁。

  1. NEXT-KEY 锁
    相当于 RECORD LOCK + GAP LOCK。

  2. 插入意向锁(INSERT INTENTION LOCK)
    GAP 锁的一种,在执行 INSERT 前,如果待插入记录的下一条记录上被加了 GAP 锁,则 INSERT 语句被阻塞,且生成一个插入意向锁。

仅会被 GAP 锁阻塞。

  1. 隐式锁
    新插入的记录,不生成锁结构,但由于事务 ID 的存在,相当于加了隐式锁;别的事务要对这条记录加锁前,先帮助其生成一个锁结构,然后再进入等待状态。
2.3.2 产生死锁的原因

INSERT 语句加锁类型

  1. 被 GAP 锁阻塞时,生成一个插入意向锁。
  2. 遇到重复键冲突时
    主键冲突,产生 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语句扫描条件的锁,会被释放。

建议:

  1. 以固定的顺序访问表和行
  2. 大事务拆分成小事务。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  5. 为表添加合理的索引(如果不走索引,将会为表的每一行记录添加上锁,増加死锁的概率)。
  6. 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

文章来源:https://blog.csdn.net/alidingding/article/details/135212931
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。