[译]深入了解Spring事务管理:@Transactional
您可以使用本指南来简单实用地了解 Spring 使用 @Transactional 注释进行事务管理的工作原理。
唯一的先决条件?您需要对 ACID 有一个粗略的了解,即什么是数据库事务以及为什么使用它们。此外,这里不讨论分布式事务或反应式事务,但就 Spring 而言,一般原则仍然适用。
介绍
在本指南中,您将了解 Spring 核心事务抽象框架的主要支柱(这是一个令人困惑的术语,不是吗?) - 用大量代码示例进行描述:
@Transactional
(声明式事务管理)与编程式事务管理。- 物理事务与逻辑事务。
- Spring
@Transactional
与 JPA/Hibernate 集成。 - Spring
@Transactional
与 Spring Boot 或 Spring MVC 集成。 - 回滚、代理、常见陷阱等等。
与 Spring 官方文档相反,本指南不会直接深入探讨 Spring-first 主题,从而让您感到困惑。
相反,您将以非常规的方式学习 Spring 事务管理:从头开始,一步一步。这意味着,从普通的老式 JDBC 事务管理开始。
Why? 为什么?
因为 Spring 所做的一切都是基于这些 JDBC 基础知识。如果您掌握了这些基础知识,稍后您将可以使用 Spring 的 @Transactional 注释节省大量时间。
普通 JDBC 事务管理的工作原理
如果您在不完全了解 JDBC 事务的情况下考虑跳过本节:请不要这样做。
如何启动、提交或回滚 JDBC 事务
第一个重要的收获是:无论您使用 Spring 的 @Transactional 注释、普通 Hibernate、jOOQ 还是任何其他数据库库,都没有关系。
最后,它们都执行相同的操作来打开和关闭(我们称之为“管理”)数据库事务。普通的 JDBC 事务管理代码如下所示:
import java.sql.Connection;
Connection connection = dataSource.getConnection(); // (1)
try (connection) {
connection.setAutoCommit(false); // (2)
// execute some SQL statements...
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
- 您需要连接到数据库才能启动事务。 DriverManager.getConnection(url,user,password)也可以工作,尽管在大多数企业应用程序中,您将配置一个数据源并从中获取连接。
- 这是在 Java 中“启动”数据库事务的唯一方法,尽管这个名字听起来有点不对劲。 setAutoCommit(true) 确保每个 SQL 语句自动包装在自己的事务中,而 setAutoCommit(false) 则相反:您是事务的主人,您需要开始调用
*commit*
标志在连接打开的整个时间内都有效,这意味着您只需调用该方法一次,而不是重复。 - 让我们提交我们的交易…
- 或者,如果出现异常,则回滚我们的更改。
是的,这 4 行(过于简单化的)是当你使用 @Transactional 注释时 Spring 所做的一切。在下一章中,您将了解其工作原理。但在我们开始之前,您还需要学习一些知识。
(聪明人的一个快速说明:像 HikariCP 这样的连接池库可能会根据配置自动为您切换自动提交模式。但这是一个高级主题。)
如何使用 JDBC 隔离级别和保存点
如果您已经使用过 Spring 的 @Transactional 注释,您可能遇到过类似的情况:
@Transactional(propagation=TransactionDefinition.NESTED,
isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)
稍后我们将更详细地介绍嵌套 Spring 事务和隔离级别,但再次了解这些参数都可以归结为以下基本 JDBC 代码会有所帮助:
import java.sql.Connection;
// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)
// propagation=TransactionDefinition.NESTED
Savepoint savePoint = connection.setSavepoint(); // (2)
...
connection.rollback(savePoint);
- 这就是 Spring 在数据库连接上设置隔离级别的方式。不完全是火箭科学,是吗?
- Spring 中的嵌套事务只是 JDBC/数据库保存点。例如,如果您不知道什么是保存点,请查看本教程。请注意,保存点支持取决于您的 JDBC 驱动程序/数据库。
Spring 或 Spring Boot 的事务管理如何工作
现在您已经对 JDBC 事务有了很好的了解,让我们看看简单的、核心的 Spring 是如何管理事务的。这里的所有内容都一一适用于 Spring Boot 和 Spring MVC,稍后会详细介绍。
Spring 的事务管理或其(名称相当令人困惑的)事务抽象框架实际上是什么?
请记住,事务管理简单地说就是:Spring 如何启动、提交或回滚 JDBC 事务?这听起来是不是很熟悉?
问题是:使用普通 JDBC,您只有一种方法 (setAutocommit(false)) 来管理事务,而 Spring 为您提供了许多不同的、更方便的方法来实现相同的目的。
如何使用 Spring 的程序化事务管理?
在 Spring 中定义事务的第一种但很少使用的方法是通过编程方式:通过 TransactionTemplate 或直接通过 PlatformTransactionManager。从代码角度来看,它看起来像这样:
@Service
public class UserService {
@Autowired
private TransactionTemplate template;
public Long registerUser(User user) {
Long id = template.execute(status -> {
// execute some SQL that e.g.
// inserts the user into the db and returns the autogenerated id
return id;
});
}
}
与普通 JDBC 示例相比:
- 您不必自己搞乱打开或关闭数据库连接(try-finally)。相反,您使用事务回调。
- 您也不必捕获 SQLException,因为 Spring 会为您将这些异常转换为运行时异常。
- 并且您可以更好地融入 Spring 生态系统。 TransactionTemplate 将在内部使用 TransactionManager,它将使用数据源。所有这些都是您必须在 Spring 上下文配置中指定的 bean,但以后不必再担心。
虽然这算是一个小小的改进,但程序化事务管理并不是 Spring 事务框架的主要功能。相反,这都是关于声明式事务管理的。让我们看看那是什么。
如何使用 Spring 的 XML 声明式事务管理?
过去,当 XML 配置成为 Spring 项目的标准时,您可以直接在 XML 中配置事务。除了一些遗留的企业项目之外,您将不再在野外找到这种方法,因为它已被更简单的 @Transactional 注释所取代。
我们不会在本指南中详细介绍 XML 配置,但您可以使用此示例作为深入研究的起点 - 如果需要的话(直接取自 Spring 官方文档):
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
您使用上面的 XML 块指定 AOP 建议(面向方面 的编程),然后可以将其应用到 UserService bean,如下所示:
<aop:config>
<aop:pointcut id="userServiceOperation" expression="execution(* x.y.service.UserService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="userServiceOperation"/>
</aop:config>
<bean id="userService" class="x.y.service.UserService"/>
您的 UserService bean 将如下所示:
public class UserService {
public Long registerUser(User user) {
// execute some SQL that e.g.
// inserts the user into the db and retrieves the autogenerated id
return id;
}
}
从 Java 代码的角度来看,这种声明式事务方法看起来比编程方法简单得多。但它会导致大量复杂、冗长的 XML,以及切入点和顾问程序配置。
因此,这就引出了一个问题:是否有比 XML 更好的声明式事务管理方法?是的,有:@Transactional 注释。
如何使用 Spring 的@Transactional 注解(声明式事务管理)
现在让我们看看现代 Spring 事务管理通常是什么样子的:
public class UserService {
@Transactional
public Long registerUser(User user) {
// execute some SQL that e.g.
// inserts the user into the db and retrieves the autogenerated id
// userDao.save(user);
return id;
}
}
这怎么可能?不再需要 XML 配置,也不需要其他代码。相反,您现在需要做两件事:
- 确保您的 Spring 配置带有 @EnableTransactionManagement 注释(在 Spring Boot 中,这将自动为您完成)。
- 确保您在 Spring 配置中指定了事务管理器(无论如何您都需要这样做)。
- 然后 Spring 足够智能,可以透明地为您处理事务:您用 @Transactional 注释注释的任何 bean 的公共方法都将在数据库事务内执行(注意:存在一些陷阱)。
因此,要使 @Transactional 注释正常工作,您需要做的就是:
@Configuration
@EnableTransactionManagement
public class MySpringConfig {
@Bean
public PlatformTransactionManager txManager() {
return yourTxManager; // more on that later
}
}
现在,当我说 Spring 透明地为您处理事务时。那个的真实意义是什么?
借助 JDBC 事务示例中的知识,上面的 @Transactional UserService 代码可以直接转换(简化)为:
public class UserService {
public Long registerUser(User user) {
Connection connection = dataSource.getConnection(); // (1)
try (connection) {
connection.setAutoCommit(false); // (1)
// execute some SQL that e.g.
// inserts the user into the db and retrieves the autogenerated id
// userDao.save(user); <(2)
connection.commit(); // (1)
} catch (SQLException e) {
connection.rollback(); // (1)
}
}
}
- 这只是 JDBC 连接的标准打开和关闭。这就是 Spring 的事务注释自动为您做的事情,而无需您显式地编写它。
- 这是您自己的代码,通过 DAO 或类似的东西保存用户。
这个例子可能看起来有点神奇,但是让我们看看 Spring 如何为您插入这个连接代码。
CGlib 和 JDK 代理 - @Transactional 的幕后黑手
Spring 无法真正重写您的 Java 类,就像我上面所做的那样,以插入连接代码(除非您使用字节码编织等高级技术,但我们现在忽略它)。
你的 registerUser() 方法实际上只是调用 userDao.save(user) ,没有办法动态改变它。
但 Spring 有一个优势。它的核心是一个 IoC 容器。它为您实例化一个 UserService 并确保将该 UserService 自动装配到任何其他需要 UserService 的 bean 中。
现在,每当您在 bean 上使用 @Transactional 时,Spring 都会使用一个小技巧。它不仅实例化 UserService,而且还实例化该 UserService 的事务代理。
它在 Cglib 库的帮助下通过一种称为代理通过子类化的方法来实现这一点。还有其他构建代理的方法(例如动态 JDK 代理),但我们暂时就这样吧。
让我们看看这张图中代理的作用:
从该图中可以看出,代理有一项工作。
- 打开和关闭数据库连接/事务。
- 然后委托给真正的 UserService,即您编写的那个。
- 而其他 bean,例如 UserRestController,永远不会知道它们正在与代理通信,而不是与真实的事物通信。
快速考试
看一下下面的源代码,告诉我 Spring 自动构造什么类型的 UserService,假设它带有 @Transactional 标记或具有 @Transactional 方法。
@Configuration
@EnableTransactionManagement
public static class MyAppConfig {
@Bean
public UserService userService() { // (1)
return new UserService();
}
}
- 正确的。 Spring 在这里为您的 UserService 类构造一个动态 CGLib 代理,它可以为您打开和关闭数据库事务。您或任何其他 bean 甚至不会注意到它不是您的 UserService,而是包装您的 UserService 的代理。
为什么需要事务管理器(如 PlatformTransactionManager)?
现在只缺少一条关键信息,尽管我们已经提到过几次了。
您的 UserService 会动态被代理,并且代理会为您管理事务。但处理所有事务状态(打开、提交、关闭)的不是代理本身,而是代理委托给事务管理器。
Spring 为您提供了 PlatformTransactionManager / TransactionManager 接口,默认情况下,它附带了一些方便的实现。其中之一是数据源事务管理器。
它所做的正是您迄今为止所做的管理事务的操作,但首先,让我们看看所需的 Spring 配置:
@Bean
public DataSource dataSource() {
return new MysqlDataSource(); // (1)
}
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource()); // (2)
}
- 您可以在此处创建特定于数据库或特定于连接池的数据源。本示例使用 MySQL。
- 在这里,您创建事务管理器,它需要一个数据源才能管理事务。
简单如。然后,所有事务管理器都有像“doBegin”(用于启动事务)或“doCommit”这样的方法,它们看起来像这样——直接取自 Spring 的源代码并进行了一些简化:
public class DataSourceTransactionManager implements PlatformTransactionManager {
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
Connection newCon = obtainDataSource().getConnection();
// ...
con.setAutoCommit(false);
// yes, that's it!
}
@Override
protected void doCommit(DefaultTransactionStatus status) {
// ...
Connection connection = status.getTransaction().getConnectionHolder().getConnection();
try {
con.commit();
} catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
}
因此,数据源事务管理器在管理事务时使用与您在 JDBC 部分中看到的完全相同的代码。
考虑到这一点,让我们从上面扩展我们的图片:
总结一下:
- 如果 Spring 检测到 bean 上的 @Transactional 注释,它会创建该 bean 的动态代理。
- 代理可以访问事务管理器,并要求它打开和关闭事务/连接。
- 事务管理器本身将简单地执行您在普通 Java 部分中所做的操作:管理良好的旧 JDBC 连接。
物理事务和逻辑事务有什么区别?
想象一下以下两个事务类。
@Service
public class UserService {
@Autowired
private InvoiceService invoiceService;
@Transactional
public void invoice() {
invoiceService.createPdf();
// send invoice as email, etc.
}
}
@Service
public class InvoiceService {
@Transactional
public void createPdf() {
// ...
}
}
UserService 有一个事务性 Invoice() 方法。它调用 InvoiceService 上的另一个事务方法 createPdf()。
现在就数据库事务而言,这实际上应该只是一个数据库事务。 (记住:getConnection().setAutocommit(false).commit()。)Spring 调用此物理事务,尽管一开始这听起来有点令人困惑。
然而,从 Spring 的角度来看,发生了两个逻辑事务:第一个在 UserService 中,另一个在 InvoiceService 中。 Spring 必须足够聪明,知道这两个 @Transactional 方法应该使用相同的底层物理数据库事务。
如果对 InvoiceService 进行以下更改,情况会有什么不同?
@Service
public class InvoiceService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
将传播模式更改为 require_new 是告诉 Spring createPDF() 需要在自己的事务中执行,独立于任何其他已存在的事务。回想一下本指南的纯 Java 部分,您是否看到了将事务“拆分”为两半的方法?我也不。
这基本上意味着您的代码将打开两个到数据库的(物理)连接/事务。 (再次:getConnection() x2.setAutocommit(false) x2.commit() x2)Spring 现在必须足够智能,两个逻辑事务部分 (invoice()/createPdf()) 现在也映射到两个不同的物理数据库交易。
所以,总结一下:
- 物理事务:是您实际的 JDBC 事务。
- 逻辑事务:是(可能嵌套的)@Transactional 注解的(Spring)方法。
这使我们能够更详细地介绍传播模式。
@Transactional 传播级别的用途是什么?
查看 Spring 源代码时,您会发现可以插入 @Transactional 方法的各种传播级别或模式。
@Transactional(propagation = Propagation.REQUIRED)
// or
@Transactional(propagation = Propagation.REQUIRES_NEW)
// etc
完整列表:
- REQUIRED 必需的
- SUPPORTS 支持
- MANDATORY 强制的
- REQUIRES_NEW REQUIRES_NEW
- NOT_SUPPORTED 不支持
- NEVER 绝不
- NESTED 嵌套
练习:
在普通 Java 部分中,我向您展示了 JDBC 在事务方面可以执行的所有操作。花点时间思考一下每个 Spring 传播模式最终对您的数据源(或者更确切地说,您的 JDBC 连接)到底做了什么。
那就看看下面的回答吧。
答案:
- Required (default):我的方法需要一个事务,要么为我打开一个事务,要么使用现有的事务 → getConnection()。设置自动提交(假)。犯罪()。
- Supports:我并不关心事务是否打开,我可以以任何一种方式工作 → 与 JDBC 无关
- Mandatory:我自己不会打开一个事务,但是如果没有人打开一个事务我会哭 → 与 JDBC 无关
- Require_new:我想要完全自己的事务 → getConnection()。设置自动提交(假)。犯罪()。
- Not_Supported:我真的不喜欢事务,我什至会尝试挂起当前正在运行的事务 → 与 JDBC 无关
- **Never:**如果其他人启动事务我会哭 → 与 JDBC 无关
- Nested: 听起来很复杂,但我们只是在谈论保存点! → 连接.setSavepoint()
正如您所看到的,大多数传播模式实际上与数据库或 JDBC 无关,而更多地与您如何使用 Spring 构建程序以及 Spring 期望事务出现的方式/时间/地点有关。
看这个例子:
public class UserService {
@Transactional(propagation = Propagation.MANDATORY)
public void myMethod() {
// execute some sql
}
}
在这种情况下,每当您调用 UserService 类的 myMethod() 时,Spring 都会期望打开一个事务。它本身不会打开一个方法,相反,如果您在没有预先存在的事务的情况下调用该方法,Spring 将抛出异常。请记住这一点,作为“逻辑事务处理”的附加点。
@Transactional 隔离级别有什么用?
此时这几乎是一个棘手的问题,但是当您像这样配置 @Transactional 注释时会发生什么?
@Transactional(isolation = Isolation.REPEATABLE_READ)
是的,它确实会导致这样的结果:
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
然而,数据库隔离级别是一个复杂的主题,您应该花一些时间来完全掌握它们。一个好的开始是 Postgres 官方文档及其有关隔离级别的部分。
另请注意,在事务期间切换隔离级别时,您必须确保咨询 JDBC 驱动程序/数据库以了解支持哪些场景,哪些不支持。
最常见的@Transactional 陷阱
Spring 初学者通常会遇到一个陷阱。看一下下面的代码:
@Service
public class UserService {
@Transactional
public void invoice() {
createPdf();
// send invoice as email, etc.
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
您有一个带有事务发票方法的 UserService 类。它调用 createPDF(),这也是事务性的。
一旦有人调用 invoice(),您期望打开多少实际交易?
不,答案不是两个,而是一个。为什么?
让我们回到本指南的代理部分。 Spring 为您创建事务性 UserService 代理,但是一旦您进入 UserService 类并调用其他内部方法,就不再涉及代理。这意味着,您没有新的交易。
我们用一张图来看看:
您可以使用一些技巧(例如自注入)来绕过此限制。但主要的要点是:始终牢记代理事务边界。
如何在 Spring Boot 或 Spring MVC 中使用 @Transactional
到目前为止,我们只讨论了简单的、核心的 Spring。但是 Spring Boot 呢?还是 Spring Web MVC?他们处理交易的方式有什么不同吗?
最简洁的答案是不。
对于任一框架(或者更确切地说:Spring 生态系统中的所有框架),您将始终使用 *@Transactional*
注释,并结合事务管理器和 @EnableTransactionManagement 注释。没有其他办法。
然而,与 Spring Boot 的唯一区别是,它使用 JDBC 自动配置自动设置 *@EnableTransactionManagement*
注释并为您创建 *PlatformTransactionManager*
。在此处了解有关自动配置的更多信息。
Spring 如何处理回滚(以及默认回滚策略)
有关 Spring 回滚的部分将在本指南的下一版本中处理。
Spring 和 JPA/Hibernate 事务管理如何工作
目标:同步 Spring 的 @Transactional 和 Hibernate / JPA
在某些时候,您会希望 Spring 应用程序与另一个数据库库集成,例如 Hibernate(一种流行的 JPA 实现)或 Jooq 等。
让我们以普通 Hibernate 为例(注意:直接使用 Hibernate,或者通过 JPA 使用 Hibernate 并不重要)。
将之前的 UserService 重写为 Hibernate 如下所示:
public class UserService {
@Autowired
private SessionFactory sessionFactory; // (1)
public void registerUser(User user) {
Session session = sessionFactory.openSession(); // (2)
// lets open up a transaction. remember setAutocommit(false)!
session.beginTransaction();
// save == insert our objects
session.save(user);
// and commit it
session.getTransaction().commit();
// close the session == our jdbc connection
session.close();
}
}
- 这是一个普通的、旧的 Hibernate SessionFactory,是所有 Hibernate 查询的入口点。
- 使用 Hibernate API 手动管理会话(即:数据库连接)和事务。
然而,上面的代码有一个很大的问题:
- Hibernate 不会知道 Spring 的 @Transactional 注释。
- Spring 的 @Transactional 对 Hibernate 的事务一无所知。
但我们实际上希望 Spring 和 Hibernate 能够无缝集成,这意味着它们了解彼此的事务。
用纯代码表示:
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory; // (1)
@Transactional
public void registerUser(User user) {
sessionFactory.getCurrentSession().save(user); // (2)
}
}
- 与之前相同的 SessionFactory
- 但不再需要手动状态管理。相反, getCurrentSession() 和 @Transactional 是同步的。
到那里怎么走?
使用 HibernateTransactionManager
对于这个集成问题有一个非常简单的解决方法:
您将使用 HibernateTransactionManager(如果使用普通 Hibernate)或 JpaTransactionManager(如果通过 JPA 使用 Hibernate),而不是在 Spring 配置中使用 DataSourcePlatformTransactionManager。
专门的 HibernateTransactionManager 将确保:
- 通过 Hibernate(即 SessionFactory)管理事务。
- 足够聪明,允许 Spring 在非 Hibernate 中使用相同的事务,即 @Transactional Spring 代码。
与往常一样,图片可能更容易理解(但请注意,代理和实际服务之间的流程仅在概念上正确且过于简单)。
简而言之,就是如何集成 Spring 和 Hibernate。
对于其他集成或更深入的理解,快速查看 Spring 提供的所有可能的 PlatformTransactionManager 实现会有所帮助。
Fin
到目前为止,您应该对事务管理如何与 Spring 框架配合使用以及它如何应用于其他 Spring 库(如 Spring Boot 或 Spring WebMVC)有一个很好的概述。最大的收获应该是,最终使用哪个框架并不重要,这都与 JDBC 基础知识有关。
如果它们正确(记住:getConnection().setAutocommit(false).commit().),您将更容易理解稍后在复杂的企业应用程序中发生的情况。
谢谢阅读。
致谢
感谢 Andreas Eisele 对本指南早期版本的反馈。感谢 Ben Horsfield 提供了急需的 Javascript 片段来增强本指南。
原文链接:https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth