理解 Spring 框架事务抽象 (Understanding the Spring Framework Transaction Abstraction)
Spring 事务抽象的关键在于事务策略的概念。事务策略由 TransactionManager 定义,具体而言,对于命令式事务管理,使用的是 org.springframework.transaction.PlatformTransactionManager 接口;对于响应式事务管理,使用的是 org.springframework.transaction.ReactiveTransactionManager 接口。
以下列表显示了 PlatformTransactionManager API 的定义:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}这主要是一个服务提供者接口 (SPI),尽管你也可以在应用程序代码中以编程式方式使用它。由于 PlatformTransactionManager 是一个接口,因此可以根据需要轻松地对其进行 mock 或 stub。它不绑定到像 JNDI 这样的查找策略。PlatformTransactionManager 的实现定义与 Spring 框架 IoC 容器中的任何其他对象(或 Bean)一样。仅凭这一优势,就使得使用 Spring 框架的事务抽象非常值得,即使你正在配合 JTA 工作。与直接使用 JTA 相比,你可以更轻松地对事务代码进行测试。
同样,秉承 Spring 的哲学,PlatformTransactionManager 接口的任何方法可能抛出的 TransactionException 都是非检查异常(即它继承自 java.lang.RuntimeException 类)。事务基础设施的失败几乎总是致命的。在极少数应用程序代码能够从事务失败中真正恢复的情况下,开发人员仍然可以选择捕获并处理 TransactionException。关键点在于,开发人员不会被强制这样做。
getTransaction(..) 方法根据 TransactionDefinition 参数返回一个 TransactionStatus 对象。返回的 TransactionStatus 可能代表一个新事务,也可能代表一个现有的事务(如果当前调用栈中存在匹配的事务)。在后一种情况下,与 Jakarta EE 事务上下文一样,TransactionStatus 与执行线程相关联。
Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下列表显示了由 org.springframework.transaction.ReactiveTransactionManager 定义的事务策略:
public interface ReactiveTransactionManager extends TransactionManager {
Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}响应式事务管理器主要是一个服务提供者接口 (SPI),尽管你也可以在应用程序代码中以编程式方式使用它。由于 ReactiveTransactionManager 是一个接口,因此可以根据需要轻松地对其进行 mock 或 stub。
TransactionDefinition 接口指定了:
- 传播行为 (Propagation):通常,事务范围内的所有代码都在该事务中运行。但是,你可以指定当事务方法在已存在事务上下文时运行的行为。例如,代码可以继续在现有事务中运行(常见情况),或者暂停现有事务并创建一个新事务。Spring 提供了 EJB CMT 中所有的事务传播选项。要了解有关 Spring 中事务传播语义的信息,请参阅事务传播。
- 隔离级别 (Isolation):此事务与其他事务工作的隔离程度。例如,此事务是否可以看到来自其他事务的未提交写入?
- 超时 (Timeout):此事务在运行多长时间后超时,并被底层事务基础设施自动回滚。
- 只读状态 (Read-only status):当你的代码读取但不修改数据时,可以使用只读事务。在某些情况下(例如使用 Hibernate 时),只读事务是一种非常有用的优化。
这些设置反映了标准的事务概念。如有必要,请参阅讨论事务隔离级别和其他核心事务概念的相关资源。理解这些概念对于使用 Spring 框架或任何事务管理解决方案都是必不可少的。
TransactionStatus 接口为事务代码提供了一种简单的方法来控制事务执行并查询事务状态。这些概念应该很熟悉,因为它们在所有事务 API 中都很常见。以下列表显示了 TransactionStatus 接口:
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
@Override
boolean isNewTransaction();
boolean hasSavepoint();
@Override
void setRollbackOnly();
@Override
boolean isRollbackOnly();
void flush();
@Override
boolean isCompleted();
}无论你在 Spring 中选择声明式事务管理还是编程式事务管理,定义正确的 TransactionManager 实现都是绝对必要的。你通常通过依赖注入来定义此实现。
TransactionManager 实现通常需要了解它们所工作的环境:JDBC、JTA、Hibernate 等。以下示例显示了如何定义本地 PlatformTransactionManager 实现(在本例中,使用纯 JDBC)。
你可以通过创建类似于以下内容的 Bean 来定义 JDBC DataSource:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>相关的 PlatformTransactionManager Bean 定义随后引用了 DataSource 定义。它应该类似于以下示例:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>如果你在 Jakarta EE 容器中使用 JTA,那么你会使用通过 JNDI 获取的容器 DataSource,并配合 Spring 的 JtaTransactionManager。以下示例显示了 JTA 和 JNDI 查找版本的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<!-- 此处为其他 <bean/> 定义 -->
</beans>JtaTransactionManager 不需要了解 DataSource(或任何其他特定资源),因为它使用的是容器的全局事务管理基础设施。
注意
前述 dataSource Bean 的定义使用了来自 jee 命名空间的 <jndi-lookup/> 标签。有关更多信息,请参阅 JEE Schema。
注意
如果你使用 JTA,无论你使用哪种数据访问技术(无论是 JDBC、Hibernate JPA 还是任何其他受支持的技术),你的事务管理器定义都应该看起来是一样的。这是因为 JTA 事务是全局事务,它可以登记任何事务资源。
在所有的 Spring 事务设置中,应用程序代码都不需要更改。你仅通过更改配置就可以更改事务的管理方式,即使这种更改意味着从本地事务移动到全局事务,反之亦然。
Hibernate 事务设置
你也可以轻松地使用 Hibernate 本地事务,如下例所示。在这种情况下,你需要定义一个 Hibernate LocalSessionFactoryBean,你的应用程序代码可以使用它来获取 Hibernate Session 实例。
DataSource Bean 的定义与前面显示的本地 JDBC 示例类似,因此在以下示例中不再重复显示。
注意
如果 DataSource(被任何非 JTA 事务管理器使用)是通过 JNDI 查找并由 Jakarta EE 容器管理的,那么它应该是非事务性的,因为由 Spring 框架(而不是 Jakarta EE 容器)来管理事务。
在这种情况下,txManager Bean 的类型是 HibernateTransactionManager。与 DataSourceTransactionManager 需要引用 DataSource 的方式相同,HibernateTransactionManager 需要引用 SessionFactory。以下示例声明了 sessionFactory 和 txManager Bean:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>如果你使用 Hibernate 和 Jakarta EE 容器管理的 JTA 事务,你应该使用与前面 JDBC 的 JTA 示例中相同的 JtaTransactionManager,如下例所示。此外,建议通过 Hibernate 的事务协调器(transaction coordinator)以及可能的连接释放模式配置让 Hibernate 感知 JTA:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
hibernate.transaction.coordinator_class=jta
hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>或者,你也可以将 JtaTransactionManager 传递到你的 LocalSessionFactoryBean 中,以强制执行相同的默认设置:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
<property name="jtaTransactionManager" ref="txManager"/>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>补充教学
1. SPI 的威力:为什么要将事务管理抽象化?
PlatformTransactionManager 本质上是一个 策略模式 (Strategy Pattern)。
- 统一性:它将“开始事务”、“提交”和“回滚”这些动作与具体的持久化技术(JDBC, Hibernate, Jta)解耦。
- 可测试性:正如原文所言,因为它是接口,你可以轻松 mock 一个
TransactionManager来测试业务逻辑,而不需要起一个真实的数据库。
2. 线程与上下文:命令式 vs 响应式
- 命令式 (Imperative):
PlatformTransactionManager依赖于ThreadLocal来存储事务上下文。这意味着事务是绑定到线程的。这也就是为什么在getTransaction时,它能知道当前线程是否已有正在进行的事务。 - 响应式 (Reactive):由于响应式编程(WebFlux, Project Reactor)中一个任务可能在不同线程间跳转,
ReactiveTransactionManager依赖的是 Reactor Context。事务信息随着流(Stream)传递,而不是绑定在静态的线程变量上。
3. 理解 TransactionDefinition 里的“只读”优化
很多人认为 read-only = true 只是一个暗示,但它在某些场景下有显著性能提升:
- Hibernate 优化:当 Session 被标记为只读时,Hibernate 会关闭 脏检查 (Dirty Checking)。这意味着它不会在事务结束时对比所有加载的对象是否有变化,大幅减少内存和 CPU 消耗。
- JDBC 层优化:有些数据库驱动在连接标记为
read-only时,会进行一些底层锁优化或路由到从库(Read Replicas)。
4. 为什么异常是 Unchecked 的?
Spring 的 TransactionException 继承自 RuntimeException。
- 逻辑理由:绝大多数事务异常(如数据库断开、死锁检测、资源耗尽)通常是不可恢复的。强制开发者写
try-catch除了增加代码噪声外,并没有实际意义。 - 代码简洁:相比 JTA 那个满屏都是
throws的复杂 API,Spring 的设计让业务逻辑看起来更清晰。
5. 关于 Hibernate 版本的说明
原文示例中使用的是 Hibernate 相关的类。需要注意的是,在 Spring 6+ 或 Spring Boot 3 中,推荐使用 JPA(JpaTransactionManager)作为首选,哪怕底层使用的是 Hibernate。因为 JPA 是标准,耦合度更低。