Spring中使用@Async与@Transactional协调异步与事务处理

本文旨在阐明 Spring@Transactional和@Async注释的协同使用,提供对它们的集体应用程序的见解,以优化 Java 应用程序的性能。 

什么是 Spring 中的事务管理
事务管理在任何企业应用程序中对于确保数据一致性和完整性都至关重要。在Spring中,这是通过@Transactional注解来实现的,注解抽象了底层的事务管理机制,使开发人员更容易以声明方式控制事务边界。

@Transactional 注解
Spring中的注解@Transactional可以应用于类和方法级别。它声明类的一个方法或所有方法应该在事务上下文中执行。

Spring@Transactional支持各种属性,例如propagation、isolation、timeout和readOnly,允许微调事务管理。

  • 传播Propagation:定义事务如何相互关联;常见选项包括REQUIRED、REQUIRES_NEW和SUPPORTS
  • 隔离性Isolation:确定一项事务所做的更改如何对其他事务可见;选项包括READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。
  • 超时Timeout:指定事务必须完成的时间范围
  • ReadOnly:表示事务是否只读,优化某些数据库操作

Spring中的异步编程是什么
Spring 中的异步操作通过 @Async 注解进行管理,使方法调用在后台线程池中运行,从而不会阻塞调用线程。这对于耗时或独立于主执行流的操作尤其有利。

@Async 注解
只要 Spring 任务执行器配置正确,用 @Async 标记一个方法就能使其异步执行。此注解可用于返回 void、Future 或 CompletableFuture 对象的方法,允许调用者跟踪操作的进度和结果。

将 @Transactional 与 @Async 结合起来
将事务管理与异步操作相结合会带来独特的挑战和难度,这主要是因为事务与线程的上下文相关联,而 @Async 会导致方法执行切换到不同的线程。

异步与事务结合的问题
挑战和考虑因素

  • 事务上下文传播:从 @Transactional 上下文中调用 @Async 方法时,事务上下文不会自动传播到异步方法执行线程。
  • 最佳实践:要在异步方法中管理事务,关键是要确保负责事务管理的方法与标有 @Async 的方法不是同一个。相反,异步方法应调用另一个 @Transactional 方法,以确保正确建立事务上下文。

案例代码

@Service
public class InvoiceService {

    @Async
    public void processInvoices() {
        // Asynchronous operation
        updateInvoiceStatus();
    }

    @Transactional
    public void updateInvoiceStatus() {
       
// Transactional operation
    }
}


在本例中,processInvoices 是一个异步方法,它调用 updateInvoiceStatus(一个事务方法)。这种分离可确保在异步执行上下文中进行适当的事务管理。

@Service
public class ReportService {

    @Async
    public CompletableFuture<Report> generateReportAsync() {
        return CompletableFuture.completedFuture(generateReport());
    }

    @Transactional
    public Report generateReport() {
        // Transactional operation to generate a report
    }
}

在这里,generateReportAsync 异步执行并返回 CompletableFuture,而 generateReport 则处理报告生成的事务性问题。

关于事务传播的讨论
Spring 中的事务传播行为定义了事务之间的关系,尤其是在一个事务方法调用另一个事务方法的情况下。选择正确的传播行为对于实现所需的事务语义至关重要。

  • REQUIRED (默认):这是默认的传播行为。如果存在现有事务,方法将在该事务中运行。如果没有现有事务,Spring 将创建一个新事务。
  • REQUIRES_NEW:此行为总是启动一个新事务。如果有一个现有事务,它将被暂停,直到新事务完成。当你需要确保方法在一个新的、独立的事务中执行时,这种方式非常有用。
  • SUPPORTS:使用此行为时,如果存在事务,方法将在现有事务中执行。但是,如果没有现有事务,方法将以非事务方式运行。
  • NOT_SUPPORTED(不支持):此行为将以非事务方式执行方法。如果存在事务,该事务将被暂停,直到方法完成。
  • MANDATORY(必须):此行为要求有一个现有事务。如果没有现有事务,Spring 将抛出异常。
  • NEVER:该方法绝不应在事务中运行。如果存在事务,Spring 将抛出异常。
  • NESTED:如果存在现有事务,该行为将启动嵌套事务。嵌套事务允许部分提交和回滚,某些事务管理器(但并非所有事务管理器)都支持嵌套事务。

异步操作的传播行为
将 @Transactional 与 @Async 结合使用时,理解传播行为的含义变得更加重要。由于异步方法在单独的线程中运行,某些传播行为可能会因为新线程中没有事务上下文而无法按预期运行。

  • REQUIRED 和 REQUIRES_NEW:这是最常用、最直接的行为。不过,当与 @Async 一起使用时,REQUIRES_NEW 行为通常更可预测,因为它能确保异步方法始终启动一个新事务,避免与调用方法的事务发生意外交互。
  • SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER:当与 @Async 一起使用时,这些行为可能会导致意想不到的结果,因为调用线程的事务上下文不会传播到异步方法的线程。在异步处理中使用这些行为时,需要仔细考虑和测试。
  • NESTED:鉴于嵌套事务的复杂性和 @Async 方法的独立线程上下文,一般不建议在异步操作中使用嵌套事务。它可能导致复杂的事务管理情况,难以调试和维护。

异步操作的传播
为了说明不同传播行为与异步操作之间的交互,让我们来考虑一个异步服务方法调用具有不同传播行为的事务方法的示例。

@Service
public class OrderProcessingService {

    @Autowired
    private OrderUpdateService orderUpdateService;

    @Async
    public void processOrdersAsync(List<Order> orders) {
        orders.forEach(order -> orderUpdateService.updateOrderStatus(order, Status.PROCESSING));
    }
}

@Service
public class OrderUpdateService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrderStatus(Order order, Status status) {
        // Implementation to update order status
    }
}

在本例中,processOrdersAsync 是一个异步方法,用于处理订单列表。它调用每个订单上的 updateOrderStatus,并用 @Transactional(propagation = Propagation.REQUIRES_NEW)标记。这确保了每个订单状态更新都发生在一个新的、独立的事务中,将每个更新操作与其他操作和原始异步流程隔离开来。

示例@Transactional(REQUIRES_NEW) 与@Async

@Service
public class UserService {

    @Async
    public void updateUserAsync(User user) {
        updateUser(user); // Delegate to the synchronous, transactional method
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
       
// 更新用户的逻辑
       
// 持续更改的数据库操作
    }
}

在这里,updateUserAsync 是一个异步方法,它调用 updateUser,这是一个注释了 @Transactional 和 REQUIRES_NEW 传播行为的方法。这种配置可确保每次用户更新操作都在一个新事务中进行,并与任何现有事务隔离。这在更新操作必须不受其他事务结果影响的情况下特别有用。

在类级别上将 @Async 与 @Transactional 结合起来

@Service
@Transactional
public class OrderService {

    @Async
    public void processOrderAsync(Order order) {
        processOrder(order); // 委托同步方法
    }

    public void processOrder(Order order) {
       
// 处理订单的逻辑
       
// 订单处理过程中涉及的数据库操作
    }
}

在这种情况下,OrderService 类被注释为 @Transactional,默认情况下对其所有方法应用事务管理。带有 @Async 标记的 processOrderAsync 方法通过调用 processOrder 异步执行订单处理。类级 @Transactional 注解确保订单业务处理逻辑能在事务上下文中执行,从而为相关数据库操作提供一致性和完整性。

@Async 方法调用多个 @Transactional 方法

@Service
public class ReportGenerationService {

    @Autowired
    private DataService dataService;

    @Async
    public void generateReportAsync(ReportParameters params) {
        Report report = dataService.prepareData(params);
        dataService.saveReport(report);
    }
}

@Service
public class DataService {

    @Transactional
    public Report prepareData(ReportParameters params) {
        // 数据准备逻辑
        return new Report();
//实际报告生成的占位符
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveReport(Report report) {
       
//保存报告的逻辑
    }
}

此示例中的异步方法 generateReportAsync 通过调用两个独立的事务方法:prepareData 和 saveReport 来协调报告生成过程。

  • prepareData 方法封装在默认事务上下文中,
  • 而 saveReport 被明确配置为始终在新事务中执行。

这种设置非常适合报告保存操作saveReport 需要在事务上独立于数据准备阶段prepareData 的情况,可确保报告的保存saveReport不受前面操作成功或失败的影响。

上述每个示例都展示了如何通过 @Transactional 和 @Async 的不同组合,在异步处理上下文中实现特定的事务行为,从而为 Spring 开发人员提供了根据应用程序要求定制事务管理策略的灵活性。

结论
在 Spring 应用程序中,了解并谨慎选择适当的事务传播行为至关重要,尤其是在将事务操作与异步处理相结合时。通过考虑每种传播行为的具体要求和影响,开发人员可以在其 Spring 应用程序中设计出更稳健、高效和可靠的事务管理策略。有了这些扩展知识,就能更自信、更精确地处理复杂的事务场景,最终实现更高质量的软件解决方案。