⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

@Transactional 和 @Async 这两个注解更是开发者们常常使用的得力工具。然而,当这两个注解相遇,它们能否和谐共处,发挥出最大的效能呢?

相信很多开发者都没有深入思考过这个问题。今天,就让我们一起深入探讨一下 Spring 框架中 @Transactional 和 @Async 注解之间的兼容性。

深入理解 @Transactional 和 @Async

@Transactional 注解就像是一位严谨的管家,它会创建一个原子代码块。在这个代码块里,所有的操作都被视为一个整体。一旦其中某个操作出现异常,就如同多米诺骨牌一样,所有已经执行的部分都会被回滚。只有当这个原子单元中的所有操作都成功完成时,才会通过提交操作正式生效。使用事务机制,我们可以有效地避免代码出现部分失败的情况,从而大大提高数据的一致性。

@Async 注解则像是一位充满活力的短跑选手,它告诉 Spring,被注解的方法或类可以与调用线程并行运行。当我们从一个线程调用一个 @Async 方法时,Spring 会在另一个具有不同上下文的线程中启动该方法的执行。这种异步执行的方式可以显著提高程序的执行效率,尤其是在处理一些耗时的操作时。

在某些复杂的业务场景中,我们既希望代码能够保证数据的一致性,又希望能够提高执行性能。在 Spring 中,我们确实可以尝试将 @Transactional 和 @Async 结合使用,以实现这两个看似矛盾的目标。但在实际操作中,我们必须格外小心,注意如何正确地使用这两个注解。

@Transactional\ 和 *@Async* 能一起使用吗?

1. 构建示例应用:银行转账功能

为了更好地说明事务和异步代码的使用,我们以银行的转账功能为例。简单来说,转账就是从一个账户中取出一定金额的钱,然后将其添加到另一个账户中。这一系列操作可以看作是对数据库中账户信息的更新操作。

我们的具体实现步骤如下:首先,使用 findById() 方法根据账户 ID 查找对应的账户信息。如果给定的 ID 没有找到对应的账户,就会抛出 IllegalArgumentException 异常。

接着,我们会用新的金额更新检索到的账户信息。最后,使用 CrudRepository 的 save() 方法将更新后的账户信息保存到数据库中。

在这个看似简单的例子中,其实存在着一些潜在的风险点。比如,我们可能找不到目标账户,从而导致操作因异常而失败。又或者,save() 操作在更新转出账户时成功了,但在更新转入账户时却失败了。

这些情况都属于部分失败,因为在失败之前已经执行的操作无法撤销。如果我们不使用事务机制来管理这些代码,部分失败就很可能会导致数据一致性问题。

例如,我们可能从一个账户中移除了资金,但却没有成功将其转移到另一个账户中。

2. 从 @Async 调用 @Transactional

当我们从 @Async 方法中调用 @Transactional 方法时,Spring 会发挥其强大的管理能力,正确地管理事务并传播其上下文,从而确保数据的一致性。

让我们来看一个具体的例子。假设我们有一个 transferAsync() 方法,它被 @Async 注解修饰,这意味着它会在一个与调用线程不同的上下文中并行运行。在这个方法中,我们调用了一个被 @Transactional 注解修饰的 transfer() 方法来执行关键的业务逻辑。

在这种情况下,Spring 会将 transferAsync() 线程的上下文正确地传播给 transfer() 方法。这样一来,我们就不会在这个交互过程中丢失任何数据。

transfer() 方法定义了一组关键的数据库操作,如果在执行过程中发生任何失败,Spring 会自动回滚这些操作。需要注意的是,Spring 只处理 transfer() 方法内部的事务,会将 transfer() 方法体外的所有代码与事务隔离开来。因此,只有当 transfer() 方法内部发生失败时,Spring 才会回滚其代码。

从 @Async 方法中调用 @Transactional 方法是一种非常巧妙的设计,它既可以通过并行执行操作来提高性能,又可以确保特定内部操作的数据一致性,实现了性能和数据一致性的双赢。

3. 从 @Transactional 调用 @Async

Spring 目前使用 ThreadLocal 来管理当前线程的事务,这意味着它不会在应用程序的不同线程之间共享线程上下文。因此,如果 @Transactional 方法调用 @Async 方法,Spring 不会将同一事务的线程上下文传播给 @Async 方法。

为了更直观地理解这个问题,我们在 transfer() 方法内部添加一个对异步 printReceipt() 方法的调用。

transfer() 方法的逻辑与之前相同,只是增加了调用 printReceipt() 方法来打印转账结果的操作。由于 printReceipt() 方法被 @Async 注解修饰,Spring 会在另一个上下文的不同线程上运行其代码。

这里就存在一个问题,收据信息的打印依赖于 transfer() 方法的整个正确执行。然而,printReceipt() 方法和 transfer() 方法中保存到数据库的其余代码在不同的线程上运行,且数据不同,这就使得应用程序的行为变得不可预测。例如,我们可能会打印一个在成功保存到数据库之前的转账交易结果。

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

4. 在类级别使用 @Transactional

使用 @Transactional 注解定义一个类时,该类的所有公共方法都会被纳入 Spring 的事务管理范围。这意味着该注解会一次性为所有方法创建事务。

在类级别使用 @Transactional 时,可能会出现同一个方法同时使用 @Async 注解的情况。实际上,我们是在该方法的周围创建了一个事务单元,并且这个事务单元会在与调用线程不同的线程上运行。

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

因此,如果发生失败,transferAsync() 内部的代码会回滚,因为它是 @Transactional 的。然而,由于该方法也是 @Async 的,Spring 不会将调用上下文传播给它。因此,在失败的情况下,Spring 不会回滚 trasnferAsync() 之外的任何代码,就像我们调用一系列仅包含事务的方法时一样。因此,这与从 @Transactional 中调用 @Async 面临相同的数据完整性问题。

类级别的注解对于编写较少代码以创建定义一系列完全事务性方法的类非常有用。

但是,这种混合的事务性和异步行为在调试代码时可能会造成混淆。例如,我们期望在发生失败时,一系列仅包含事务的方法调用中的所有代码都会回滚。然而,如果这一系列方法中的某个方法也是 @Async 的,那么行为就会出乎意料。

总结

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

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

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

文章目录
  1. 1. 深入理解 @Transactional 和 @Async
  2. 2. @Transactional\ 和 *@Async* 能一起使用吗?
    1. 2.1. 1. 构建示例应用:银行转账功能
    2. 2.2. 2. 从 @Async 调用 @Transactional
    3. 2.3. 3. 从 @Transactional 调用 @Async
    4. 2.4. 4. 在类级别使用 @Transactional
  3. 3. 总结