掘金 后端 ( ) • 2024-04-15 21:41

1. 引言

@Transactional@Async能混在一起使用吗?我猜“百分之180”的人都回答不上来这个问题,也许根本就没有这么用过。

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

2. 理解 @Transactional@Async

@Transactional 注解创建了一个原子代码块。因此,如果其中一个代码块执行异常,所有部分都会回滚。因此,新创建的原子单元只有在所有部分都成功时,通过提交才能成功完成。

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

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

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

有些场景下,我们需要代码中同时具备性能和一致性。在 Spring 中,我们可以将 @Transactional@Async 结合使用,以实现这两个目标,只要我们注意如何一起使用这些注解。

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

3. @Transactional 和 @Async 能一起使用吗?

如果我们没有正确实现异步和事务代码,可能会产生诸如数据不一致等问题。

要充分利用 @Async@Transactional 并避免Bug和陷阱,我们必须注意 Spring 的事务上下文以及上下文之间的数据传播。

3.1. 创建Demo应用程序

在这里,我们以银行的转账功能来说明事务和异步代码的使用。

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

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

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

在这个简单的例子中,存在几个潜在的失败点。例如,我们可能找不到 favoredAccount 并因异常而失败。或者,save() 操作对 depositorAccount 完成,但对 favoredAccount 失败。这些被定义为部分失败,因为失败之前发生的事情无法撤销。

因此,如果我们不使用事务机制来编写和管理代码,部分失败会导致数据一致性问题。例如,我们可能从一个账户中移除了资金,但没有有效地将其传递给另一个账户。

3.2. 从 @Async 调用 @Transactional

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

例如,让我们从 @Async 调用者中调用一个 @Transactionaltransfer() 方法:

@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() 方法体外的所有代码与事务隔离开来。因此,只有在 transfer() 中发生失败时,Spring 才会回滚其代码。

@Async 方法中调用 @Transactional 方法可以提高性能,因为它可以在与调用线程并行执行操作的同时,确保特定内部操作的数据一致性

3.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 方法,因为不会发生线程上下文传播

3.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 的,那么行为就会出乎意料。 file

4. 结论

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

通常,@Async 方法中调用 @Transactional 方法可以保证数据完整性,因为 Spring 会正确地传播相同的上下文。

但是,@Transactional 中调用 @Async 方法时,我们可能会遇到数据完整性问题

你学废了吗?