找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

433

积分

0

好友

55

主题
发表于 昨天 18:36 | 查看: 3| 回复: 0

在日常开发中,一个常见的模式是:在接口中执行数据库写入,随后发起一个异步任务来处理后续逻辑。然而,当一个方法被 @Transactional 注解修饰时,一个极其隐蔽但破坏性极强的问题便随之而来——异步任务可能早于数据库事务提交执行

本文将结合一个典型的业务场景,深入剖析这一问题的根本原因,并提供可靠的最佳实践。

业务场景分析

假设我们有一个预约创建接口,其核心逻辑如下:

  1. 保存预约主表信息。
  2. 写入一条通知任务记录。
  3. 异步处理该通知任务。

最初的实现代码可能如下所示:

@Transactional
@Override
public void createReservation(ReservationInfoVO reservationInfoVO) {
    // 1. 保存预约信息
    ReservationInfoPO reservationInfoPO = ...build();
    reservationInfoMapper.insert(reservationInfoPO);
    // 2. 保存通知任务
    NotificationTaskPO notificationTaskPO = ...build();
    notificationTaskMapper.insert(notificationTaskPO);
    // 3. 异步处理任务
    CompletableFuture.runAsync(() -> 
        reservationHandler.reservationProcess(notificationTaskPO.getId()),
        ThreadPoolUtils.RESERVATION_SYNC_THREAD_POOL
    );
}

代码逻辑看似清晰无误,但在实际运行中,异步线程 reservationHandler.reservationProcess 偶尔会抛出异常,提示:

  • 查询不到对应的 notification_task 记录。
  • 或关联的 reservation_id 为空。

问题根源探究

现象:为什么异步线程查不到刚刚插入的数据?

许多开发者的第一反应可能是:

  • MyBatis 的 insert() 方法没有真正执行?
  • 异步任务启动得太快了?
  • 主键没有成功回填?

实际上,这些都是误解。insert() 方法是同步执行的,当方法调用返回时,SQL 已经发送到数据库执行,并且生成的主键通常也已回填到实体对象中。

关键在于:数据“写入”不等于数据“提交”

在 Spring 的 事务管理 机制下,被 @Transactional 注解的方法,其内部所有数据库操作都归属于同一个数据库事务。这个事务的提交时机,是方法正常执行完毕并返回之后

而代码中通过 CompletableFuture.runAsync 发起的异步任务,很可能在方法返回前、事务提交前就已经开始执行了。此时,异步线程去查询数据库,由于常见的数据库隔离级别(如 READ_COMMITTED)保证了它无法读取其他未提交事务的数据,因此自然“看”不到主线程刚刚插入的记录。

流程示意图如下: 事务与异步任务时序问题

结论:问题的本质是事务提交的时机晚于异步任务的执行时机,导致异步任务读取到了一个不一致的数据库状态。

最佳解决方案:在事务提交后执行

要彻底解决此问题,必须确保异步任务在数据库事务成功提交之后才启动。Spring 提供了强大的事务同步机制 TransactionSynchronizationManager,可以让我们精准地挂接到事务生命周期的特定阶段。

优化后的代码如下:

@Transactional
@Override
public void createReservation(ReservationInfoVO reservationInfoVO) {
    // 1. 保存预约信息
    reservationInfoMapper.insert(reservationInfoPO);
    // 2. 保存通知任务
    notificationTaskMapper.insert(notificationTaskPO);
    Long taskId = notificationTaskPO.getId();
    // 3. 注册事务同步器,在提交后执行异步逻辑
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                CompletableFuture.runAsync(() ->
                    reservationHandler.reservationProcess(taskId),
                    ThreadPoolUtils.RESERVATION_SYNC_THREAD_POOL
                );
            }
        }
    );
}

为何这是最优解?

  • 强一致性:确保异步任务读取到的数据是已提交的、稳定的。
  • 规避风险:从根本上避免了因脏读、查不到数据导致的业务逻辑失败。
  • 解耦清晰:将异步任务的触发时机与事务生命周期明确绑定,代码意图更清晰。

其他可行方案

1. 使用 @TransactionalEventListener

Spring 提供了事件监听机制,可以监听与事务相位绑定的事件。

// 发布事件
applicationEventPublisher.publishEvent(new NotificationCreatedEvent(taskId));

// 监听事件,在事务提交后处理
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleNotificationEvent(NotificationCreatedEvent event) {
    // 执行异步逻辑
    reservationHandler.reservationProcess(event.getTaskId());
}

2. 异步重试机制(辅助方案)

为异步任务增加重试逻辑,可以作为一种兜底策略,提高系统的健壮性,但无法从根源上解决数据不可见的问题。

总结

@Transactional 注解的方法内直接启动异步任务,是一个典型的“坑”。其表象是异步任务读取数据失败,根本原因在于数据库事务的提交滞后于异步线程的执行

最佳实践是:利用 Spring 的 TransactionSynchronizationManager.afterCommit()@TransactionalEventListener(phase = AFTER_COMMIT),确保异步逻辑在事务提交后触发。 这种处理方式能使你的代码更加健壮,并能从容应对高并发场景下的数据一致性问题。




上一篇:Kubernetes本地存储实战:LocalPathProvisioner部署与调度机制详解
下一篇:.NET Core高性能日志解析实战:基于SIMD的向量化优化指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 00:24 , Processed in 0.092867 second(s), 36 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表