SpringBoot事务异步调用引发的bug

3552 篇文章 114 订阅

前言

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

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

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

typescript复制代码@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的方法即可

typescript复制代码@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执行完事务提交完,再同步发起异步调用岂不就绕开了这个问题。

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

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

TransactionSynchronizationManager事务回调

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

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

直接上代码举例子

typescript复制代码@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 的硬编码。

首先需要自定义监听器

java复制代码@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();
      }
   }
}

定义事件

arduino复制代码public class TaskCompletedEvent {
   private Task task;

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

   public Task getTask() {
      return task;
   }
}

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot中,我们可以使用@Async注解来实现异步方法调用。首先,我们需要在主类上添加@EnableAsync注解,以开启异步功能。然后,在需要异步执行的方法上添加@Async注解。Spring会将这些方法的执行放在异步线程中进行。使用@Async注解需要满足以下条件: 在测试类中,我们可以通过创建一个TaskExecutor来执行异步任务。首先,我们需要在测试类中注入TaskExecutor的实例。然后,我们可以使用taskExecutor.execute方法来执行异步任务。例如,在测试类中的test1方法中,我们使用taskExecutor.execute(() -> System.out.println("异步任务"));来执行一个异步任务。同时,我们还可以使用同步操作来进行对比。执行完成后,我们可以通过StopWatch来计算耗时,并输出结果。 因此,通过添加@Async注解和使用TaskExecutor来执行异步任务,我们可以在Spring Boot中实现异步调用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [SpringBoot实现异步调用的几种方式](https://blog.csdn.net/qinxun2008081/article/details/131093083)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值