2
头图

问题背景

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

看下具体的业务实现细节

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

接下来复现问题操作

  • 根据t1的值查询表a中有没有对应的记录
  • 如果有值,则更新t2的值
  • 如果没查到结果,则执行insert插入操作

这里批量操作我们采用了多线程的方式来执行
image.png
image.png

问题复现

  • step1 - ⾸先插⼊测试数据
    image.png
  • step2 - 我们开启两个窗⼝去模拟死锁。
    Session1:
    image.png
    Session2:
    image.png
    此时,Session 1和Session 2都会对区间(20, ⽆穷⼤)加锁, 因为间隙锁只是⽤来防⽌其他事务在区间中插⼊数据。
  • step3 - Session1继续插⼊操作:
    image.png

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

  • step4- 紧接着Session2继续插⼊操作:
    image.png

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

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

问题原因

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

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

  • record lock:⾏锁,也就是仅仅锁着单独的⼀⾏。
  • gap lock:间隙锁,仅仅锁住⼀个区间(注意这⾥的区间都是开区间,也就是不包括边界值)。
  • next-key lock:record lock+gap lock,所以next-key lock也就半开半闭区间,且是下界开,上界闭。
    加锁规则特性

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

  • 加锁的基本单位是(next-key lock),他是前开后闭原则
  • 查找过程中访问的对象会增加锁
  • 间隙锁仅阻⽌其他事务插⼊间隙。在删除数据的时候,会去加间隙锁,但是多个事务是可以同时对⼀个间隙去加锁的,⽽如果需要对该间隙进⾏插⼊,则需要等待锁释放。

解决方式

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

间隙锁只存在于可重复读的隔离级别下,因为要防⽌幻读。这个⽅法不现实,不可能为了这个问题 把整个线上数据库隔离级别给改掉。
2、避免先删除后插⼊的操作.

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


skyarthur
1.6k 声望1.3k 粉丝

技术支持业务,技术增强业务,技术驱动业务