记项目的BUG,关于Spring事务传播特性项目中的使用

本文介绍了一个因事务传播特性导致的死循环问题,在使用Dubbo组件生成序列号时出现,并提供了通过调整事务传播级别来解决问题的具体方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

就是我之前通过Dubbo调用过的一个生成序列号的一个组件服务 他的作用就是按照规则生成一个序列号 我的另一个项目需要用到这个 但是我不想再创建一个新的服务 就直接把组件的包放入这个项目中,之后导致项目代码陷入死循环

源码以及SQL

这边写了一个问题复现的代码程序地址如下 

https://gitee.com/echohawk/DemoProject

CREATE TABLE `seq` (
   `id` bigint(20) NOT NULL COMMENT '主键',
   `type` varchar(255) NOT NULL,
   `num` bigint(20) NOT NULL DEFAULT '0',
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO seq (id,type,num) VALUES (1,'order_no',1);

代码分析

 执行测试代码cn.echohawk.AppTest#testEndlessLoop 这里用两个线程去同时执行 doBussiness方法

@Test
public void testEndlessLoop() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 2; i ++) {
            executorService.submit(() -> {
                for (int i1 = 0; i1 < 1; i1++) {
                    bussinessServer.doBussiness();
                }
            });
        }
        Scanner scanner = new Scanner(System.in);
        scanner.nextLine();
}

而doBussiness方法是开启了事务的,这里的事物的传播特性默认是REQUIRED。也就是支持当前事务,如果不存在就新建一个

@Override
    @Transactional(rollbackFor = Exception.class)
    public void doBussiness() {
        System.out.println("线程【" + Thread.currentThread().getName() + "】 开始执行");
        Long order_no = seqNoService.getSeqAndIncrement("order_no", 1);
        System.out.println("线程【" + Thread.currentThread().getName() + "】 生成成功 order_no为" + order_no);
        //假装处理业务耗时
        try {
            Thread.sleep(1000);
        }catch (Exception e) {
        }
        System.out.println("线程【" + Thread.currentThread().getName() + "】 执行结束");
    }

SeqNoService没更改事务的隔离级别的话 必然是使用了doBussiness的事务 问题就出现在下面两段代码中

这里的正常的逻辑是开启事务 并且查询当前数据库中的num的值 然后计算出新的num的值 并通过数据库原值作为更新的条件 把数据库的值更新为新的值 之前项目是把SeqNoService作为一个微服务提供Dubbo接口给其他微服务使用 那必然是不会有问题的 事务会在调用getAndIncreWithOptimisticLock方法打开 方法结束之后提交 并且如果更新失败会提交事务 并在getSeqAndIncrement方法继续循环调用getAndIncreWithOptimisticLock并且开启新事务 此时getSeq方法查询的值进入是getAndIncreWithOptimisticLock方法的时候的值,所以每次循环查询的都是最新的值

但是导致这里出问题的原因是doBussiness的开启的事务传播到了底层所有逻辑 也就是getAndIncreWithOptimisticLock使用的还是doBussiness开启的事务 此时getSeq方法查询的值是doBussiness开启的事务时候的状态,所以每次循环都用的是doBussiness时开启的事务。所以查询的值 一直是doBussiness开启事务的时候的快照值 所以更新一直失败

public Long getSeqAndIncrement(String numberName, int increment) {
        Long seq;
        do {
            seq = this.seqServer.getAndIncreWithOptimisticLock(numberName, increment);
            if (seq == null) {
                System.out.println("线程【" + Thread.currentThread().getName() + "】 重新尝试");
            }
        } while(seq == null);

        return seq;
    }
@Override
    @Transactional(rollbackFor = Exception.class)
    public Long getAndIncreWithOptimisticLock(String type, int increment) {
        long oldSeq = getSeq(type);
        long newSeq = oldSeq + increment;

        SeqDO entity = new SeqDO();
        entity.setNum(newSeq);
        LambdaQueryWrapper<SeqDO> qw = new LambdaQueryWrapper();
        qw.eq(SeqDO::getType, type);
        qw.eq(SeqDO::getNum, oldSeq);
        int update = seqMapper.update(entity, qw);
        if (update==1) {
            System.out.println("线程【" + Thread.currentThread().getName() + "】 更新成功 已经把num更新为" + newSeq);
            return newSeq;
        } else {
            System.out.println("线程【" + Thread.currentThread().getName() + "】 更新失败 更新条件 update from seq set num = " + newSeq + " where num=" + oldSeq + " and type = '" + type + "'");
        }
        return null;
    }

这里就是执行结果 一直循环:

Sql的执行过程还原的截图

解决方案     

 只需要在SeqNoService加上一个注解 @Transactional(propagation = Propagation.NOT_SUPPORTED) 使用

NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务;

这样的话调用getSeqAndIncrement的时候就是不使用事务执行,那时候doBussiness事务是挂起的状态 直到getAndIncreWithOptimisticLock才会开启新事务 这样就和之前微服务那种情况下的事务的情况是一样的

@Transactional(propagation = Propagation.NOT_SUPPORTED)
    public Long getSeqAndIncrement(String numberName, int increment) {
        Long seq;
        do {
            seq = this.seqServer.getAndIncreWithOptimisticLock(numberName, increment);
            if (seq == null) {
                System.out.println("线程【" + Thread.currentThread().getName() + "】 重新尝试");
            }
        } while(seq == null);

        return seq;
    }

  事实测试之后也是能正常运行 测试代码cn.echohawk.AppTest#testWithoutPropagation

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值