死锁问题排查过程-间隙锁的复现以及解决- 踩坑总结集锦 20(一周一更)

1,439 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

ps:因为周末有事情,所以本周的踩坑总结提前发啦~

问题背景

我们在开启多线程对数据库进行操作的时候,先批量对数据进行删除,然后再新增,本来想着是考虑到不走更新,性能会提升,但是执行的时候发现报错,执行的sql等待超时,阻塞了进程,dbcp连接池被打满,数据库表访问不可用。针对这个问题,我们进行了深入的挖掘,逐渐解开了问题的真相。

问题复现

复现准备工作

业务问题这里就不细讲了,为了方便讲解,操作直接简化。

  • 数据库表a有唯⼀索引t1,业务字段t2,主键id。数据库表a的定义如下:

image.png

  • 现在导⼊⼀批数据A的集合,A的定义如下所示:

image.png 接下来复现问题操作

  • 根据t1的值查询表a中有没有对应的记录
  • 如果有值,则更新t2的值
  • 如果没查到结果,则执行insert插入操作 这里批量操作我们采用了多线程的方式来执行

image.png image.png 不走更新操作,先删除,后插入,保证只有2次数据库操作。

从业务的⻆度考虑,表中只有id,t1,t2字段,且客户端实际也只关⼼t2字段。因此决定先根据A的t1字段去数据库⾥将对应的值删全部除,然后进⾏批量插⼊操作,这样的话,⽆论更新还是插⼊,都只有两次数据库操作,提高了性能。为了避免插⼊失败,导致数据被删除,在handle⽅法上添加事务。

复现报错

报错如下:

Deadlock found when trying to get lock; try restarting transaction; nested exception is

com.ibatis.common.jdbc.exception.NestedSQLException...

可以看到,发⽣了数据库死锁。于是开始排查问题

问题原因

查询相关资料得知,引起死锁的原因是MYSQL的间隙锁。

间隙锁

间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引⼊的锁机制,幻读的问题存在是因为新增或者更新操作,这时如果进⾏范围查询的时候(加锁查询),会出现不⼀致的问题,这时使⽤不同的⾏锁已经没有办法满⾜要求,需要对⼀定范围内的数据进⾏加锁,间隙锁就是解决这类问题的。在可重复读隔离级别下,数据库是通过⾏锁和间隙锁共同组成的(next-key lock)来实现的。

⾏锁和间隙锁的定义如下所示:

  • record lock:⾏锁,也就是仅仅锁着单独的⼀⾏。

  • gap lock:间隙锁,仅仅锁住⼀个区间(注意这⾥的区间都是开区间,也就是不包括边界值)。

  • next-key lock:record lock+gap lock,所以next-key lock也就半开半闭区间,且是下界开,上界闭。

加锁规则特性

加锁规则有⼀些特性,其中我们需要关注的有:

  • 加锁的基本单位是(next-key lock),他是前开后闭原则

  • 查找过程中访问的对象会增加锁

  • 间隙锁仅阻⽌其他事务插⼊间隙。在删除数据的时候,会去加间隙锁,但是多个事务是可以同时对⼀个间隙去加锁的,⽽如果需要对该间隙进⾏插⼊,则需要等待锁释放。

间隙锁复现死锁

下⾯可以做个实验,去复现死锁问题。

step1- ⾸先插⼊测试数据

image.png

表a数据初始化后,这时候那么可能的next-key lock的包括:

(⽆穷⼩, 10],(10,11],(11,13],(13,20],(20, ⽆穷⼤)

step2- 我们开启两个窗⼝去模拟死锁。

Session1:

image.png

Session2:

image.png

此时,Session 1和Session 2都会对区间(20, ⽆穷⼤)加锁, 因为间隙锁只是⽤来防⽌其他事务在区间中插⼊数据。

step3- Session1继续插⼊操作:

image.png

此时Session1阻塞(因为Session2持有间隙锁)。

step4- 紧接着Session2继续插⼊操作:

image.png

此时Session2死锁,因为Session1持有间隙锁。⽽我们的代码⾥⾯,因为涉及到多线程在事务⾥进⾏先删除后插⼊的操作,就会发⽣死锁。

解决方式

1、将事务隔离级别将为read commit.

间隙锁只存在于可重复读的隔离级别下,因为要防⽌幻读。这个⽅法不现实,不可能为了这个问题 把整个线上数据库隔离级别给改掉。

2、避免先删除后插⼊的操作.

修改代码,避免先删除后插⼊的操作。牺牲性能,在业务中,先根据唯⼀索引查出存在的记录,然后对存在的记录进⾏根据主键Id在循环中更新,对于不存在的记录进⾏批量插⼊。