背景
就是我之前通过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