并发场景下,mysql死锁和乐观锁异常

测试反馈在认证中心的日志中会报错死锁和乐观锁异常。

其反馈的报错大致如下:

死锁

乐观锁

源代码:

查看了相关的源代码,发现这个地方是一个菜单上报的接口。并且这个接口使用了数据库事务,这个事务因为修改和查询的步骤很多,所以是一个长事务。

根据其他产品研发的反馈,他们通过自动化上报菜单,所以是有并发场景的,于是为了可以重现bug,我先写了一个简单的测试程序,模拟并发的场景。

测试程序:

通过测试程序,也复现了BFC测试环境中的问题。打印了同样的报错日志。

从代码中可以发现这种长事务确实是有可能发生死锁。死锁本质上就是A线程持有资源1,需要资源2,B线程持有资源2,需要资源1。这种情况下我认为主要有两种手段可以解决:

  1. 调整事务中获取资源的顺序,如果调整成都是先获取资源1,再获取资源2的顺序。那就不会造成死锁。因为A线程持有资源1时,B线程不可能持有资源2,因为B线程也需要按顺序先获取了资源1才能走到后面的代码获取资源2。不过这种方式需要调整业务的逻辑,相对复杂并且需要大量的测试,风险较大。
  2. 使用全局锁,该接口作为菜单上报接口,本身是低频操作。只会在微服务启动时调用,并且接口本身的逻辑耗时不长,可以接受微服务“串行”的上报菜单。

于是我在代码中加入一个synchronized全局锁,如下图:

在添加了全局锁之后进行测试。我发现死锁的问题已经解决了,但是乐观锁的报错还是有。

其实我一开始,我就注意到这个乐观锁的报错有点奇怪。报错的堆栈信息指向的是一个查询语句。我们都知道乐观锁实际上是查询时有一个版本号-》修改时验证当前的版本号和我之前查询的版本号是否一致-》如果一致则修改成功,不一致则修改失败。

那么我们认为一般发生乐观锁的报错应该是在修改而不是查询。既然我们自己的代码是一个查询语音,那可以大胆的猜测应该是JPA框架在执行这个findAll的代码行时执行了什么修改操作。不然不可能会报乐观锁的异常。

但是回过头来,当务之急是解决加锁后依然存在乐观锁的问题,JPA的问题我们先放一放。仔细分析代码后我发现,虽然我给整个方法都加上了一个全局锁,让整个过程从并发变成了串行,并且也从日志中看到确实是串行的。但是却忽略了一个问题,那就是两个线程是乎同一时刻开启了事务。也就是说我上面的代码得到的效果是这样的:

但是非并发操作应该是这样的:

基于上面的分析,我把代码改成了在controller中加全局锁,这样就保证了流程和非并发完全一致了。

测试后发现死锁和乐观锁的问题都已解决。

到这里实际上问题就已经完全解决了。但是刚才的问题依然困扰我们,为什么报错会发生在一个查询语句的地方。通过debug JPA源码后发现。其实乐观锁这个报错是被包装过的:

真正发生问题的原因是:

在事务中执行每一条sql时,JPA都要判断是否需要flush,flush就是将挂起的更改同步到数据库。而在执行这个查询操作时,事务中已经有两条修改语句了。上面图中就是出问题的这条SQL语句。SQL的位置是在addApplicationLevelMenu(menuReportDto) –》addMenu()-》

permissionMenuRepository.save(permissionMenu)

而报错信息中所说的就是这条修改期望是有一条记录的,但是实际数据库中没有。

没有的原因在于,上一个事务执行了后面代码的删除逻辑:

发生错误的整体时间线是这样的:

之后我们在编码中需要注意:尽量减少长事务的使用,事务中尽可能保证获取资源的顺序。必要时使用锁来保证并发场景下的代码正确性。

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值