20220507 1. Data Access - Transaction Management

前言

文档地址

Spring 提供了全面的事务支持。Spring Framework 为事务管理提供了一致的抽象,具有以下优点:

  • 跨不同事务 API 的一致编程模型,例如 Java 事务 API ( JTA )、 JDBC 、Hibernate 和 Java Persistence API ( JPA )
  • 支持 声明式事务管理
  • 比复杂的事务 API(例如 JTA)更简单的用于 编程式 事务管理的 API
  • 与 Spring 的数据访问抽象完美集成

相关 Maven 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>


<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>

Spring Framework 的事务支持模型的优点

传统上,Java EE 开发人员对事务管理有两种选择:全局事务或本地事务,但是两者都有很大的局限性。

全局事务( Global Transactions )

全局事务允许使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTA 管理全局事务,这是一个繁琐的 API(部分原因是它的异常模型)。此外,JTA UserTransaction 通常需要来自 JNDI,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的重用的可能性,因为 JTA 通常仅在应用服务器环境中可用。

以前,使用全局事务的首选方法是通过 EJB CMT(容器管理事务,Container Managed Transaction)。CMT 是声明式事务管理的一种形式(区别于编程式事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管 EJB 本身的使用需要使用 JNDI 。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。显著的缺点是 CMT 与 JTA 和应用服务器环境相关联。此外,只有在选择在 EJB 中(或至少在事务性 EJB 外观之后)实现业务逻辑时,它才可用。总体来说,EJB 的缺点很大,以至于这不是一个有吸引力的提议,尤其是面对声明式事务管理的令人信服的替代方案时。

本地事务( Local Transactions )

本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更易于使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行。因为应用服务器不参与事务管理,所以它不能帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源)

另一个缺点是本地事务对编程模型具有侵入性。

Spring Framework 的一致性编程模型

Spring 解决了全局事务和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。您编写一次代码,它可以从不同环境中的不同事务管理策略中受益。

Spring Framework 提供声明式和编程式事务管理。大多数用户更喜欢声明式事务管理,在大多数情况下推荐使用它。

通过编程式事务管理,开发人员可以使用 Spring Framework 事务抽象,它可以在任何底层事务基础架构上运行。首选使用声明式模型,开发人员通常很少编写或不编写与事务管理相关的代码,因此不依赖于 Spring Framework 事务 API 或任何其他事务 API。

理解 Spring 框架事务抽象

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 是一个接口,所以可以根据需要轻松调用。它与查找策略(例如 JNDI)无关。 PlatformTransactionManager 的定义与 Spring Framework IoC 容器中的任何其他对象一样。即使在使用 JTA 时,仅此好处就使 Spring Framework 事务成为有价值的抽象。与直接使用 JTA 相比,您可以更轻松地测试事务代码。

同样,按照 Spring 的理念,可以由 PlatformTransactionManager 接口的任何方法抛出的 TransactionException 是非检查的(扩展 java.lang.RuntimeException 类)。事务基础设施失败几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理 TransactionException 。 重点是开发人员并没有被迫这样做。

getTransaction(..) 方法根据 TransactionDefinition 参数返回一个 TransactionStatus 对象 。如果当前调用堆栈中存在匹配的事务,则返回的 TransactionStatus 可能表示一个新事务,或者可以表示一个现有事务。后一种情况的含义是,与 Java EE 事务上下文一样,TransactionStatus 与执行线程相关联。

从 Spring Framework 5.2 开始,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;
}

TransactionDefinition 接口指定:

  • 传播行为(Propagation) :通常,事务作用域内的所有代码都在该事务中运行。但是,如果在事务上下文已经存在时运行事务方法,您可以指定事务传播行为。例如,代码可以在现有事务中继续运行(常见情况),或者可以暂停现有事务并创建新事务。Spring 提供了 EJB CMT 中熟悉的所有事务传播选项。要了解 Spring 中事务传播的语义,请参阅事务传播
  • 隔离级别(Isolation) :此事务与其他事务的工作隔离的程度。例如,这个事务可以看到来自其他事务的未提交的写入吗?
  • 超时(Timeout) :此事务在超时并被底层事务基础设施自动回滚之前运行的时间。
  • 只读状态(Read-only status) :当您的代码读取但不修改数据时,您可以使用只读事务。在某些情况下,只读事务可能是一种有用的优化,例如当您使用 Hibernate 时。

这些设置反映了标准的事务概念。

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>

如果您在 Java EE 容器中使用 JTA,那么您可以使用通过 JNDI 获得的容器 DataSource 与 Spring 的 JtaTransactionManager 。以下示例:

<?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" />

    <!-- other <bean/> definitions here -->

</beans>

JtaTransactionManager 并不需要了解 DataSource(或任何其他特定资源),因为它使用容器的全局事务管理。

如果您使用 JTA ,那么您的事务管理器定义应该看起来相同,无论您使用什么数据访问技术,无论是 JDBC、Hibernate JPA 还是任何其他受支持的技术。这是因为 JTA 事务是全局事务,可以征用任何事务资源。

在所有 Spring 事务设置中,应用程序代码不需要更改。您可以仅通过更改配置来更改事务的管理方式,即使该更改意味着从本地事务移动到全局事务,反之亦然。

Hibernate 事务配置

使用 Hibernate 本地事务需要定义一个 LocalSessionFactoryBean ,您的应用程序代码可以使用它来获取 Hibernate Session 实例。

如果 DataSource(由任何非 JTA 事务管理器使用)通过 JNDI 查找并由 Java EE 容器管理,则它应该是非事务性的,因为 Spring Framework(而不是 Java EE 容器)管理事务。

下例中,txManager bean 属于 HibernateTransactionManager 类型。与 DataSourceTransactionManager 需要对 DataSource 的引用相同, HibernateTransactionManager 需要对 SessionFactory 的引用。以下示例声明 sessionFactorytxManager 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 和 Java EE 容器管理的 JTA 事务,您应该使用与前面 JDBC 的 JTA 示例相同的 JtaTransactionManager ,如以下示例所示。此外,建议 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"/>

使用事务同步资源

本节描述应用程序代码(直接或间接,通过使用持久性 API,如 JDBC、Hibernate 或 JPA)如何确保正确创建、重用和清理这些资源。

高级同步方法

首选方法是使用 Spring 的最高级别基于模板的持久层集成 API,或者使用带有事务感知工厂 bean 或代理的原生 ORM API 来管理原生资源工厂。这些事务感知解决方案在内部处理资源创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以完全专注于非样板持久层逻辑。

通常,您使用本地 ORM API ,或者通过使用 JdbcTemplate 采用模板方法进行 JDBC 访问。

底层同步方法

诸如 DataSourceUtils(对于 JDBC )、EntityManagerFactoryUtils(对于 JPA )、 SessionFactoryUtils(对于 Hibernate )等类存在于底层。当您希望应用程序代码直接处理原生持久化 API 的资源类型时,您可以使用这些类来确保获得适当的 Spring Framework 管理的实例,事务同步(可选),并且过程中发生的异常正确映射到一致的 API。

例如,在 JDBC 的情况下,您可以使用 Spring 的 org.springframework.jdbc.datasource.DataSourceUtils 类代替传统的 JDBC 方式在 DataSource 上调用 getConnection() 方法,如下所示:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果现有事务已经有一个与之同步(链接)的连接,则返回该实例。否则,该方法调用会触发新连接的创建,该连接同步到任何现有事务(可选),并可供后续在同一事务中重用。如前所述, SQLException 被包装在 Spring Framework CannotGetJdbcConnectionException 中,Spring Framework 的非检查异常 DataAccessException 类型层次结构之一。这种方法为您提供了比从 SQLException 中获取的更多信息,并确保了跨数据库甚至跨不同持久层技术的可移植性。

这种方法也可以在没有 Spring 事务管理的情况下工作(事务同步是可选的),因此无论您是否使用 Spring 进行事务管理,都可以使用它。

当然,一旦您使用了 Spring 的 JDBC 支持、JPA 支持或 Hibernate 支持,您通常不喜欢使用 DataSourceUtils 或其他辅助类,因为通过 Spring 抽象比直接使用相关 API 更轻松。例如,如果您使用 Spring JdbcTemplatejdbc.object 包来简化您对 JDBC 的使用,则正确的连接检索发生在幕后,您无需编写任何特殊代码。

TransactionAwareDataSourceProxy

在最底层存在 TransactionAwareDataSourceProxy 类。这是一个目标 DataSource 的代理,它包装了目标 DataSource 以增加对 Spring 管理事务的感知。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource

您几乎不需要使用此类,除非必须调用现有代码并传递标准 JDBC DataSource 接口实现。在这种情况下,此代码可能可用但正在参与 Spring 管理的事务。您可以使用前面提到的更高级别的抽象来编写新代码。

声明式事务管理

大多数 Spring Framework 用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理想。

Spring Framework 的声明式事务管理通过 Spring 面向切面编程 (AOP) 实现。

Spring Framework 的声明式事务管理类似于 EJB CMT,因为您可以将事务行为指定到单个方法级别。如有必要,您可以在事务上下文中进行 setRollbackOnly() 调用。两种类型的事务管理之间的区别是:

  • 与绑定到 JTA 的 EJB CMT 不同,Spring Framework 的声明式事务管理可在任何环境中工作。它可以通过调整配置文件使用 JDBC、JPA 或 Hibernate 来处理 JTA 事务或本地事务
  • 您可以将 Spring Framework 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。
  • Spring Framework 提供了声明式 回滚规则,这是一个没有 EJB 等效项的特性。提供了对回滚规则的编程和声明性支持
  • Spring Framework 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意通知以及事务通知。使用 EJB CMT,您不能影响容器的事务管理,除非使用 setRollbackOnly()
  • Spring Framework 不支持跨远程调用传播事务上下文,而高端应用服务器支持。如果您需要此功能,我们建议您使用 EJB 。但是,在使用此类功能之前请仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。

回滚规则的概念很重要。它们让您指定哪些异常应该导致自动回滚。您可以在配置中(而不是在 Java 代码中)以声明方式指定此项。所以,尽管你仍然可以调用 TransactionStatus 对象上的 setRollbackOnly() 方法回滚当前事务,但通常可以指定 MyApplicationException 异常导致回滚的规则。此选项的显著优点是业务对象不依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API 。

尽管 EJB 容器默认行为会在发生系统异常(通常是运行时异常)时自动回滚事务,但 EJB CMT 不会在发生应用程序异常(即除 java.rmi.RemoteException 之外的检查异常)时自动回滚事务。虽然声明性事务管理的 Spring 默认行为遵循 EJB 约定(仅在非检查异常时自动回滚),但自定义此行为通常很有用。

理解 Spring 框架的声明式事务实现

仅仅告诉您使用 @Transactional 注解对类进行注解、添加 @EnableTransactionManagement 到您的配置中是不够的 。

关于 Spring Framework 的声明式事务支持,需要掌握的最重要的概念是这种支持是通过 AOP 代理启用的 ,并且事务通知是由元数据(目前基于 XML 或注解)驱动的。AOP 与事务元数据的组合产生了一个 AOP 代理,该代理使用 TransactionInterceptor 结合适当的 TransactionManager 实现来驱动围绕方法调用的事务。

Spring Framework 的 TransactionInterceptor 为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回反应式类型的方法,例如 Publisher 或 Kotlin Flow(或它们的子类型),进行反应式事务管理。所有其他返回类型包括 void 使用进行命令式事务管理。

事务管理风格影响所需的事务管理器。命令式事务需要 PlatformTransactionManager ,而反应式事务使用 ReactiveTransactionManager 实现。

@Transactional 通常与由 PlatformTransactionManager 管理的线程绑定事务一起使用 ,将事务暴露给当前执行线程内的所有数据访问操作。注意:这不会传播到方法中新启动的线程。

ReactiveTransactionManager 管理的反应式事务使用 Reactor 上下文而不是 thread-local 属性。因此,所有参与的数据访问操作都需要在同一个反应管道中的同一个 Reactor 上下文中执行。

下图显示了在事务代理上调用方法的概念视图:

发送

  • org.springframework.transaction.annotation.Transactional
  • org.springframework.transaction.annotation.EnableTransactionManagement
  • org.springframework.transaction.interceptor.TransactionInterceptor
  • org.springframework.transaction.TransactionManager

声明式事务实现示例

就本示例而言,DefaultFooService 类在每个实现的方法中抛出 UnsupportedOperationException 很好。该行为让您可以看到正在创建的事务,然后回滚以响应 UnsupportedOperationException 实例。以下清单显示了 FooService 接口 :

// the service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}

以下示例显示了上述接口的实现:

package x.y.service;

public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}

假设 FooService 接口的前两个方法 getFoo(String)getFoo(String, String) 必须在具有只读语义的事务上下文中运行,而其他方法 insertFoo(Foo)updateFoo(Foo) 必须在具有读写语义的事务上下文中运行。下面几段详细解释下面的配置:

<!-- from the file 'context.xml' -->
<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

        <!-- similarly, don't forget the TransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 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>

    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- don't forget the DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

</beans>

检查前面的配置。它假设您想要创建一个事务处理的服务对象 fooService bean 。要应用的事务语义封装在 <tx:advice/> 定义中。<tx:advice/> 定义读作 “所有以 get 开头的方法都将在只读事务的上下文中运行,而所有其他方法将使用默认事务语义运行” 。<tx:advice/> 标签的 transaction-manager 属性设置为将驱动事务的 TransactionManager bean 的名称。

如果 TransactionManager bean 的名字是 transactionManager ,可以省略事务通知( <tx:advice/> )的 transaction-manager 属性的。如果 TransactionManager bean 具有任何其他名称,则必须显式使用 transaction-manager 属性。

<aop:config/> 定义确保 txAdvice bean 定义的事务通知在程序中的适当位置运行。首先,您定义一个切点,该切点与 FooService 接口 ( fooServiceOperation ) 中定义的任何操作的执行相匹配。然后使用顾问将切点与 txAdvice 关联。结果表明,在执行 fooServiceOperation 时,会运行由 txAdvice 定义的通知。

一个常见的需求是使整个服务层具有事务性。最好的方法是更改切点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

前面显示的配置用于围绕 fooService bean 定义创建的对象创建事务代理。代理配置有事务通知,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、暂停、标记为只读等事务。考虑以下驱动前面配置的测试程序:

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
        FooService fooService = ctx.getBean(FooService.class);
        fooService.insertFoo(new Foo());
    }
}

运行上述程序的输出应类似于以下内容(为清晰起见,DefaultFooService 类的 insertFoo(..) 方法抛出 UnsupportedOperationException 的 Log4J 输出和堆栈跟踪已被截断):

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

要使用反应式事务管理,代码必须使用反应式类型。

Spring Framework 使用 ReactiveAdapterRegistry 来确定方法返回类型是否为响应式。

以下清单显示了之前使用的 FooService 修改版本 ,但这次代码使用了反应式类型:

// the reactive service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Flux<Foo> getFoo(String fooName);

    Publisher<Foo> getFoo(String fooName, String barName);

    Mono<Void> insertFoo(Foo foo);

    Mono<Void> updateFoo(Foo foo);

}

上述接口的实现:

package x.y.service;

public class DefaultFooService implements FooService {

    @Override
    public Flux<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Publisher<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}

命令式和反应式事务管理共享相同的事务边界和事务属性定义语义。命令式事务和反应式事务之间的主要区别在于后者的延迟性质。TransactionInterceptor 用事务运算符装饰返回的反应类型以开始和清理事务。因此,调用事务反应方法将实际事务管理推迟到激活反应类型处理的订阅类型。

反应式事务管理的另一方面涉及数据转义,这是编程模型的自然结果。

命令式事务的方法返回值在方法成功终止时从事务方法返回,因此部分计算的结果不会跳出方法闭包。

反应式事务方法返回反应式包装器类型,它表示一个计算序列以及开始和完成计算的承诺。

Publisher 可以在事务正在进行但未必完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保完成并在调用代码中缓冲结果。

回滚声明式事务

向 Spring 框架的事务基础设施指示要回滚事务的推荐方法是从当前在事务上下文中执行的代码中抛出 Exception 。Spring 框架的事务基础结构代码捕获任何未处理的 Exception ,因为它会弹出调用堆栈,并决定是否将事务标记为回滚。

默认配置中,Spring Framework 的事务基础设施代码仅在运行时异常(非检查异常)的情况下才将事务标记为回滚。也就是说,当抛出的异常是 RuntimeException ( 默认情况下,Error 也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置中的回滚。

您可以准确配置哪些 Exception 类型将事务标记为回滚,包括检查异常。以下 XML 片段演示了如何为检查 Exception 类型配置回滚:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

**如果您不希望在抛出异常时回滚事务,您还可以指定 no-rollback-for **。以下示例告诉 Spring Framework 的事务基础结构即使面对未处理的 InstrumentNotFoundException 也要提交事务:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

当 Spring Framework 的事务基础设施捕获异常并查询配置的回滚规则以确定是否将事务标记为回滚时,最强匹配规则获胜。因此,在以下配置的情况下,除 InstrumentNotFoundException 之外的任何异常都会导致伴随事务回滚:

<tx:advice id="txAdvice">
    <tx:attributes>
    	<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

您还可以以编程方式指示需要的回滚。尽管很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础设施紧密耦合。以下示例显示了如何以编程方式指示所需的回滚:

public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

如果可能,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用编程回滚,但它的使用与实现基于简介 POJO 的架构背道而驰。

为不同 Bean 配置不同的事务语义

考虑这样一个场景,您有多个服务层对象,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义不同的 <aop:advisor/> 引用不同的 pointcutadvice-ref 属性值。

作为比较,首先假设您的所有服务层类都定义在一个根 x.y.service 包中。要使作为该包(或子包)中定义的类的实例的所有 Service bean 具有默认事务配置,您可以编写以下内容:

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="serviceOperation"
                expression="execution(* x.y.service..*Service.*(..))"/>

        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>

    </aop:config>

    <!-- these two beans will be transactional... -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... and these two beans won't -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

以下示例显示如何使用完全不同的事务设置配置两个不同的 bean:

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="defaultServiceOperation"
                expression="execution(* x.y.service.*Service.*(..))"/>

        <aop:pointcut id="noTxServiceOperation"
                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>

        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>

        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>

    </aop:config>

    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this bean will also be transactional, but with totally different transactional settings -->
    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>

    <tx:advice id="defaultTxAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <tx:advice id="noTxAdvice">
        <tx:attributes>
            <tx:method name="*" propagation="NEVER"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

<tx:advice/> 设置

本节总结了您可以使用 <tx:advice/> 标签指定的各种事务设置。默认<tx:advice/> 设置为:

  • 事务传播行为 REQUIRED
  • 隔离级别 DEFAULT
  • 事务是读写的
  • 事务超时默认为底层事务系统的默认超时,如果不支持超时,则无
  • RuntimeException 会触发回滚,检查异常不会触发回滚

您可以更改这些默认设置。下表总结了嵌套在 <tx:advice/><tx:attributes/> 标签中的 <tx:method/> 标签的各种属性:

属性 是否必需 默认值 描述
name 与事务属性关联的方法名称。通配符 * 可用于将相同的事务属性设置与多种方法(例如,get*handle*on*Event 等)相关联
propagation REQUIRED 事务传播行为
isolation DEFAULT 事务隔离级别。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
timeout -1 事务超时(秒)。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
read-only false 读写与只读事务。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
rollback-for 触发回滚的以逗号分隔的 Exception 实例列表。例如, com.foo.MyBusinessException,ServletException
no-rollback-for 不触发回滚的以逗号分隔的 Exception 实例列表。例如, com.foo.MyBusinessException,ServletException

使用 @Transactional

除了用于事务配置的基于 XML 的声明性方法之外,您还可以使用基于注解的方法。直接在 Java 源代码中声明事务语义会使声明更接近受影响的代码。不存在过度耦合的危险,因为旨在以事务方式使用的代码几乎总是以这种方式部署的。

标准 javax.transaction.Transactional 注解也被支持作为 Spring 注解的替代品。

使用 @Transactional 注解提供的易用性最好用一个例子来说明:

// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}

如上在类级别使用,注解指示声明类(及其子类)的所有方法的默认值。或者,每个方法都可以单独注解。请注意,类级别的注解不适用于在类层次结构中向上的祖先类;在这种情况下,需要在本地重新声明继承的方法才能参与子类级别的注解。

当上面的 POJO 类在 Spring 上下文中定义为 bean 时,您可以通过 @Configuration 类中的 @EnableTransactionManagement 注解使 bean 实例具有事务性。

在 XML 配置中,<tx:annotation-driven/> 标签提供了类似的便利:

<!-- from the file 'context.xml' -->
<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- enable the configuration of transactional behavior based on annotations -->
    <!-- a TransactionManager is still required -->
    <tx:annotation-driven transaction-manager="txManager"/> 

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (this dependency is defined somewhere else) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>

反应式事务方法使用反应式返回类型,如下面的清单所示:

// the reactive service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Publisher<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Mono<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}

请注意,Publisher 对于 Reactive Streams 取消信号的返回有特殊考虑。

方法可见性和 @Transactional

当您在 Spring 的标准配置中使用事务代理时,您应该只将 @Transactional 注解应用于具有 public 可见性的方法。如果您对 protectedprivate 或包可见的方法进行 @Transactional 注解,则不会引发错误,但被注解的方法不会生效配置的事务设置。如果您需要注解非公共方法,请考虑以下段落中有关基于类的代理的提示,或考虑使用 AspectJ 编译时或加载时编织。

@Configuration 类中使用 @EnableTransactionManagement 时,也可以通过注册自定义 transactionAttributeSource bean 来使基于类的代理的 protected 或包可见方法成为事务性方法,如下例所示。但是请注意,基于接口的代理中的事务方法必须始终在代理接口中定义为 public

/**
 * Register a custom AnnotationTransactionAttributeSource with the
 * publicMethodsOnly flag set to false to enable support for
 * protected and package-private @Transactional methods in
 * class-based proxies.
 *
 * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
 */
@Bean
TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

Spring TestContext 框架 默认支持非 private@Transactional 测试方法。

您可以将 @Transactional 注解应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在 @Transactional 注解还不足以激活事务行为。@Transactional 注解仅仅是一些运行时基础设施可以使用的元数据,这些运行时基础设施具有 @Transactional 感知能力,并且可以使用元数据来配置具有事务行为的适当 bean 。在前面的示例中,<tx:annotation-driven/> 元素启用事务行为。

Spring 团队建议您只使用 @Transactional 注解来注解具体类(和具体类的方法),而不是注解接口。您当然可以将 @Transactional 注解放在接口(或接口方法)上,但是如果您使用基于接口的代理,这只能像您期望的那样工作。Java 注解不是从接口继承的事实意味着,如果您使用基于类的代理 ( proxy-target-class="true" ) 或基于编织的切面 ( mode="aspectj"),则代理和编织基础设施无法识别事务设置,并且对象不会包装在事务代理中。

在代理模式下(默认模式),只有通过代理进入的外部方法调用才会被拦截。这意味着自调用(目标对象中的一个方法调用目标对象的另一个方法)在运行时不会导致实际事务,即使被调用的方法被标记为 @Transactional 。此外,代理必须完全初始化以提供预期的行为,因此您不应在初始化代码中依赖此功能 - 例如,在 @PostConstruct 方法中。

如果您希望自调用也与事务一起包装,请考虑使用 AspectJ 模式(请参阅下表中的 mode 属性)。在这种情况下,首先没有代理。相反,目标类被编织(即,它的字节码被修改)以支持任何类型方法的 @Transactional 运行时行为。

注解驱动的事务设置

XML 属性 注解属性 默认 说明
transaction-manager N/A transactionManager 要使用的事务管理器的名称。仅当事务管理器的名称不是 transactionManager 时才需要
mode mode proxy 默认模式 ( proxy ) 使用 Spring 的 AOP 框架处理要代理的带注解的 bean(遵循代理语义,仅适用于通过代理传入的方法调用)。另一种模式 ( aspectj ) 不是将受影响的类与 Spring 的 AspectJ 事务切面编织在一起,而是修改目标类字节码以应用于任何类型的方法调用。AspectJ 编织需要 spring-aspects.jar 在类路径中以及启用加载时编织(或编译时编织)
proxy-target-class proxyTargetClass false 仅适用于 proxy 模式。控制为 @Transactional 注解的类创建什么类型的事务代理。如果 proxy-target-class 属性设置为 true ,则会创建基于类的代理。如果 proxy-target-classfalse 或属性被省略,则创建标准的基于 JDK 接口的代理
order order Ordered.LOWEST_PRECEDENCE 定义@Transactional 注解的 bean 的事务通知的顺序 。没有指定意味着 AOP 子系统决定通知的顺序

处理 @Transactional 注解的默认通知模式是 proxy ,它只允许通过代理拦截调用。不能以这种方式拦截同一类中的本地调用。对于更高级的拦截模式,请考虑切换到 aspectj 与编译时或加载时编织相结合的模式。

proxy-target-class 属性控制为使用 @Transactional 注解的类创建哪种类型的事务代理。如果 proxy-target-class 设置为 true ,则创建基于类的代理。如果 proxy-target-classfalse 或属性被省略,则创建标准的基于 JDK 接口的代理。

@EnableTransactionManagement<tx:annotation-driven/> 仅在定义它们的同一应用上下文中查找 @Transactional bean 。这意味着,如果您将注解驱动的配置放在针对 DispatcherServletWebApplicationContext 中,它只会在控制器中而不是在您的服务中检查 @Transactional bean 。有关更多信息,请参阅 MVC

在计算方法的事务设置时,派生最多的位置优先。在以下示例中,DefaultFooService 类在类级别使用只读事务的设置进行 @Transactional 注解,但同一类中 updateFoo(Foo) 方法的 @Transactional 注解优先于在类级别定义的事务设置。

@Transactional(readOnly = true)
public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        // ...
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // ...
    }
}
@Transactional 设置

@Transactional 注解指定接口,类或必须有事务语义方法的元数据(例如,“调用该方法时启动一个全新的只读事务,挂起已有的事务”)。默认 @Transactional 设置如下:

  • 传播行为 PROPAGATION_REQUIRED
  • 隔离级别 ISOLATION_DEFAULT
  • 事务是读写的
  • 事务超时默认为底层事务系统的默认超时,如果不支持超时,则为无
  • RuntimeException 会触发回滚,检查异常都不会回滚

您可以更改这些默认设置。下表总结了 @Transactional 注解的各种属性:

属性 类型 描述
value String 指定要使用的事务管理器的可选限定符
propagation enumPropagation 事务传播行为,可选
isolation enumIsolation 隔离级别,可选。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
timeout int (以秒为单位) 事务超时,可选。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
readOnly boolean 读写与只读事务。仅适用于 REQUIREDREQUIRES_NEW 的传播行为
rollbackFor Class 对象数组,必须派生自 Throwable 必须导致回滚的异常类的数组,可选
rollbackForClassName 类名数组。类必须派生自 Throwable 必须导致回滚的异常类名称的数组,可选
noRollbackFor Class 对象数组,必须派生自 Throwable 不得导致回滚的异常类的数组,可选
noRollbackForClassName String 类名数组,必须派生自 Throwable 不得导致回滚的异常类名称的数组,可选
label 用于向事务添加表达性描述的 String 标签数组 事务管理器可以对标签进行评估,以将特定于实现的行为与实际事务相关联

目前,您无法显式控制事务的名称,其中 “name” 表示出现在事务监视器(例如,WebLogic 的事务监视器)和日志输出中的事务名称。对于声明式事务,事务名称始终是 完全限定的类名称 + . + 事务通知类的方法名称 。例如,如果 BusinessService 类的 handlePayment(..) 方法启动了一个事务,则该事务的名称将是:com.example.BusinessService.handlePayment

存在多个事务管理器的情况下使用 @Transactional

大多数 Spring 应用程序只需要一个事务管理器,但在某些情况下,您可能需要在一个应用程序中使用多个独立的事务管理器。您可以使用 @Transactional 注解的 valuetransactionManager 属性来选择性地指定要使用的 TransactionManager 。这可以是事务管理器 bean 的名称或限定符值。例如,使用限定符表示法,您可以将以下 Java 代码与应用上下文中的以下事务管理器 bean 声明相结合:

public class TransactionalService {

    @Transactional("order")
    public void setSomething(String name) { ... }

    @Transactional("account")
    public void doSomething() { ... }

    @Transactional("reactive-account")
    public Mono<Void> doSomethingReactive() { ... }
}

以下清单显示了 bean 声明:

<tx:annotation-driven/>

<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    ...
    <qualifier value="order"/>
</bean>

<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    ...
    <qualifier value="account"/>
</bean>

<bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
    ...
    <qualifier value="reactive-account"/>
</bean>

这种情况下,TransactionalService 的各个方法在单独的事务管理器运行,通过差异化 orderaccountreactive-account 限定符区分。如果未找到特别限定的 TransactionManager bean ,则仍使用默认 <tx:annotation-driven> 目标 bean 名称 transactionManager

自定义组合注解

如果您发现 @Transactional 在许多不同的方法上重复使用相同的属性,Spring 的元注解支持允许您为特定用例定义自定义组合注解。例如,考虑以下注解定义:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}

前面的注解让我们将上一节中的示例编写如下:

public class TransactionalService {

    @OrderTx
    public void setSomething(String name) {
        // ...
    }

    @AccountTx
    public void doSomething() {
        // ...
    }
}

事务传播

本节介绍 Spring 中事务传播的一些语义。请注意,本节不是对事务传播的正确介绍。相反,它详细介绍了有关 Spring 中事务传播的一些语义。

在 Spring 管理的事务中,注意物理事务和逻辑事务之间的区别,以及传播行为设置如何应用于这种区别。

理解 PROPAGATION_REQUIRED

需要 tx 道具

PROPAGATION_REQUIRED 强制执行一个物理事务,如果还不存在事务,则针对当前作用域在本地执行物理事务,或者参与为更大作用域定义的现有“外部”事务。这是同一线程中常见调用堆栈安排中的一个很好的默认设置(例如,委托给多个操作数据库方法的服务门面,其中所有底层资源都必须参与服务级别事务)。

默认情况下,参与事务加入外部作用域的特征,静默忽略本地隔离级别、超时值或只读标志。如果您希望在参与具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑将事务管理器上的 validateExistingTransactions 标志切换为true。这种非宽松模式还拒绝只读不匹配(即尝试参与只读外部作用域的内部读写事务)。

当传播行为设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建一个逻辑事务作用域。每个这样的逻辑事务作用域可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些作用域都映射到同一个物理事务。因此,在内部事务作用域内设置的仅回滚标记确实会影响外部事务实际提交的机会。

但是,在内部事务作用域设置了仅回滚标记的情况下,外部事务本身并没有决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。此时抛出一个对应的 UnexpectedRollbackException 是预期的行为,因此事务的调用者永远不会被误导,认为提交已执行,而实际上并未执行。所以,如果一个内部事务(外部调用者不知道)默默地将一个事务标记为仅回滚,外部调用者仍然调用 commit 。外部调用者需要收到一个 UnexpectedRollbackException ,以清楚地表明执行了回滚。

理解 PROPAGATION_REQUIRES_NEW

tx 道具需要新的

PROPAGATION_REQUIRES_NEWPROPAGATION_REQUIRED 相比,始终为每个受影响的事务作用域使用独立的物理事务,从不参与外部作用域的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在完成后立即释放。这样一个独立的内部事务也可以声明自己的隔离级别、超时和只读设置,而不继承外部事务的特性。

理解 PROPAGATION_NESTED

PROPAGATION_NESTED 使用具有多个可以回滚的保存点的单个物理事务。这种部分回滚允许内部事务作用域触发其作用域的回滚,而外部事务能够继续物理事务,尽管某些操作已回滚。此设置通常映射到 JDBC 保存点,因此它仅适用于 JDBC 资源事务。请参阅 Spring 的 DataSourceTransactionManager

为事务操作提供通知

假设您想要同时运行事务操作和一些基本的分析通知。如何在 <tx:annotation-driven/> 上下文中实现这一点?

当您调用 updateFoo(Foo) 方法时,您希望看到以下操作:

  • 配置的分析切面启动
  • 事务通知运行
  • 通知的目标对象上的方法运行
  • 事务提交
  • 分析通知报告整个事务方法调用的确切持续时间

以下代码显示了前面讨论的简单分析切面:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;

public class SimpleProfiler implements Ordered {

    private int order;

    // allows us to control the ordering of advice
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    // this method is the around advice
    public Object profile(ProceedingJoinPoint call) throws Throwable {
        Object returnValue;
        StopWatch clock = new StopWatch(getClass().getName());
        try {
            clock.start(call.toShortString());
            returnValue = call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
        return returnValue;
    }
}

通知的顺序是通过 Ordered 接口控制的。

以下配置创建了一个 fooService bean,该 bean 以所需的顺序应用了分析和事务切面:

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the aspect -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <tx:annotation-driven transaction-manager="txManager" order="200"/>

    <aop:config>
            <!-- this advice runs around the transactional advice -->
            <aop:aspect id="profilingAspect" ref="profiler">
                <aop:pointcut id="serviceMethodWithReturnValue"
                        expression="execution(!void x.y..*Service.*(..))"/>
                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
            </aop:aspect>
    </aop:config>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

您可以以类似的方式配置任意数量的额外的切面。

以下示例创建与前两个示例相同的设置,但使用纯 XML 声明方法:

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the profiling advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <aop:config>
        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
        <!-- runs after the profiling advice (c.f. the order attribute) -->

        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
        <!-- order value is higher than the profiling aspect -->

        <aop:aspect id="profilingAspect" ref="profiler">
            <aop:pointcut id="serviceMethodWithReturnValue"
                    expression="execution(!void x.y..*Service.*(..))"/>
            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
        </aop:aspect>

    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other <bean/> definitions such as a DataSource and a TransactionManager here -->

</beans>

前面配置的结果是一个 fooService bean,该 bean 按顺序应用了分析和事务切面。如果您希望分析通知在进入事务通知之后和退出事务通知之前运行,您可以交换分析切面 bean 的 order 属性值,使其高于事务通知的顺序值。

您可以以类似的方式配置其他切面。

结合 AspectJ 使用 @Transactional

您还可以通过 AspectJ 切面在 Spring 容器之外使用 Spring Framework 的 @Transactional 支持。为此,首先使用 @Transactional 注解来注解您的类(或者类的方法),然后将您的应用程序与 spring-aspects.jar 中定义的 org.springframework.transaction.aspectj.AnnotationTransactionAspect 链接(编织) 。您还必须使用事务管理器配置切面。您可以使用 Spring Framework 的 IoC 容器来处理对切面的依赖注入。配置事务管理切面的最简单方法是使用 <tx:annotation-driven/> 标签并指定 mode 属性为 aspectj 。因为我们在这里专注于在 Spring 容器之外运行的应用程序,所以我们向您展示了如何以编程方式执行此操作。

以下示例显示了如何创建事务管理器并配置 AnnotationTransactionAspect 以使用它:

// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);

当您使用此切面时,您必须注解实现类(或该类中的方法),而不是该类实现的接口。AspectJ 遵循 Java 的规则,即接口上的注解不能被继承。

类上的 @Transactional 注解为类中任何公共方法的执行指定了默认事务语义。

类中方法上的 @Transactional 注解会覆盖类注解给出的默认事务语义。无论可见性如何,您都可以注解任何方法。

要使用 AnnotationTransactionAspect 编织您的应用程序,您必须使用 AspectJ(请参阅AspectJ 开发指南)构建您的应用程序或使用加载时编织。

编程式事务管理

Spring 框架提供了两种编程式事务管理的方法,通过使用:

  • TransactionTemplateTransactionalOperator
  • 直接使用 TransactionManager 实现

Spring 团队通常推荐 TransactionTemplate 用于命令式流、 TransactionalOperator 用于反应式代码中的编程化事务管理。第二种方法类似于使用 JTA UserTransaction API,尽管异常处理不那么麻烦。

使用 TransactionTemplate

TransactionTemplate 采用和其他 Spring 模板相同的方法,如 JdbcTemplate 。它使用回调方法(使应用程序代码免于执行样板获取和释放事务资源)并产生意图驱动的代码,因为您的代码只关注您想要做什么。

使用 TransactionTemplate 会将您与 Spring 的事务基础设施和 API 结合起来。编程式事务管理是否适合您的开发需求是您必须自己做出的决定。

必须在事务上下文中运行并显式使用 TransactionTemplate 的应用程序代码类似于以下示例。作为应用程序开发人员,您可以编写 TransactionCallback 实现(通常表示为匿名内部类),其中包含您需要在事务上下文中运行的代码。然后,您可以通过自定义 TransactionCallback 实例传递给 TransactionTemplateexecute(..) 方法。以下示例显示了如何执行此操作:

public class SimpleService implements Service {

    // single TransactionTemplate shared amongst all methods in this instance
    private final TransactionTemplate transactionTemplate;

    // use constructor-injection to supply the PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // the code in this method runs in a transactional context
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}

如果没有返回值,可以使用 TransactionCallbackWithoutResult 类,如下:

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});

回调中的代码可以通过调用 TransactionStatus 对象上的 setRollbackOnly() 方法来回滚事务 ,如下所示:

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});
指定事务设置

您可以使用 TransactionTemplate 以编程方式或在配置中指定事务设置(例如传播模式、隔离级别、超时等)。默认情况下,TransactionTemplate 实例具有 默认事务设置 。以下示例显示了特定事务设置的编程自定义 TransactionTemplate

public class SimpleService implements Service {

    private final TransactionTemplate transactionTemplate;

    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);

        // the transaction settings can be set here explicitly if so desired
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
        // and so forth...
    }
}

使用 Spring XML 配置:

<bean id="sharedTransactionTemplate"
        class="org.springframework.transaction.support.TransactionTemplate">
    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
    <property name="timeout" value="30"/>
</bean>

TransactionTemplate 类的实例是线程安全的,因为实例不维护任何会话状态。但是,TransactionTemplate 实例会维护配置状态。因此,虽然多个类可能共享 TransactionTemplate 的单个实例,但如果一个类需要使用具有不同设置(例如,不同的隔离级别)的 TransactionTemplate ,则需要创建两个不同的 TransactionTemplate 实例。

使用 TransactionalOperator

TransactionalOperator 遵循与其他反应式操作符类似的设计。

使用示例:

public class SimpleService implements Service {

    // single TransactionOperator shared amongst all methods in this instance
    private final TransactionalOperator transactionalOperator;

    // use constructor-injection to supply the ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionOperator = TransactionalOperator.create(transactionManager);
    }

    public Mono<Object> someServiceMethod() {

        // the code in this method runs in a transactional context

        Mono<Object> update = updateOperation1();

        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}

TransactionalOperator 可以通过两种方式使用:

  • 操作式风格,使用 Project Reactor 类型 ( mono.as(transactionalOperator::transactional)
  • 回调风格( transactionalOperator.execute(TransactionCallback)

回调中的代码可以通过调用 ReactiveTransaction 对象上的 setRollbackOnly() 方法来回滚事务,如下所示:

transactionalOperator.execute(new TransactionCallback<>() {

    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
});
取消信号

在 Reactive Streams 中, Subscriber 可以取消 Subscription 并停止它的 Publisher 。Project Reactor 以及在其它的库里的操作符,例如 next()take(long)timeout(Duration) 等,可以发出取消信号。无法知道取消的原因,无论是由于错误还是只是缺乏进一步消费的兴趣。从版本 5.3 开始取消信号导致回滚。因此,考虑事务 Publisher 下游使用的运算符很重要 。特别是在 Flux 或其他多值 Publisher 的情况下,必须消耗全部输出才能完成事务。

指定事务设置
public class SimpleService implements Service {

    private final TransactionalOperator transactionalOperator;

    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

        // the transaction settings can be set here explicitly if so desired
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 seconds
        // and so forth...

        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}

使用 TransactionManager

使用 PlatformTransactionManager

对于命令式事务,您可以直接使用 org.springframework.transaction.PlatformTransactionManager 来管理您的事务。为此,请通过 bean 引用将您使用的 PlatformTransactionManager 实现传递给您的 bean 。然后,通过使用 TransactionDefinitionTransactionStatus 对象,您可以发起事务、回滚和提交。以下示例显示了如何执行此操作:

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
    // put your business logic here
}
catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
使用 ReactiveTransactionManager

在处理反应事务时,可以使用 org.springframework.transaction.ReactiveTransactionManager 直接管理事务。为此,通过 bean 引用将使用的 ReactiveTransactionManager 的实现传递给 bean 。然后,通过使用 TransactionDefinition ReactiveTransaction 对象,您可以发起事务、回滚和提交。下面的例子说明如何这样做:

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);

reactiveTx.flatMap(status -> {

    Mono<Object> tx = ...; // put your business logic here

    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});

在编程式和声明式事务管理之间进行选择

只有在具有少量事务操作的情况下,适合选择编程式事务管理。例如,如果您的 Web 应用程序只需要针对某些更新操作的事务,您可能不想使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用 TransactionTemplate 可能是一个好方法。显式设置事务名称也只能通过使用编程方法进行事务管理来完成。

如果您的应用程序有大量事务性操作,适合选择声明式事务管理。它将事务管理保持在业务逻辑之外,并且不难配置。当使用 Spring Framework 而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。

事务绑定事件

从 Spring 4.2 开始,事件的监听器可以绑定到事务的某个阶段。典型的例子是在事务成功完成时处理事件。

您可以使用 @EventListener 注解注册常规事件监听器。如果需要将其绑定到事务,请使用 @TransactionalEventListener 。 这样做时,监听器默认绑定到事务的提交阶段。

假设一个组件发布了一个订单创建的事件,并且我们想要定义一个监听器,它应该只在发布它的事务已成功提交后才处理该事件。以下示例设置了这样的事件监听器:

@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        // ...
    }
}

@TransactionalEventListener 注解公开了一个 phase 属性,可让您自定义监听器应绑定到的事务阶段。有效阶段是 BEFORE_COMMITAFTER_COMMIT(默认)、AFTER_ROLLBACK 以及 AFTER_COMPLETION (聚合事务完成,无论是提交还是回滚)。

如果没有事务在运行,则根本不会调用监听器,因为我们无法满足所需的语义。但是,您可以通过将 fallbackExecution 注解的属性设置为 true 来覆盖该行为。

@TransactionalEventListener 仅适用于由 PlatformTransactionManager 管理的线程绑定事务, ReactiveTransactionManager 使用 Reactor 上下文而不是 thread-local 属性,因此从事件监听器的角度来看,它不能参与兼容两者的事务。

特定于应用服务器的集成

Spring 的事务抽象通常与应用服务器无关。

Spring JtaTransactionManager 是在 Java EE 应用服务器上运行的标准选择,并且可以在所有常见服务器上运行。

在 IBM WebSphere 6.1.0.9 及更高版本上,推荐使用的 Spring JTA 事务管理器是 WebSphereUowTransactionManager

在 Oracle WebLogic Server 9.0 或更高版本上,推荐使用 WebLogicJtaTransactionManager

常见问题解决方案

对特定 DataSource 使用错误的事务管理器

根据您选择的事务技术和需求使用正确的 PlatformTransactionManager 实现。如果使用得当,Spring Framework 仅提供了一个简单且可移植的抽象。如果使用全局事务,则必须将 org.springframework.transaction.jta.JtaTransactionManager 类用于所有事务操作。否则,事务基础设施会尝试在诸如容器 DataSource 实例之类的资源上执行本地事务。这样的本地事务没有意义,好的应用服务器会将它们视为错误。

进一步资源

posted @ 2022-06-09 21:18  流星<。)#)))≦  阅读(63)  评论(0编辑  收藏  举报