Spring中@Transactional与@Async共同使用

在本文中,我们将研究Spring 框架的@Transactional和@Async注解之间的兼容性。

什么是@Transactional和@Async
@Transactional注释从许多其他注释创建原子代码块。所以,如果一个区块异常完成,所有部分都会回滚。因此,新创建的原子单元只有在其所有部分都成功时才能通过提交成功完成。

创建事务使我们能够避免代码中的部分失败,从而提高数据一致性。

另一方面,@Async告诉Spring被注解的单元可以与调用线程并行运行。换句话说,如果我们从线程调用@Async方法或类,Spring 将在具有不同上下文的另一个线程中运行其代码。

定义异步代码可以通过与调用线程并行执行单元来提高执行时间性能。

在某些情况下,我们需要代码的性能和一致性。使用 Spring,我们可以混合@Transactional和@Async来实现这两个目标,只要我们注意如何一起使用注释即可。

在以下部分中,我们将探讨不同的场景。

@Transactional和@Async可以一起工作吗?
如果我们没有正确实现异步和事务性代码,则可能会带来数据不一致等问题。

关注Spring的事务上下文和上下文之间的数据传播是充分利用@Async和@Transactional并避免陷阱的基础。

1.创建演示应用程序
我们将使用银行服务的转账功能来说明交易和异步代码的使用。

简而言之,我们可以通过从一个账户中取出资金并将其添加到另一个账户来实现转账。我们可以将其想象为数据库操作,例如选择相关帐户并更新其资金余额:

public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
    Account depositorAccount = accountRepository.findById(depositorId)
      .orElseThrow(IllegalArgumentException::new);
    Account favoredAccount = accountRepository.findById(favoredId)
      .orElseThrow(IllegalArgumentException::new);
    depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
    favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
    accountRepository.save(depositorAccount);
    accountRepository.save(favoredAccount);
}

我们首先使用findById()查找涉及的帐户,如果给定的 ID 找不到该帐户,则抛出IllegalArgumentException 。

然后,我们用新金额更新检索到的帐户。最后,我们使用CrudRepository的save()方法保存新更新的帐户。

在这个简单的示例中,存在一些潜在的故障。例如,我们可能找不到喜爱的帐户并因异常而失败。或者,depositorAccount 的save()操作完成,但favoredAccount的 save() 操作失败。这些被定义为部分失败,因为失败之前发生的事情无法撤消。

因此,如果我们没有通过事务正确管理代码,部分故障就会产生数据一致性问题。例如,我们可能会从一个帐户中取出资金,但没有有效地将其转移到另一个帐户。

2.从@Async调用@Transactional
如果我们从@Async方法调用@Transactional方法,Spring会正确管理事务并传播其上下文,确保数据一致性。

例如,让我们从 @Async调用者调用@Transactional  Transfer()方法:

@Async
public void transferAsync(Long depositorId, Long favoredId, BigDecimal amount) {
    transfer(depositorId, favoredId, amount);
    // other async operations, isolated from transfer
}
@Transactional
public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
    Account depositorAccount = accountRepository.findById(depositorId)
      .orElseThrow(IllegalArgumentException::new);
    Account favoredAccount = accountRepository.findById(favoredId)
      .orElseThrow(IllegalArgumentException::new);
    depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
    favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
    accountRepository.save(depositorAccount);
    accountRepository.save(favoredAccount);
}

TransferAsync  ()方法与不同上下文中的调用线程并行运行,因为它是@Async。

然后,我们调用事务性的transfer()方法来运行关键的业务逻辑。在这种情况下,Spring 正确地将TransferAsync()线程上下文传播到Transfer()。因此,我们不会在该交互中丢失任何数据。

Transfer  ()方法定义了一组关键的数据库操作,如果出现故障则必须回滚这些操作。 Spring仅处理transfer()事务,它将transfer()主体之外的所有代码与事务隔离。因此,Spring 仅在出现故障时回滚 Transfer()代码。

从@Async方法调用@Transactional可以通过与调用线程并行执行操作来提高性能,而不会在特定内部操作中出现数据不一致。

3.从 @Transactional调用@Async
Spring目前使用ThreadLocal来管理当前线程事务。因此,它不会在应用程序的不同线程之间共享线程上下文。

因此,如果@Transactional方法调用@Async方法,Spring 不会传播事务的相同线程上下文。

为了说明这一点,我们在Transfer()中添加对异步printReceipt()方法的调用:

@Async
public void transferAsync(Long depositorId, Long favoredId, BigDecimal amount) {
    transfer(depositorId, favoredId, amount);
}
@Transactional
public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
    Account depositorAccount = accountRepository.findById(depositorId)
      .orElseThrow(IllegalArgumentException::new);
    Account favoredAccount = accountRepository.findById(favoredId)
      .orElseThrow(IllegalArgumentException::new);
    depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
    favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
    printReceipt();
    accountRepository.save(depositorAccount);
    accountRepository.save(favoredAccount);
}
@Async public void printReceipt() { // logic to print the receipt with the results of the transfer }

Transfer ()逻辑与之前相同,但现在我们调用printReceipt() 来打印转账结果。由于printReceipt()是@Async,Spring 在具有另一个上下文的不同线程上运行其代码。

问题是收据信息取决于正确执行整个transfer()方法。此外,  printReceipt()和保存到数据库中的其余Transfer()代码在具有不同数据的不同线程上运行,使得应用程序行为不可预测。例如,我们可能会打印未成功保存到数据库中的汇款交易的结果。

因此,为了避免这种数据一致性问题,我们必须避免从@Transactional调用@Async方法,因为不会发生线程上下文传播。

4.在类级别使用@Transactional
使用@Transactional定义一个类 ,使其所有公共方法都可用于 Spring 事务管理。因此,注释会同时为所有方法创建事务。

在类级别 使用@Transactional时可能发生的一件事是在同一方法中将其与@Async混合。实际上,我们围绕该方法创建一个事务单元,该单元在与调用线程不同的线程中运行:

@Transactional
public class AccountService {
    @Async
    public void transferAsync() {
        // this is an async and transactional method
    }
    public void transfer() {
       
// transactional method
    }
}

在示例中,transferAsync()方法是事务性且异步的。因此,它定义了一个事务单元并在不同的线程上运行。因此,它可用于事务管理,但不能与调用线程处于同一上下文中。

因此,如果出现故障, transferAsync()内部的代码就会回滚,因为它是@Transactional。但是,由于该方法也是 @Async, Spring 不会将调用上下文传播给它。因此,在失败场景中,Spring 不会回滚 trasnferAsync()之外的任何代码,就像我们调用一系列仅事务性方法时一样。因此,这会遇到与从 @Transactional调用@Async相同的数据完整性问题。

类级注释可以方便地编写更少的代码来创建定义一系列完全事务性方法的类。

但是,在对代码进行故障排除时,这种混合的事务和异步行为可能会造成混乱。例如,我们期望在发生故障时回滚仅事务性方法调用序列中的所有代码。但是,如果该序列的方法也是@Async,则该行为是意外的。

结论
在本教程中,我们从数据完整性的角度了解了何时可以安全地一起使用@Transactional和@Async注释。

  • 一般来说,从@Async方法调用 @Transactional可以保证数据完整性,因为 Spring 正确地传播相同的上下文。
  • 另一方面,当从@Transactional调用@Async时,我们可能会陷入数据完整性问题。