likes
comments
collection
share

SpringBoot事务异步调用引发的bug

作者站长头像
站长
· 阅读数 14

前言

日常开发中有没有遇到这种场景,save一条数据后发起一次异步调用,举个例子,假设我们以mysql组件和xxl-job组件为例,创建一条数据导出任务,创建后默认启动任务。那么逻辑可能大致为三步。

  1. 创建导出任务(DB.EXPORT_TASK)
  2. 创建导出任务历史记录(DB.EXPORT_TASK_HISTORY)
  3. 触发导出任务(XXL-JOB)

因为需要同时要创建导出任务导出任务历史两条记录,所以代码中需要通常要添加事务

@Service
public class TaskService {

    @Transactional(rollbackFor = Exception.class)
    public String saveExportTask() {
        // 1. save export task

        // 2. save export task history 

        // 3. execute xxl-job
    }

}

外层controller层只需要调用service的方法即可

@RestController
public class TaskController {
    @Resource TaskService taskService;
    
    @PostMapping
    public String save() {
        taskService.saveExportTask();
    }
}

我们使用了xxl-job去触发任务是一个异步调用的过程,当xxl-job回调执行器去执行时可能需要根据job_id获取到导出任务的配置,通过查询db获取任务详情,比如导出地址了,导出规范等等。

看似非常和谐的场面,实际执行起来则会出现任务不存在的问题。问题的根源其实也很好理解,就是因为在异步方法里做了同步的事就会出现这种问题,当第一步没有执行完,第三步的回调方法已经到执行器了,也就是说一个任务还没存到数据库,执行这个任务时去数据库查该任务的明细肯定会报任务不存在异常了。

那么如何解决呢。

代码拆分

最简单的一个方案,web应用通常划分为controller、service、dao层那么几层,业务逻辑按规范写在service层,我们把发起异步调用的方法挪到controller层,service只做数据库操作,servcie执行完事务提交完,再同步发起异步调用岂不就绕开了这个问题。

@RestController
public class TaskController {
    @Resource TaskService taskService;
    
    @PostMapping
    public String save() {
        taskService.saveExportTask();
        // 3. execute xxl-job
    }
}

如果秉持着代码和人有一个能跑就行的原则,此时已经结束战斗了,对于秉持着该原则且有点代码洁癖同学顶多也就是把触发任务的动作封装到一个触发service里调用。

TransactionSynchronizationManager事务回调

当然还是有很多同学对待技术是追求极致精神的,那么有没有优雅的方式去解决这个问题,那就要看springboot的事务回调能力了。

TransactionSynchronizationManager 事务同步器,从new TransactionSynchronization()可实现的方法上即可管中窥豹可见一斑,我们完全可以通过实现歇歇方法实现事务完成后回调的逻辑。

SpringBoot事务异步调用引发的bug

直接上代码举例子

@Service
public class TaskService {

    @Transactional(rollbackFor = Exception.class)
    public String saveExportTask() {
        // 1. save export task

        // 2. save export task history 

        
        
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter(){
            public void afterCommit(){
                
                System.out.println("commit!!!");
                // 3. execute xxl-job
            }
        });
    }

}

这样一来就可以保证是在事务结束之后去执行xxl-job的任务。

@TransactionalEventListener注解要和事务事件监控

TransactionalEventListener,自 Spring 4.2 以来,可以使用基于注释的配置为提交后事件(或更一般的事务同步事件,例如回滚)定义侦听器。本质上是基于核心 spring中的事件处理。使用这种方法可以避免对 TransactionSynchronizationManager 的硬编码。

首先需要自定义监听器

@Component
public class TaskEventListener {

   @Autowired
   private TaskService taskService;

   @TransactionalEventListener
   public void handleOrderCreatedEvent(TaskCreatedEvent event) {
      Task task = event.getTask();
      // 处理订单创建事件
      try {
         taskService.processOrder(task);
      } catch (Exception e) {
         // 处理失败,抛出异常,事务回滚
         throw new RuntimeException(e);
      }
   }

   @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
   public void handleOrderCompletedEvent(TaskCompletedEvent event) {
      Task task = event.getTask();
      // 处理订单完成事件
      try {
         taskService.sendOrderConfirmationEmail(task);
      } catch (Exception e) {
         // 处理失败,不影响事务
         e.printStackTrace();
      }
   }
}

定义事件

public class TaskCompletedEvent {
   private Task task;

   public TaskCompletedEvent(Task task) {
      this.task = task;
   }

   public Task getTask() {
      return task;
   }
}

@TransactionalEventListener注解要和@Transactional注解配合使用,确保在事务完成后才会触发回调方法。@TransactionalEventListener注解也可以指定回调方法的触发时机,可以选择在事务提交后触发(默认)或在事务回滚后触发。