事务+锁?锁+事务?解析事务与锁的错综Bug!

Java专栏

共 4621字,需浏览 10分钟

 · 2024-03-24

    
      


胖虎和朋友原创的视频教程有兴趣的可以看看


(文末附课程大纲)


👏2024 最新,Java成神之路,架构视频(点击查看)


😉超全技术栈的Java入门+进阶+实战!(点击查看)


来源:juejin.cn/post/7311167273650454580

背景

我们的主要业务是台湾省的一个小商城,这次出问题的是我们「仓库系统」。在仓库系统中有这么一段逻辑:员工可以领取「新建」的订单,然后去执行拣货发货的操作,领取的时候,发货单的状态会从「新建」变为「待拣货」,也就是说「找新建状态的发货单,领取然后变为待拣货然后去拣货」。❞

bug内容

突然有一天,仓库的同事发消息说「有两位员工领取了同一个发货单」。拣货的时候报错该发货单已拣货。这可就奇了怪了,为了防止并发,且这个仓库是单节点部署的,记得是「加了锁的」

模拟

这时候需要定位问题,先模拟一下我们的场景。
    
    @GetMapping(path = "test")
@ResponseBody
public void test(Pageable request){
    for (int i = 0; i < 100; i++) {
        //新建线程处理
        new Thread(() -> {
            userInfoService.testDemo();
        }).start();
    }
}
肯定是并发导致的,这里模拟一下高并发的情况
    
    @Transactional(rollbackOn = Exception.class)
public synchronized void testDemo() 
{
    UserInfo byUserId = userRepository.findByUserId(1);
    byUserId.setAge(byUserId.getAge() + 1);
    userRepository.save(byUserId);
}
可以看到业务逻辑里有个锁,并且有事务。数据大概长这样,我拿之前我写的demo的表来处理,修改这个age 100次。 048179bd1759fcffa5dc0c597fef3e00.webp执行,不用看表了,看log就知道有问题的:

6930b8727897a72ee11bd28c5a377f84.webp

好家伙,这并发了啊,这锁了个寂寞啊。

0c6a661523e70b88a7ba4356c4ab0c93.webp


好吧,查资料,很容易就查到了:这种情况下,锁可能会失效。因为「synchronized锁的是这个方法」,而@Transactional是Spring的AOP在开启时自动锁定的。在进入这个方法前,「AOP会先开启事务,然后进入方法」,此时会加锁。「当方法结束后,锁被释放,然后才会提交事务」。如果在锁释放和提交事务之间有其他线程请求并再次加锁,这可能导致程序不安全。❞所以,所以,所以,这应该就是问题所在了吧。先改一版试试:
    
    @GetMapping(path = "test")
@ResponseBody
public void test(Pageable request) {
    for (int i = 0; i < 100; i++) {
        //新建线程处理
        new Thread(() -> {
            synchronized (UserController.class{
                userInfoService.testDemo();
            }
        }).start();
    }
}
将锁放到整个事务的外层,这样事务提交之后才会释放锁。
    
    @Transactional(rollbackOn = Exception.class)
public void testDemo() 
{
    UserInfo byUserId = userRepository.findByUserId(1);
    log.info("当前线程:{},当前年龄:{}",Thread.currentThread().getName(),byUserId.getAge());
    byUserId.setAge(byUserId.getAge() + 1);
    userRepository.save(byUserId);
    log.info("当前线程:{},当前年龄:{}",Thread.currentThread().getName(),byUserId.getAge());
}
看log,也是没有问题的。

d7bfbd605b747d014181f497697a8a62.webp


数据也没有问题。 0cb0b9846ac7bd9a100d54637d277345.webp 欸欸欸,好了。搞定,提代码,打包,发包,一气呵成。解决bug就是这么迅速。

又发生了

过了几天,仓库又反馈了,「这bug又发生了」,啊啊啊?奇了怪了。还没锁住? a3a848e2446ed347da3b4bf61cdd3c39.webp在研究了好久,觉得这锁没问题啊,是哪里出了问题呢?各种权衡之下,先加了个分布式锁,以求解决问题。然而不出所料,过了两天又又又发生了。又发生了,那就不是锁的问题了,那是什么的问题呢?这段业务很简单,拿到发货单,然后修改状态,然后存进去。就这,也没啥bug可以发生啊。「再从数据看,发现一个奇怪的地方,所有重复领取的发货单,都是在整点领取,然后发生bug的。这绝不是偶然,我在整点干啥了呢?」 想起来了,我前一阵加了个功能,「使用定时任务批量请求货代的打印面单接口然后将面单的url存到了发货单表里」,这样,发货的时候就不用一个个请求这个接口了。「这个任务就是整点跑的」。。。。。哎呦呦,我想通了,这样并发了啊,修改的同一张表,而且修改url的只会修改新建状态的url。
  1. 若是修改url的先获取发货单里所有「新建状态」的发货单
  2. 然后员工获取发货单数据,这时候因为修改url的线程需要调用api会稍微慢点
  3. 员工会把发货单修改为待拣货,然后保存入库
  4. 修改url的线程执行完了,由于我是执行的jpa的save方法,他会把自己读取到「新建状态」的数据,修改个url再保存回表,这时候,表里的数据又变成「新建状态」了。这样之后的员工又能取到这条发货单数据,然后这样不就重了。
找到原因了解决就很简单了,修改url的时候只修改这一个字段,其他的不修改就好了。至此,bug解决掉了,修改数据库数据的时候save方法还是少用啊。一波三折啊,之前那种写法也是有问题的,幸亏一起改掉了,不然之后肯定也会继续发生。刚刚我发文前,还翻到群友的对话了,摘录一些给大家。可以引入乐观锁来解决,当然只修改改动的字段是挺好的习惯。主要是事务,看提交事务在解锁前还是解锁后。要是先解锁,在提交事务,有可能被其他线程取到事务提交前的数据,这样就有问题了。其实你第一种用synchronized,我们CR会直接否掉。。因为服务那么多,你没法保证并发的时候多个服务节点的并行的情况。简单点儿说就是定时任务查询的时候查出来的对象是新建状态,然后员工改成了待拣货状态,之后定时任务更新整个对象,因为定时任务取出来的对象还是新建状态,所以更新的时候,又把待拣货状态改成新建状态了以上,简单实用的避坑小知识,送给大家!
    
      
        
              

胖虎联合两位大佬朋友,一位是知名培训机构讲师和科大讯飞架构,联合打造了 《Java架构师成长之路》 的视频教程。完全对标外面2万左右的培训课程。

除了基本的视频教程之外,还提供了超详细的课堂笔记,以及源码等资料包..


课程阶段:

  1. Java核心 提升阅读源码的内功心法
  2. 深入讲解企业开发必备技术栈,夯实基础,为跳槽加薪增加筹码
  3. 分布式架构设计方法论。为学习分布式微服务做铺垫
  4. 学习NetFilx公司产品,如Eureka、Hystrix、Zuul、Feign、Ribbon等,以及学习Spring Cloud Alibabba体系
  5. 微服务架构下的性能优化
  6. 中间件源码剖析
  7. 元原生以及虚拟化技术
  8. 从0开始,项目实战 SpringCloud Alibaba电商项目

点击下方超链接查看详情 ef5546ce2188d05d7667cd23afeaf7bc.webpef5546ce2188d05d7667cd23afeaf7bc.webpef5546ce2188d05d7667cd23afeaf7bc.webp(或者点击文末阅读原文):

(点击查看)  2024年,最新Java架构师成长之路 视频教程!

以下是课程大纲,大家可以双击打开原图查看

f42916aca50bf2a3d3b45df47a2bbdc3.webp


浏览 5
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报