「译」Spring 事务管理:@Transactional 深度解析|by MarcoBehler.md
写在翻译前
原文链接为:Spring Transaction Management: @Transactional In-Depth | MarcoBehler
阅读建议: 原文没有配置可运行项目代码,译者根据理解补充了一份。 为了帮助理解和调试,建议下载 poc/spring-transaction 示例项目,边动手边学习。 示例项目使用 Embedded H2 数据库,通过 spring schema.sql 初始化数据结构,通过访问 http://localhost:8080/h2-console 管理界面,可进行基础的 CURD 查询,方便读者进行数据校验。 当然有经验的读者,也可以把数据源切换为 mysql,毕竟其 GUI 工具更加齐全。
为什么要翻译此文? 理由有三:
- 讲解透彻,作者专业功底扎实,读之直呼彩。
- 内容深入浅出,从原理到上层实践娓娓道来,作者自下向上解构一个特性的思维值得学习。
- 编排为启发式架构,全程由问题引出解答,由解答引出下一个问题,环环相扣。
⬇️⬇️⬇️ 话不在多,请看正文 ⬇️⬇️⬇️
你可以通过本文,对 @Transactional
注解在 Spring 事务管理中的运行机制,形成一个简明实用的理解。
唯一的阅读前提?你需要对数据库 ACID 原则有个大致了解,即数据库事务是什么,我们为什么需要它。此外,本文没有覆盖分布式事务和反应式事务(reactive transactions),尽管在 Spring 中下文提到的一些通用原则也是适用的。
简介
在本文中,你将学到 Spring 事务抽象框架的核心概念(黑人脸+问号?),同时会有很多示例代码帮助你理解
@Transactional
(声明式事务管理)vs 编程式事务管理- 物理 vs 逻辑 事务
- Spring
@Transactional
与 JPA / Hibernate 集成 - Spring
@Transactional
和 Spring Boot 或 Spring MVC 集成 - 回滚、代理、常见陷阱等
相对于 Spring 官方文档,本文不会让你迷失在 Spring 的上层概念里。 相反你会以不同寻常的路径来学习 Spring 事务管理。从底层开始,一层层向上。也就是说,你将从普通原始的 JDBC 事务 学起。
普通的 JDBC 事务是如何工作的
如果你对 JDBC 事务还没有透彻的了解,请不要想着忽略此章节。
如何 start, commit 或 rollback 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)
则相反:你可以基于此来开关事务。 - 提交执行事务...
- 或者,假如发生了意外,则回滚我们的变更。
这 4 行高度简化的代码,就是 Spring @Transactional
为你在背后做的所有事情。在下一章节中,你将会学到他们是如何工作的。在此之前,我们还有一丁点知识点要补充。
(快速入门:根据配置,类似 HikariCP 的连接池可以自动切换 autocommit 模式。但这是个高级话题了,不扯远)
如何使用 JDBC 隔离级别和保存点(savepoints)
如果你已经使用过 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 是如何在数据库连接上设置隔离级别的。是不是完全不像造火箭(Rocket Science)那样复杂?
- Spring 中的嵌套事务等价于 JDBC 中的保存点。如果你不知道什么是保存点,可以看下这个教程「译者注:参见配套项目
PlainOldJDBCSample
实现」。注意保存点特性支持依赖于你的 JDBC 驱动/数据库。
Spring 或 Spring Boot 的事务是如何工作的
既然现在你对 JDBC 事务有了基础的理解,让我们再去探究下纯粹的 Spring 核心 事务。这里所讲都可 1:1 适用于 Spring Boot 和 Sring MVC,但又做了一些补充。
到底什么是 Spring 事务管理或事务抽象框架(更加困惑的命名)?
记住,事务管理可简单理解为:Spring 如何 start, commit 或 rollback JDBC 事务?是不是听着和前文讲得很相似?
抓住重点:基于 JDBC 你只有一个方法(setAutocommit(false)
)来开启事务管理,Spring 提供了许多不同,但更方便的封装来做相同的事情。
如何使用 Spring 编程式事务管理?
最初,但现在很少使用方式,是在 Spring 通过编程定义事务:通过 TransactionTemplate
或者直接使用 PlatformTransactionManager
。代码示例如下「译者注:参见配套项目 BookingServcie
实现」:
@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),取而代之的是 Transaction Callbacks。
- 你也不再需要手动捕获
SQLExceptions
,Spring 将这些异常转换成了运行时异常。 - 还有,你的代码能更好的集成进 Spring 生态。
TransactionTemplate
内部会使用到TransactionManager
,后者会使用到某个数据源(data source)。这些都是需要你事先在Spring context
配置里定义的 Beans,定义完后你就不用操心了。
尽管这是一个不小的改进,但编程式事务管理并不是 Spring 事务框架主要关注的。相反,声明式事务管理才是重头戏。让我们一探究竟
如何使用 Spring 的 XML 声明式事务管理?
在过去,使用 XML 进行配置是 Spring 项目的标配,你可以在 XML 文件中直接配置事务。但现在,除了一些历史遗留,企业项目,你很难在日常开发中遇到此类用法,取而代之是更加简明的 @Transactional
注解。
我们不会在本文中深入讲解 XML 配置,但是如果你有兴趣的话,你可以通过这个示例作为深入研究的起点(示例直接摘录自 official Spring documentation)
<!-- 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 advice(面向切面编程),你可以通过如下配置应用到 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 代码的角度来看,这种声明式事务实现比编程式简单很多。但是,为了配置 pointcut
和 advisor
也衍生了很多复杂冗余的 XML。
如何使用 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-boot-starter-data-jdbc
可以自动帮你配置事务管理器」 - 之后,聪明的 Spring 能够为你处理事务了,这一切对于你来说是透明的:任何 Bean 上标注了
@Transactional
的公共方法,会在一个数据库事务中执行(注意:这里有一些坑)。
所以,为了让 @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)
}
}
}
- 这些都是标准的数据库连接开闭操作。Spring 事务管理为你自动做了这一切,你无需显式地编码。
- 这是你的业务代码,通过一个 DAO 保存用户等
这个示例看起来像魔术,让我们继续探究下 Spring 是如何为你自动插入这些连接代码的。
CGLIB & JDK 代理 - 在 @Transactional
之下
Spring 不能真的像我上面做的那样,去重写你的 Java 类来插入连接代码(除非你使用字节码增强等高级技术,在这我们暂时忽略它)
你的 registerUser()
方法还是只是调用了 userDao.save(user)
,这是无法实时改变的。
但是 Spring 有它的优势。在核心层,它有一个 IoC 容器。它实例化一个 UserService
单例并可自动注入到任何需要 UserService
的 Bean 中。
不管何时你在一个 Bean 上使用 @Transactional
,Spring 使用了个小伎俩。它不是直接实例化一个 UserService
原始对象,而不是一个 UserService
的事务代理对象。
借助 Cglib library 的能力,它可以使用子类继承代理(proxy-through-subclassing)的方式来实现。当然还有其他方式可以构造代理对象(例如 Dynamic JDK proxies 「译者注:这要求代理对象有相应接口类」),这里暂不做展开。
让我们通过一张图来了解代理的具体操作:
正如你在图中所看到的,这代理做了一件事:
- 开闭数据库连接和事务
- 然后代理到你所写的原始
UserService
对象 - 最后其他 Beans,像你的
UserRestController
永远不知道他们在和代理对象交互,而不是原始对象。
Quick Exam
看看下面的源代码,并告诉我 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
可以动态生成代理类,并且代理可以帮你管理事务。但是并不是代理类本身去处理事务状态(open,commit,close),而是委托给了事务管理器(transaction manager)。
Spring 提供了 PlatformTransactionManager
/ TransactionManager
接口定义,默认也提供了些实用的实现。其中一个是数据源事务管理器(datasource transaction manager)。
它与你到目前为止管理事务的操作完全相同,但是首先让我们看一下所需的Spring配置:
// 「译者注」:Spring Boot 也有自动配置见:
// org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
@Bean
public DataSource dataSource() {
return new MysqlDataSource(); // (1)
}
// 「译者注」:Spring Boot 也有自动配置见:
// org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
@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 的动态代理类。 - 代理类可以访问事务管理器,并要求其打开和关闭事务/连接。
- 事务管理器本身也就是简单执行你之前手动做的事:管理一个实用、传统的 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()
事务方法。
现在就数据库事务而言,这里只有 1 个数据库事务。(记住:_getConnection(),setAutocommit(false),commit() _)。Spring 称之为物理事务,可能一下子不解其意。
然而从 Spring 看来,这里有 2 个逻辑事务存在:第一个在 UserService
,另外一个在 InvoiceService
。Spring 足够智能知道让两个 @Transactional
标记的方法,在底层使用同一个物理数据库事务。
我们做了如下变更后,呈现会有什么不同?
@Service
public class InvoiceService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
更改事务传播模式为 requires_new
告诉 Spring:createPDF()
需要在它自己的、独立于其他任何已经在的事务里执行。会想下本文中之前的原始 Java 版本,你看到过有一种方式可以将事务一分为二的吗?我是没看到的。
这意味着你的底层代码会打开 2(物理)连接/事务 到数据库(再提:getConnection() x2,setAutocommit(false) x2,commit() x2 )。Spring 依旧能够机智的把 2 个 逻辑事务( invoice()/createPdf()
)映射到两个不同的物理数据库事务上。
因此,总结如下:
- 物理事务:实际上等价于 JDBC 事务
- 逻辑事务:Spring 中被
@Transactional
标记的方法(可能存在嵌套)
接下来,我们将深入了解下事务传播模式一些细节。
@Transactional
传播级别(Propagation Levels)有什么用?
当你去查阅 Spring 源码,你会发现有多种传播级别可以挂载到 @Transactional
方法上。
// 「译者注:这是 Spring 中的默认传播方式」
@Transactional(propagation = Propagation.REQUIRED)
// or
@Transactional(propagation = Propagation.REQUIRES_NEW)
// etc
完整列表如下:
- REQUIRED
- SUPPORTS
- MANDATORY
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- NESTED
练习: 在原始 Java 实现那节,我展示了 JDBC 能够对事务进行的所有操作。花几分钟思考下,每个 Spring 传播模式在 数据库或 JDBC 连接层面到底做了什么。
然后再看下下面的解答。
Answers:
- Required (default): 我的方法需要事务支持,使用现有的或为我新建一个 → getConnection(),setAutocommit(false),commit()。
- Supports: 我不在乎是否有事务打开,我都能正常工作 → 不在 JDBC 层面做任何事情
- Mandatory: 我不打算为自己打开一个事务,但如果没有人为我打开事务,我会大呼小叫 「译者注:我需要事务,但是个伸手党,需要别人为我打开,不然我会抛错」→ 不在 JDBC 层面做任何事情
- Require_new: 我需要一个独占的事务 → getConnection(),setAutocommit(false),commit()。
- Not_Supported: 我不喜欢事务,我甚至会挂起当前的运行事务 → 不在 JDBC 层面做任何事情
- Never: 如果有人为我开启了事务,我会大呼小叫 → 不在 JDBC 层面做任何事情
- Nested: 听起来有些复杂,但其实我们在谈保存点!→ connection.setSavepoint()
如你所见,大多数传播模式并没有在数据库或 JDBC 层面做什么事情。更多的是通过 Spring 来组织你的代码,告诉 Spring 如何/什么时候/哪里需要事务处理。
看下这个示例:
public class UserService {
@Transactional(propagation = Propagation.MANDATORY)
public void myMethod() {
// execute some sql
}
}
在示例中,任何时候你调用 UserService
的 myMethod()
方法,Spring 期望这里有一个打开的事务。它不会为自己开启,相反,在没有已开启事务的情况下调用方法,Spring 会抛出异常。请记住这 “逻辑事务处理”的补充知识点。
@Transactional
上隔离级别(Isolation Levels)代表什么?
这是个抖机灵的问题,但当你如下配置的时候,到底发生了什么:
@Transactional(isolation = Isolation.REPEATABLE_READ)
哈,这可以简单地等价于:
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
然而数据库事务隔离级别,是一个复杂的主题,你需要自己花些时间去掌握。Pstgres 的官方文档中的 isolation levels 章节,是个不错的入门文档。
再提一嘴,当你在一个事务中切换隔离级别的时候,你必须事先确认底层 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
类,事务方法 invoice
内部调用了事务方法 createPdf()
。
所以,当有人调用 invoice()
的时候,最终有个多少个物理事务被打开?
答案不是 2 个,而是 1 个,为什么呢?
让我们回到本文中代理那章节。Spring 为你创建了 UserService
代理类,但代理类内部的方法调用,是无法被代理的。也就是说,没有新的事务为你生成。
看下示例图:
这里有些技巧(例如:self-injection 「译者注:参见示例项目
InnerCallSercie
」),可以帮助你绕过该限制。但主要收获是:始终牢记代理事务的边界。
如何在 Spring Boot 或 Spring MVC 中使用 @Transactional
我们目前只是讨论了纯粹的核心 Spring 上的用法。那在 Spring Boot 或 Spring MVC 中会有什么使用差异吗?
答案是:没有。
无论使用何种框架(或更确切地说:Spring 生态系统中的所有框架),您都将始终使用 @Transactional 注解,配合事务管理器,以及 @EnableTransactionManagement
注解。 没有其他用法了。
但是,与 Spring Boot 的唯一区别是,通过 JDBC 自动配置,它会自动设置 @EnableTransactionManagement
注解,并为你创建 PlatformTransactionManager
。
Spring 是如何处理回滚的(以及默认的回滚策略)
关于 Spring 回滚的部分,将会在下一次文章修订中补充。
「译者注:Spring Boot 内回滚是通过
@Transactional
注解上 rollback 系列配置实现的,读者可查阅源码注释了解使用方式,注释还是写得很完备的,本质上也是根据配置条件,确定何时调用 commit,何时调用 rollback」
Spring 和 JPA / Hibernate 事务管理是如何一起工作的
目标:同步 Spring @Transactional
和 Hibernate / JPA
在这个节点上,你期望 Spring 可以和其他数据库框架,类似 Hibernate(一个流行的 JPA 实现)或 Jooq 等整合。
让我来看一个纯粹的 Hibernate 示例(注意:直接使用 Hibernate 还是通过 JPA 使用 Hibernate 都没关系)。
用 Hibernate 将 UserService
重写如下:
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
有一个非常简单的解决此集成问题的方法:
相比在 Spring 配置里使用 DataSourcePlatformTransactionManager,你可以替换成 HibernateTransactionManager(如果使用了原生 Hibernate)或 JpaTransactionManager(如果通过 JPA 使用了 Hibernate)
这个定制化的 HibernateTransactionManager 会确保:
- 通过 Hibernate(即SessionFactory)管理事务。
- 足够智能允许 Spring 在非 Hibernate 中使用相同的事务注解,即
@Transactional
与往常一样,一图胜千言(不过请注意,代理和真实服务之间的流程在这被高度抽象和简化了)。
上诉是对 SPring 和 Hibernate 整合方式的简单概括。
在了解其他集成方式或打算进行深入了解之前,看一眼 Spring 提供的所有 PlatformTransactionManager 实现,会大有裨益。
最后
到目前为止,您应该对 Spring 框架是如何处理事务,以及如何应用于其他 Spring 类库(例如 Spring Boot 或 Spring WebMVC)有了一个很好的了解。 最大的收获应是:最终使用哪种框架都无关紧要,这一切可以映射到 JDBC 的基础概念上。
正确理解它们(记住:getConnection(),setAutocommit(false),commit() ),在以后碰到复杂的企业级项目的使用,你能更容易抓住本质。
谢谢阅读。
鸣谢
感谢 Andreas Eisele 对本指南早期版本的反馈。 感谢 Ben Horsfield 提供了急需的 Javascirpt 代码来提升本指南的阅读体验。
转载自:https://juejin.cn/post/6949753615070265357