Spring Core 官方文档阅读笔记(十二)

1. Spring的事务抽象

spring的事务抽象的核心概念是事务的策略。策略由PlatformTransactionManager接口定义,代码如下:

public interface PlatformTransactionManager {

	TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

	void commit(TransactionStatus status) throws TransactionException;

	void rollback(TransactionStatus status) throws TransactionException;
}

根据接口的说明,可以了解到,一般不会将这个接口直接当做API来使用,而是通过TransactionTemplate或者AOP来实现声明式事务。对于编程人员,建议继承的AbstractPlatformTransactionManager 类来获取预先定义的传播行为,并且负责事务的同步处理。子类必须为基础事务的特定状态实现模版方法,如开始、暂停、提交、恢复等等。

我们来分别看一下三个方法:

  • getTransaction(TransactionDefinition definition)
    根据指定的传播行为返回当前事务或者创建新事务,需要注意的是,参数中的隔离级别或者超时时间等属性只限于创建新事务时才生效。若返回当前事务,则这些属性将被忽略。另外,若遇到不支持的事务设置,应该抛出异常,不过isReadOnly除外,当不显式的支持只读模式时,直接忽略该设置,而不是抛出Exception。
    当getTransaction的参数 definition 传入null时,启用默认设置。
  • commit(TransactionStatus status)
    根据给定的事务状态提交事务。如果事务以编程方式被设置为rollback-only,则执行回滚操作。如果事务不是新建的,则省略提交以正确参与周围的事务处理。如果新事务是通过挂起当前事务新建的,则新事务提交以后会恢复当前事务。
    需要注意的是,当事务被提交完毕时,无论是正常情况还是异常情况,事务都应该结束并执行清理工作,不应该执行回滚操作。
  • rollback
    执行给定事务的回滚操作。如果事务不是新事务,则只需要将事务状态设置为rollback-only,以便正确参与周围的事务。如果是当前事务挂起创建的新事务执行回滚,则回滚后要恢复到上一个事务。对于commit引发的异常,不应执行回滚。对失败的事务执行回滚,将抛出IllegalTransactionStateException。
    回滚默认发生在运行时异常,即RuntimeException。

getTransaction方法根据TransactionDefinition返回TranscationStatus。其中,TransactionDefinition接口的定义如下:

  • Propagation
    定义了事务的传播特性,有如下几个:
    • PROPAGATION_REQUIRED
      支持当前事务,若当前事务不存在,则创建一个新事务。这是事务传播特性的默认选项。
    • PROPAGATION_SUPPORTS
      支持当前事务,若当前事务不存在,则以非事务的方式执行。需要注意的是,对于具有事务同步的事务管理器, PROPAGATION_SUPPORTS 与没有事务是有一些区别的,因为它定义了同步可能适用的事务范围。因此将为整个范围指定相同的资源。一般来说,不要在 PROPAGATION_SUPPORTS 范围内依赖 PROPAGATION_REQUIRE 或者 PROPAGATION_REQUIRES_NEW,这可能导致运行同步操作时发生冲突。
    • PROPAGATION_MANDATORY
      支持当前事务,如果当前事务不存在则抛出异常。需要注意,PROPAGATION_MANDATORY 范围内的事务同步始终由周围的事务驱动。
    • PROPAGATION_REQUIRES_NEW
      创建一个新的事务,如果存在当前事务,则挂起当前事务。需要注意,并不是所有的事务管理器都可以直接使用PROPAGATION_REQUIRES_NEW,而且PROPAGATION_REQUIRES_NEW始终自己定义事务同步,现有的同步将被挂起,并在适当时间恢复。JtaTransactionManager可以使用该传播特性。
    • PROPAGATION_NOT_SUPPORTED
      不支持当前事务,更确切的说,总是以非事务的方式执行。需要注意,在PROPAGATION_NOT_SUPPORTED下,事务同步是不可用的。现有的同步会被挂起。与PROPAGATION_REQUIRES_NEW一样,并不是所有的事务管理器都可以使用该特性。JtaTransactionManager可以使用该传播特性。
    • PROPAGATION_NEVER
      不支持当前事务,更确切的说,如果存在事务,则会抛出异常。同样的,事务同步是不可用的。
    • PROPAGATION_NESTED
      如果存在当前事务,则在嵌套事务中执行。同样,PROPAGATION_NESTED只适用于特定的事务管理器。使用JDBC3.0驱动程序的JDBC DataSourceTransactionManager可以开箱即用。
  • Isolation
    定义了事务之间的工作隔离程度。例如,在一个事务中是否可以看到其他事务中未提交的写入。
    • ISOLATION_DEFAULT
      默认隔离级别,即使用底层事务的默认隔离级别。其他的隔离级别都对应于JDBC的隔离级别。
    • ISOLATION_READ_UNCOMMITTED
      允许被一个事务更改的数在提交之前被另一个事务读取(脏读)。如果发生回滚,则第二个事务将读取到无效的数据。
    • ISOLATION_READ_COMMITTED
      此隔离级别仅禁止读取未被提交的数据,因此只会防止脏读,但是不能防止幻读和不可重复读。
    • ISOLATION_REPEATABLE_READ
      此隔离级别禁止事务读取未被提交的数据,而且禁止第二个事务更改第一个事务读取的数据,因此可以防止脏读和不可重复读,但依然可能存在幻读。
    • ISOLATION_SERIALIZABLE
      此隔离级别可以防止脏读、幻读和不可重复读。所谓幻读,即存在两个事务,第一个事务读取一些业务数据,而第二个事务则往这些业务数据数据中增加新的记录,当地一个事务再次读取这些业务数据时,会获取到一些额外的数据。
  • Timeout
    事务运行的超时时间,默认使用底层事务的超时时间,如果不支持此配置,则没有超时时间。
  • Read-only status
    当只读取但不能修改数据时,可以使用此属性将事务设置为只读。

TransactionStatus接口为事务性代码提供了一种控制事务执行和查询事务状态的简便方法。接口定义如下:

public interface TransactionStatus extends SavepointManager, Flushable {

    /**
     * 返回当前事务是否是新建事务
     */
	boolean isNewTransaction();

    /**
     * 返回当前事务是否包含保存点,即是否创建基于保存点的嵌套事务。
     */
	boolean hasSavepoint();

    /**
     * 设置rollback-only,这将指示事务管理器,该事务的唯一可能结果就是
     * 回滚,用于代替异常触发回滚
     */
	void setRollbackOnly();

    /**
     * 返回该事务是否被标识为rollback-only
     */
	boolean isRollbackOnly();

    /**
     * 将会话刷新到数据存储
     */
	@Override
	void flush();

    /**
     * 返回当前事务是否结束。commit或者rollback都属于结束的状态。
     */
	boolean isCompleted();

}

该接口提供了通过代码检索事务状态,并以编程方式请求回滚(而不是通过异常引发回滚)的功能。它继承了SavepointManager,以提供对保存点管理工具的访问。

2. 资源与实务同步

  • 首选的方案是通过spring的基于模版的持久化集成API,或者原生的ORM API与事务感知工厂bean或代理一起使用,以管理本地资源工厂。
  • 也可以使用DataSourceUtils(JDBC)、EntityManagerFactoryUtils(JPA)或SessonFactoryUtils(Hibernate)等工具类来获得spring托管的实例。
  • 还可以使用TransactionAwareDataSourceProxy类来获取spring托管事务的感知能力。

3. Spring声明式事务

Spring的声明式事务是通过AOP来实现的,AOP和事务性元数据结合产生一个AOP代理,该代理结合使用TransactionInteceptor和PlatformTransactionManager来驱动环绕通知调用事务。有如下特点:

  • 可以在任何环境中使用,通过调整配置文件,可以用于JDBC、JPA、Hibernate或者JTA等等
  • 可以应用于任何类
  • 提供了声明式的回滚规则
  • 允许编程人员通过AOP自定义事务行为
  • 不允许事务上下文在远程调用之间传播

事务代理调用方法的流程图如下:
transactionProxy

spring的官方文档中有一个声明式事务的例子,我们来看一下

// 我们希望添加事务的接口

  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);
  
  }
  // FooService的实现类
  package x.y.service;

  public class DefaultFooService implements FooService {
  
      public Foo getFoo(String fooName) {
          throw new UnsupportedOperationException();
      }
  
      public Foo getFoo(String fooName, String barName) {
          throw new UnsupportedOperationException();
      }
  
      public void insertFoo(Foo foo) {
          throw new UnsupportedOperationException();
      }
  
      public void updateFoo(Foo foo) {
          throw new UnsupportedOperationException();
      }
  
  }
  <?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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop.xsd">
  
      <bean id="fooService" class="x.y.service.DefaultFooService"/>
  
      <!-- 事务Advice,前文说到事务是通过Spring的AOP来实现的,
      那么AOP中的Advice的概念,即对应了需要被事务化的方法所配置的事务的详细属性。
      如果transaction-manager中的事务管理器名称为transactionManager,则该属性可以省略。-->
      <tx:advice id="txAdvice" transaction-manager="txManager">
          <tx:attributes>
              <!-- 所有以get开头的方法,都被设定为read-only -->
              <tx:method name="get*" read-only="true"/>
              <!-- 其他的方法则被设置为默认的事务配置 -->
              <tx:method name="*"/>
          </tx:attributes>
      </tx:advice>
  
      <!-- 设置事务AOP的作用范围 -->
      <aop:config>
          <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
          <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
      </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>
  
      <!-- 配置事务管理器(PlatformTransactionManager) -->
      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>
  
  </beans>

4. 回滚

Spring中推荐的回滚方式是通过抛出异常回滚。Spring的默认配置仅仅在抛出RuntimeException的时候发生回滚。检查型的异常默认不会触发回滚操作。我们可以在tx:advice标签中设置需要触发回滚的异常,如下:

<tx:advice>
  <tx:attributes>
      <tx:method name="get*" read-only="true" />
      <tx:method name="*" rollback-for="DataNotExistException" />
  </tx:attributes>
</tx:advice>

同样,我们也可以设置不需要回滚的具体异常,如下:

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

我们也可以通过代码来完成回滚,如下:

public void resolvePosition() {
  try {
      // some business logic...
  } catch (NoProductInStockException ex) {
      // trigger rollback programmatically
      // 前面提到过rollback-only属性,如果设置了该属性,则事务管理器只会执行回滚操作,以此来触发回滚
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  }
}

4. 为不同的Bean配置不同的事务

我们来考虑一种场景:我们有许多服务层对象,并且希望对每个对象应用完全不同的事务配置。我们可以通过在一个tx:advisor中配置不同的pointcut和advice-ref来实现。

先来看一个单独事务配置的例子:

    <?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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd
          http://www.springframework.org/schema/aop
          http://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"/> 
      <bean id="barManager" class="x.y.service.SimpleBarManager"/>
  
      <tx:advice id="txAdvice">
          <tx:attributes>
              <tx:method name="get*" read-only="true"/>
              <tx:method name="*"/>
          </tx:attributes>
      </tx:advice>
  
  </beans>

上面这个例子中,anotherService和barManager将不会启用事务。
下面我们来看一下如何配置不同的事务。

    <?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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd
          http://www.springframework.org/schema/aop
          http://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>
  
      <bean id="fooService" class="x.y.service.DefaultFooService"/>
  
      <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>
  </beans>

上面的例子中,DefaultFooService和DefaultDdlManager将被启用不同的事务配置。而DefaultDdlMananger的配置恰好就是不启用事务。

5. tx:advice配置

tx:advice下有一个tx:attributes属性,具体的事务配置项是在tx:attributes下的tx:method中设置的。我们来看一下tx:method的所有属性:

属性名 是否必须 默认值 描述
name YES 与事务属性关联的方法名称,可以使用通配符(*)
propagetion NO REQUIRED 事务的传播特性
isolation NO DEFAULT 事务的隔离级别,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
timeout NO -1 事务超时时间,-1代表使用底层事务的过期时间配置,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
read-only NO false 读写事务和只读事务,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
rollback-for NO 触发回滚的异常实例,若有多个,用逗号分隔
no-rollback-for NO 不触发回滚的异常实例,若有多个,用逗号分隔

6. @Transactional注解

除了xml配置事务外,还可以使用@Transactional注解来配置。先来看一个例子:

@Transactional
public class MyServiceImpl implements Service {
    
    Object getObject(String key);
}

当@Transactional标注在类上时,对类中的每一个方法都生效。需要注意的是,类级注解并不能对其父类生效,并且只对public修饰的方法生效,如果要对private或者protect修饰的方法添加事务管理,可以考虑AspectJ。要使用@Transactional注解,首先要启用spring注解驱动的事务管理功能,有两种方法:

    <?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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop.xsd">
  
      <bean id="fooService" class="x.y.service.DefaultFooService"/>
  
      <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>

  </beans>
  • 通过@EnableTransactionManager注解开启
@Configuration
@EnableTransactionManager
public class Config {
    
    ...
}

来看下tx:annotation-driven和@EnableTransactionManager的属性对比:

annotation-driven的属性名 EnableTransactionManager的属性名 默认值 描述
transaction-manager - transactionManager 事务管理器的名字,只有在name不是transactionManager的时候是必须的,也就是说如果name="transactionManager",此属性可以省略
mode mode proxy 使用代理模式来处理事务范围内的bean
proxy-target-class proxyTargetClass false 仅在代理模式下生效,用于控制为使用@Transactional注解的类创建的事务代理的类型,如果为true,则使用基于类的代理,若未false或省略,则使用基于JDK接口的代理
order order Ordered.LOWEST_PRECEDENCE 控制事务Advice的执行顺序,若不指定,则使用AOP子系统的通知顺序

顺带简单说一下Ordered接口的两个顺序:LOWEST_PRECEDENCE和HIGHEST_PRECEDENCE。Ordered接口有一个getOrder()方法,返回一个int值,在排序的时候,会根据这个int的值进行排序,值小的优先级高,也可以理解为正序排列,这种就对应LOWEST_PRECEDENCE,相对的,如果要将值大的排在前面,则可以设置HIGHEST_PRECEDENCE。

Spring团队推荐只在实现类或具体方法上使用@Transactional注解,虽然它可以被用在接口或接口方法上。原因是,如果将@Transactional注解用在接口或接口方法上,那么只有在使用基于接口的代理时才会生效,也就是说当使用JDK代理时可以正常工作,但是如果使用的是Cglib代理,则不会启用事务。

在代理中,只有通过代理传入的外部方法调用才会被事务拦截,也就是说如果被@Transactional标注的方法是被类里的另外一个方法调用,此时,事务是不会生效的。而且,代理必须完成初始化后才能正常使用事务,因此,不应该在初始化过程中添加事务。

另外,@EnableTransactionManager和tx:annotation-driven仅在定义他们的同一个应用的上下文中查找@Transactional注解的bean,也就是说,如果我们把这两个配置放在了DispatcherServlet的WebApplicationContext中,则它只查找Controller层的@Transactional注解的bean。

如果在类和方法上同时标注了@Transactional注解,优先使用方法上的,如下:

@Transactinal(readOnly=true)
public class MyServiceImpl implements Service {
    
    @Transactional(readOnly=false)
    public void update(Object obj) {
        ...
    }
}

虽然在MyServiceImpl上标注了readOnly=true,但是在update方法上设置了readOnly=false,因此在执行update方法时,仍然是读写事务。

下面来看一下@Transactional的内部属性:

属性名 类型 默认值 描述
value String - 指定要使用的事务管理器(PlatformTransactionManager)
propagation enum:Propagation PROPAGATION_REQUIRED 传播特性
isolation enum:Isolation ISOLATION_DEFAULT 隔离级别,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
timeout int(秒数) -1 事务超时时间,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
readonly boolean false 读写事务和只读事务,仅适用于REQUIRED和REQUIRED_NEW两种传播特性
rollbackFor Class RuntimeException 触发回滚的异常,默认RuntimeException及子类
rollbackForClassName String[] - 触发回滚的异常类名字
noRollbackFor Class - 不触发回滚的异常
noRollbackForClassName String[] - 不触发回滚的异常类名字

我们没办法显示的控制事务的名称,对于声明式的事务,事务名称是[类的完全限定名.事务通知类的方法名],例如,如果com.transaction.example包下的MyServiceImpl的update(...)方法启用了事务,那么事务的名称就是com.transaction.example.MyServiceImpl.update。

上述属性中的value,可以用来在一个应用中使用多个事务管理器,如下:

public class TransactionalService {

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

  @Transactional("account")
  public void doSomething() { ... }
}
  <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>

上面的xml中,tx:annotation-driven中没有设置transaction-manager,那么当order和account都没有找到时,依然使用transactionManager事务管理器。

如果有大量重复使用的相同配置的@Transactional注解,可以考虑自定义注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
    
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
    
}
public class TransactionalService {

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

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

7. 事务的传播特性

在介绍传播特性之前,我们要先知道,事务分为物理事务和逻辑事务,具体后面会介绍。

  • PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED会强制执行物理事务。如果当前范围内不存在事务,则在本地执行新建的事务,否则,将参与到现有的事务。
    当参与到现有事务时,自身的一些属性将被忽略,如隔离级别、超时时间、只读标识等等。如果希望在参与外部事务时拒绝适用现有事务的隔离级别和只读设置时,可以在事务管理器(AbstractTransactionManager)中的ValidateExistingTransaction标识切换成true。它会检测与内部事务定义不兼容的隔离级别和只读设置,并抛出相对应的异常来拒绝参与外部事务。
    当传播特性被设置为PROPAGATION_REQUIRED时,spring将会为每一个配置了该传播特性的方法创建一个逻辑事务。每个这样的逻辑事务都可以单独的处理rollback-only状态,而外部事务作用域在逻辑上独立于内部事务作用域,并且所有的事务作用域都映射到同一个物理事务上。因此,内部事务的rollback-only状态会影响到外部事务的提交。
    如果内部事务设置了rollback-only,而外部事务没有,那么此时的回滚并不是我们所期望的,将抛出UnexpectedRollbackException。因此,如果内部事务在未通知外部事务的情况下将事务标记为回滚,而外部事务仍然调用了commit,那么外部的调用方需要接收一个UnexpectedRollbackException,以清楚的标示出commit操作实际执行的是回滚。
  • PROPAGATION_REQUIRES_NEW
    与PROPAGATION_REQUIRED不同的是,PROPAGATION_REQUIRES_NEW始终对每个受影响的事务作用域创建单独的物理事务。也就是说,不存在内部事务作用域参与外部事务作用域的情况。也就是可以完全独立的提交和回滚,外部事务不会受到内部事务回滚的影响。
  • PROPAGATION_NESTED
    PROPAGATION_NESTED使用具有多个保存点的单个物理事务,该事务可以回滚到多个保存点。这样的部分回滚允许内部事务作用域触发其作用域内的回滚操作,而外部事务能够继续物理事务。此设置通常映射到JDBC保存点,因此它仅适用于JDBC资源事务。

现在我们回过头看一下上面提到的物理事务和逻辑事务。网上查了一些资料,没有说的特别明白的,但是在了解了上述三种传播特性之后,似乎有点思路了。我们一起整理一下:
首先,三种传播特性都提到的物理事务,但是PROPAGATION_REQUIRES_NEW和PROPAGATION_NESTED并没有提到逻辑事务,并且,对物理事务的描述也非常贴合三种传播特性的特点。那么我们可以推断,Spring中所说的事务,是指上文中我们说的物理事务。
第二,PROPAGATION_REQUIRED的介绍中,说到"每个这样的逻辑事务都可以单独的处理rollback-only状态,而外部事务作用域在逻辑上独立于内部事务作用域,并且所有的事务作用域都映射到同一个物理事务上。",这句话描述的场景是,当有多个处理都标注了PROPAGATION_REQUIRED,并且属于内部事务和外部事务的关系的情况。考虑一下PROPAGATION_REQUIRED的特点,它支持当前事务,若当前事务不存在,则新建一个事务。再来理解一下那句话,它说的应该是有多个方法都标注了@Transactional,并且传播特性都是PROPAGATION_REQUIRED,他们之间有着相互调用的关系,此时,就产生了内部事务作用域和外部事物作用域的概念,而这里的事务,按照描述中的说法,是属于逻辑事务。但是我们在第一点上说过了,Spring中的事务对应的是物理事务,该怎么理解呢?别急,我们想一下代码中有逻辑删除和物理删除,对应的,我们也可以先假设逻辑事务只是一种虚拟的事务,为了对某一个持久层的操作加一个标记。那么再回过头看,似乎可以说的通了,Spring为了方便管理,对每一个加了事务注解的方法都创建了一个单独的逻辑事务作用域,这里的单个数据库事务并不会把数据真正提交到数据库,他们只是各自执行各自的处理,并且在处理结束后通过映射的物理事务进行提交。那么,如果某个内部的逻辑事务设置了rollback-only,但是外部事务并不知道,依然做了commit操作,那么对于物理事务来说,commit操作就变成了回滚操作。所以,Spring会抛出一个UnexpectedRollbackException,来标注这一情况。
嗯!我被自己说服了,我觉着可以说得通~~

8. 事务和普通AOP通知的执行先后顺序

自定义Advice的时候可以通过实现Ordered接口,来指定执行顺序。当是顺序规则时,如果想自定义Advice在事务之前执行,可以将自定义Advice的order设置的比事务的order小,反之,如果比事务的order大,则会在事务之后执行。

9. 将@Transactional用于AspectJ

可以通过AnnotationTransactionAspect将@Transactional用于AspectJ,如下:

// 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);

10. 编程化事务管理

Spring提供了两种编程式的事务管理方法:

  • TransactionTemplate
  • PlatformTransactionManager的实现类

Spring官方推荐使用TransactionTemplate。下面我们来分别看一下:

TransactionTemplate

TransactionTemplate是线程安全的。直接上代码

  /**
  * @Author: kuromaru
  * @Date: Created in 16:29 2019/5/7
  * @Description:
  * @modified:
  */
  public interface MyService {
  
    void update();
  
    void update2();
  }
  /**
   * @Author: kuromaru
   * @Date: Created in 16:30 2019/5/7
   * @Description:
   * @modified:
   */
  public class MyServiceImpl implements MyService {
  
      @Autowired
      private DataSourceTransactionManager transactionManager;
  
      @Override
      public void update() {
          new TransactionTemplate(transactionManager).execute(new TransactionCallback<Object>() {
              @Override
              public Object doInTransaction(TransactionStatus status) {
                  status.setRollbackOnly();
                  return null;
              }
          });
      }
  
      @Transactional(rollbackFor = ArrayIndexOutOfBoundsException.class, propagation = Propagation.REQUIRED)
      @Override
      public void update2() {
          update();
      }
  }
  /**
   * @Author: kuromaru
   * @Date: Created in 15:57 2019/5/7
   * @Description:
   * @modified:
   */
  @Configuration
  @EnableTransactionManagement
  public class TransactionConfig {
  
      @Bean
      public BasicDataSource dataSource() {
          BasicDataSource dataSource = new BasicDataSource();
          dataSource.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8");
          dataSource.setDriverClassName("com.mysql.jdbc.Driver");
          dataSource.setUsername("xxx");
          dataSource.setPassword("xxx");
          dataSource.setInitialSize(10);
          dataSource.setMinIdle(10);
          dataSource.setMaxIdle(10);
          dataSource.setMaxWaitMillis(200);
          return dataSource;
      }
  
      @Bean
      public DataSourceTransactionManager dataSourceTransactionManager() {
          DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
          dataSourceTransactionManager.setDataSource(dataSource());
          return dataSourceTransactionManager;
      }
  
      @Bean("myServiceImpl")
      public MyService myServiceImpl() {
          return new MyServiceImpl();
      }
  }
  /**
   * @Author: kuromaru
   * @Date: Created in 16:22 2019/5/7
   * @Description:
   * @modified:
   */
  public class Client {
  
      public static void main(String[] args) {
          AnnotationConfigApplicationContext acac = new AnnotationConfigApplicationContext(TransactionConfig.class);
          MyService myService = (MyService) acac.getBean("myServiceImpl");
          myService.update2();
      }
  }
  五月 07, 2019 4:58:28 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
  信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@14514713: startup date [Tue May 07 16:58:28 CST 2019]; root of context hierarchy
  Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
  Exception in thread "main" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
  	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:728)
  	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:518)
  	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292)
  	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
  	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
  	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
  	at com.sun.proxy.$Proxy17.update2(Unknown Source)
  	at transaction.Client.main(Client.java:17)
  
  Process finished with exit code 1

正好验证了PROPAGATION_REQUIRED里的UnexpectedRollbackException的场景,MyServiceImpl里的update()方法,通过TransactionTemplate的execute方法,设置了rollback-only,而update2()方法中依然是正常提交,所以,Spring抛出了UnexpectedRollbackException。当然,一般的用法是在捕捉到异常以后再设置rollback-only。
再来看下execute方法的参数,传入了一个TransactionCallback的匿名类,具体的事务包含的持久层逻辑则写在doInTransaction方法中。

我们也可以对TransactionTemplate的属性做个性化设置,如下:

  public void update() {
      TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
      transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
      transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
      transactionTemplate.setReadOnly(false);
      transactionTemplate.setTimeout(-1);
      transactionTemplate.execute(new TransactionCallback<Object>() {
          @Override
          public Object doInTransaction(TransactionStatus status) {
              status.setRollbackOnly();
              return null;
          }
      });
  }
PlatformTransactionManager
  /**
   * @Author: kuromaru
   * @Date: Created in 16:30 2019/5/7
   * @Description:
   * @modified:
   */
  public class MyServiceImpl implements MyService {
  
      @Autowired
      private DataSourceTransactionManager transactionManager;

      @Override
      public void update() {
          TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
          transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
          transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
          transactionTemplate.setReadOnly(false);
          transactionTemplate.setTimeout(-1);
          transactionTemplate.execute(new TransactionCallback<Object>() {
              @Override
              public Object doInTransaction(TransactionStatus status) {
                  status.setRollbackOnly();
                  return null;
              }
          });
      }
  
      @Transactional(rollbackFor = ArrayIndexOutOfBoundsException.class, propagation = Propagation.REQUIRED)
      @Override
      public void update2() {
  //        update();
          update3();
      }
  
      public void update3() {
          DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
          defaultTransactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
          defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
          defaultTransactionDefinition.setReadOnly(false);
          defaultTransactionDefinition.setTimeout(-1);
  
          TransactionStatus transactionStatus = transactionManager.getTransaction(defaultTransactionDefinition);
          transactionStatus.setRollbackOnly();
  
          transactionManager.commit(transactionStatus);
      }
  }

调用update3()依然会得到UnexceptedRollbackException。与TransactionTemplate通过回调提交事务的做法不同,使用transactionMananger的代码,要通过transactionManager.commit(transactionStatus)来提交事务。

11. 事务绑定事件

在之前的整理中,我们用@EventListener来绑定事件。如果需要把事件绑定在事务上,可以使用@TransactionalEventListener。

先来看一下@TransactionalEventListener的定义:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

	/**
	 * Phase to bind the handling of an event to.
	 * 绑定到事务的阶段,备选值有BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION
	 * <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}.
	 * <p>If no transaction is in progress, the event is not processed at
	 * all unless {@link #fallbackExecution} has been enabled explicitly.
	 */
	TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

	/**
	 * Whether the event should be processed if no transaction is running.
	 * 没有事务运行的时候是否处理事件
	 */
	boolean fallbackExecution() default false;

	/**
	 * Alias for {@link #classes}.
	 */
	@AliasFor(annotation = EventListener.class, attribute = "classes")
	Class<?>[] value() default {};

	/**
	 * The event classes that this listener handles.
	 * <p>If this attribute is specified with a single value, the annotated
	 * method may optionally accept a single parameter. However, if this
	 * attribute is specified with multiple values, the annotated method
	 * must <em>not</em> declare any parameters.
	 */
	@AliasFor(annotation = EventListener.class, attribute = "classes")
	Class<?>[] classes() default {};

	/**
	 * Spring Expression Language (SpEL) attribute used for making the event
	 * handling conditional.
	 * <p>The default is {@code ""}, meaning the event is always handled.
	 * 通过SpEL来指定事件执行的条件,默认值是"",表示事件一定会被执行
	 * @see EventListener#condition
	 */
	String condition() default "";

}

根据@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}),可以知道这个注解只可以加在方法上或者注解类上。

我们来写个例子看看:

/**
 * @Author: kuromaru
 * @Date: Created in 16:29 2019/5/7
 * @Description:
 * @modified:
 */
public interface MyService {

    void commit();

    void rollback();
}
/**
 * @Author: kuromaru
 * @Date: Created in 16:30 2019/5/7
 * @Description:
 * @modified:
 */
public class MyServiceImpl implements MyService {

    public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    public void setMyEventPublisher(MyEventPublisher myEventPublisher) {
        this.myEventPublisher = myEventPublisher;
    }

    private MyEventPublisher myEventPublisher;

    private TransactionTemplate transactionTemplate;

    @Override
    public void commit() {
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                myEventPublisher.publish();
                System.out.println("This is commit operation");
                return null;
            }
        });
    }

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    @Override
    public void rollback() {
        myEventPublisher.publish();
        System.out.println("throw RuntimeException");
        throw new RuntimeException();
    }
}
/**
 * @Author: kuromaru
 * @Date: Created in 10:32 2019/5/8
 * @Description:
 * @modified:
 */
public class MyEvent extends ApplicationEvent {
    /**
     * Create a new ApplicationEvent.
     *
     * @param source the object on which the event initially occurred (never {@code null})
     */
    public MyEvent(Object source) {
        super(source);
    }
}
/**
 * @Author: kuromaru
 * @Date: Created in 10:33 2019/5/8
 * @Description:
 * @modified:
 */
public class MyEventPublisher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void publish() {
        applicationEventPublisher.publishEvent(new MyEvent(this));
    }
}
/**
 * @Author: kuromaru
 * @Date: Created in 10:06 2019/5/8
 * @Description:
 * @modified:
 */
public class MyEventListener {

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void beforeCommitHandler(MyEvent myEvent) {
        System.out.println("beforeCommitHandler");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommitHandler(MyEvent myEvent) {
        System.out.println("afterCommitHandler");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void afterRollbackHandler(MyEvent myEvent) {
        System.out.println("afterRollbackHandler");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void afterCompletionHandler(MyEvent myEvent) {
        System.out.println("afterCompletionHandler");
    }
}
/**
 * @Author: kuromaru
 * @Date: Created in 15:57 2019/5/7
 * @Description:
 * @modified:
 */
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public BasicDataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUsername("xxx");
        dataSource.setPassword("xxx");
        dataSource.setInitialSize(10);
        dataSource.setMinIdle(10);
        dataSource.setMaxIdle(10);
        dataSource.setMaxWaitMillis(200);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource());
        return dataSourceTransactionManager;
    }

    @Bean("myServiceImpl")
    public MyService myServiceImpl() {
        MyServiceImpl myService = new MyServiceImpl();
        TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager());
        myService.setTransactionTemplate(transactionTemplate);
        myService.setMyEventPublisher(myEventPublisher());
        return myService;
    }

    @Bean
    public MyEventListener myEventListener() {
        return new MyEventListener();
    }

    @Bean
    public MyEventPublisher myEventPublisher() {
        return new MyEventPublisher();
    }
}
五月 08, 2019 1:32:51 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@14514713: startup date [Wed May 08 13:32:51 CST 2019]; root of context hierarchy
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
This is commit operation
beforeCommitHandler
afterCommitHandler
afterCompletionHandler

Process finished with exit code 0

再来看一下AFTER_ROLLBACK的执行结果

/**
 * @Author: kuromaru
 * @Date: Created in 16:22 2019/5/7
 * @Description:
 * @modified:
 */
public class Client {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext acac = new AnnotationConfigApplicationContext(TransactionConfig.class);
        MyService myService = (MyService) acac.getBean("myServiceImpl");
//        myService.commit();
        try {
            myService.rollback();
        } catch (Exception e) {
            System.out.println("====================================================");
        }
    }
}
五月 08, 2019 1:45:46 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@14514713: startup date [Wed May 08 13:45:46 CST 2019]; root of context hierarchy
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
throw RuntimeException
afterRollbackHandler
afterCompletionHandler
====================================================

Process finished with exit code 0

先执行了AFTER_ROLLBACK,然后再执行了AFTER_COMPLETION

12. TransactionSynchronizationManager

上面介绍了Spring的事务事件绑定,可以在事务执行的前后添加我们自己的处理。除了@TransactionalEventListener之外,还有TransactionSynchronizationManager可以实现相似的效果,我们来看一下:

/**
 * @Author: kuromaru
 * @Date: Created in 16:29 2019/5/7
 * @Description:
 * @modified:
 */
public interface MyService {

    void commit();

}
/**
 * @Author: kuromaru
 * @Date: Created in 16:30 2019/5/7
 * @Description:
 * @modified:
 */
public class MyServiceImpl implements MyService {

    @Autowired
    private DataSourceTransactionManager transactionManager;

    @Override
    public void commit() {
        new TransactionTemplate(transactionManager).execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                System.out.println("This is commit operation");
                if (TransactionSynchronizationManager.isSynchronizationActive()) {
                    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                        @Override
                        public void suspend() {

                        }

                        @Override
                        public void resume() {

                        }

                        @Override
                        public void flush() {

                        }

                        @Override
                        public void beforeCommit(boolean readOnly) {
                            System.out.println("before commit");
                        }

                        @Override
                        public void beforeCompletion() {

                        }

                        @Override
                        public void afterCommit() {

                        }

                        @Override
                        public void afterCompletion(int status) {

                        }
                    });
                }

                return null;
            }
        });
    }
}
/**
 * @Author: kuromaru
 * @Date: Created in 15:57 2019/5/7
 * @Description:
 * @modified:
 */
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public BasicDataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUsername("xxx");
        dataSource.setPassword("xxx");
        dataSource.setInitialSize(10);
        dataSource.setMinIdle(10);
        dataSource.setMaxIdle(10);
        dataSource.setMaxWaitMillis(200);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource());
        return dataSourceTransactionManager;
    }

    @Bean("myServiceImpl")
    public MyService myServiceImpl() {
        return new MyServiceImpl();
    }

}
/**
 * @Author: kuromaru
 * @Date: Created in 16:22 2019/5/7
 * @Description:
 * @modified:
 */
public class Client {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext acac = new AnnotationConfigApplicationContext(TransactionConfig.class);
        MyService myService = (MyService) acac.getBean("myServiceImpl");
        myService.commit();
    }
}
五月 08, 2019 2:09:34 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@14514713: startup date [Wed May 08 14:09:34 CST 2019]; root of context hierarchy
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
This is commit operation
before commit

Process finished with exit code 0

TransactionSynchronizationManager.registerSynchronization方法用语注册一个同步处理,传入了一个TransactionSynchronization接口的匿名实现类。当然,如果不想每次都把接口的所有方法实现一遍,可以传入一个TransactionSynchronizationAdapter,这是个抽象类,我们可以有选择的覆盖里面的方法。

TransactionSynchronizationManager里面维护了一个ThreadLocal<Set>,线程安全,虽然ThreadLocal实际上是一个WeakReference,可以自动回收不使用的资源,但是最好还是手动调用一下clearSynchronization()来清理当前线程下的实例。

posted @ 2020-05-22 10:12  kuromaru  阅读(362)  评论(0)    收藏  举报