Spring事物详解

管理两种方式

spring支持编程式事务管理和声明式事务管理两种方式。

  • 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
  • 声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。

声明式事务的使用技巧

1、@Transactional可以作用于接口、接口方法、类、类方法上,当作用到类时,该类下所有public方法都将具有该类型的事务属性,同时,也可以在方法级别使用该注解来覆盖类级别的定义。Spring的建议是在具体的实现类和类方法使用@Transactional注解,而不是使用在接口上。因为注解不能继承,不能被基于接口的代理类所识别,注解失效。

2、声明式事务管理默认只对非检查型异常unchecked Exception进行回滚,也就是对RuntimeException异常以及它的子类进行回滚操作。



如果需要让checked Exception也进行回滚,需加上@Transactional(rollbackFor=Exception.class)、

如果需要让unchecked Exception不进行回滚,需加上@Transactional(notRollbackFor=Exception.class)

3、在Springboot使用声明式事务需要在Application启动类加入@EnableTransactionManagement注解,相当于Spring的自动扫描

四、声明式事务的常用配置

参 数 名 称 功 能 描 述
readOnly 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)
rollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
rollbackForClassName 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})
noRollbackFor 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})
propagation 该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
isolation 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
timeout 该属性用于设置事务的超时秒数,默认值为-1表示永不超时

spring事务特性

spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口其中TransactionDefinition接口定义以下特性:

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
  • TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。(注意这里,不要采坑
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务超时
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以int 的值来表示超时时间,其单位是秒。

默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。

事务只读属性
只读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化,比如使用Hibernate的时候。

“只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。

但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。

因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可。

spring事务回滚规则

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。

示例:基于注解的声明式事务管理配置@Transactional

spring.xml

<!-- mybatis config -->  
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
    <property name="dataSource" ref="dataSource" />  
    <property name="configLocation">  
        <value>classpath:mybatis-config.xml</value>  
    </property>  
</bean>  

<!-- mybatis mappers, scanned automatically -->  
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage">  
        <value> com.baobao.persistence.test </value>  
    </property>  
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />  
</bean>  

<!-- 配置spring的PlatformTransactionManager,名字为默认值 -->  
<bean id="transactionManager" 
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource" />  
</bean>  

<!-- 开启事务控制的注解支持 -->  
<tx:annotation-driven transaction-manager="transactionManager"/>

添加tx名字空间

xmlns:aop="http://www.springframework.org/schema/aop" 
xmlns:tx="http://www.springframework.org/schema/tx"  
xsi:schemaLocation="http://www.springframework.org/schema/aop 
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd  
http://www.springframework.org/schema/tx 
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"

MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。

@Transactional注解

使用@Transactional时,可以指定如下属性:

a、isolation:用于指定事务的隔离级别。默认为底层事务的隔离级别。
b、noRollbackFor:指定遇到指定异常时强制不回滚事务。
c、noRollbackForClassName:指定遇到指定多个异常时强制不回滚事务。该属性可以指定多个异常类名。
d、propagation:指定事务的传播属性。
e、readOnly:指定事务是否只读。
f、rollbackFor:指定遇到指定异常时强制回滚事务。
g、rollbackForClassName:指定遇到指定多个异常时强制回滚事务。该属性可以指定多个异常类名。
h、timeout:指定事务的超时时长。**

事务可回滚要注意点*

1、被注解的必须是public

@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

poxy-target-class="true"表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。</pre>

2、rollbackFor和noRollbackFor

需我们指定方式来让事务回滚 :
要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class,其它异常})
如果让unchecked例外不回滚:@Transactional(notRollbackFor=RunTimeException.class)

(关于rollbackFor配置的经历)

当的Transactional中配置rollbackFor = Exception.class时,抛出RuntimeException时是会回滚的。但是如果是Unchecked Exceptions则不会回滚。

于是查看Spring的Transactional的API文档,发现下面这段:

If no rules are relevant to the exception, it will be treated like DefaultTransactionAttribute (rolling back on runtime exceptions).

后面又试了下发现,如果不添加rollbackFor等属性,Spring碰到Unchecked Exceptions都会回滚,不仅是RuntimeException,也包括Error。

3、来自外部的方法调用才会被AOP代理捕获

默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。(我碰到后,只有重构代码再引入一层service解决)

示例一:(不要rollbackFor,unchecked异常,可以回滚)
@Autowired 
private MyBatisDao dao;  

@Transactional  
@Override public void insert(Test test) {  
    dao.insert(test); throw new RuntimeException("test");
    //抛出unchecked异常,触发事物,回滚 
} 
示例二:(noRollbackFor使用场景)
@Transactional(noRollbackFor=RuntimeException.class)  
    @Override public void insert(Test test) {  
        dao.insert(test); 
        //抛出unchecked异常,触发事物,noRollbackFor=RuntimeException.class,不回滚 
        throw new RuntimeException("test");  
    } 
示例三:当作用于类上时,该类所有 public 方法将都具有该类型的事务属性。
@Transactional 
public class MyBatisServiceImpl implements MyBatisService {  

    @Autowired private MyBatisDao dao;  

    @Override public void insert(Test test) {  
        dao.insert(test); //抛出unchecked异常,触发事物,回滚 
        throw new RuntimeException("test");  
    }
}
示例四:propagation=Propagation.NOT_SUPPORTED
@Transactional(propagation=Propagation.NOT_SUPPORTED)  
@Override public void insert(Test test) { 
 //事物传播行为是PROPAGATION_NOT_SUPPORTED,以非事务方式运行,不会存入数据库
 dao.insert(test);  
} 

事物注解方式: @Transactional

当标于类前时, 标示类中所有方法都进行事物处理 , 例子:

 @Transactional 
public class TestServiceBean implements TestService {
}

当类中某些方法不需要事物时:

@Transactional  
 public class TestServiceBean implements TestService { 
    private TestDao dao; 
    public void setDao(TestDao dao) { 
      this.dao = dao; 
    } 

@Transactional(propagation =Propagation.NOT_SUPPORTED)
 public List getAll() { 
    return null; 
  } 
}

事物传播行为介绍:

  • @Transactional(propagation=Propagation.REQUIRED)
    如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)

  • @Transactional(propagation=Propagation.NOT_SUPPORTED)
    容器不为这个方法开启事务

  • @Transactional(propagation=Propagation.REQUIRES_NEW)
    不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务

  • @Transactional(propagation=Propagation.MANDATORY)
    必须在一个已有的事务中执行,否则抛出异常

  • @Transactional(propagation=Propagation.NEVER)
    必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)

  • @Transactional(propagation=Propagation.SUPPORTS)
    如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.

事物超时设置:
 @Transactional(timeout=30) //默认是30秒

事务隔离级别:

  • @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    读取未提交数据(会出现脏读, 不可重复读) 基本不使用

  • @Transactional(isolation = Isolation.READ_COMMITTED)
    读取已提交数据(会出现不可重复读和幻读)

  • @Transactional(isolation = Isolation.REPEATABLE_READ)
    可重复读(会出现幻读)

  • @Transactional(isolation = Isolation.SERIALIZABLE)
    串行化

MYSQL:默认为REPEATABLE_READ级别
SQLSERVER:默认为READ_COMMITTED

脏读 : 一个事务读取到另一事务未提交的更新数据。
不可重复读 : 在同一事务中, 多次读取同一数据返回的结果有所不同。后续读取可以读到另一事务已提交的更新数据。
可重复读:在同一事务中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据。
幻读 : 一个事务读到另一个事务已提交的insert数据。

@Transactional注解中常用参数说明

参数名称 功能描述
readOnly 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)
rollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class);指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
rollbackForClassName 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName="RuntimeException");指定多个异常类名称:@Transactional(rollbackForClassName{"RuntimeException","Exception"})
noRollbackFor 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class);指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称@Transactional(noRollbackForClassName="RuntimeException")

指定多个异常类名称:

@Transactional(noRollbackForClassName={"RuntimeException","Exception"})

参数名称 功能描述
propagation 该属性用于设置事务的传播行为,具体取值可参考表6-7。例@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
isolation 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
timeout 该属性用于设置事务的超时秒数,默认值为-1表示永不超时

注意的几点:

1、@Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能.

2、用 spring 事务管理器,由spring来负责数据库的打开,提交,回滚.默认遇到运行期例外(throw new RuntimeException("注释");)会回滚,即遇到不受检查(unchecked)的例外时回滚;而遇到需要捕获的例外(throw new Exception("注释");)不会回滚,即遇到受检查的例外(就是非运行时抛出的异常,编译器会检查到的异常叫受检查例外或说受检查异常)时,需我们指定方式来让事务回滚要想所有异常都回滚。
要加上 @Transactional( rollbackFor={Exception.class,其它异常}) 。如果让unchecked例外不回滚:@Transactional(notRollbackFor=RunTimeException.class)如下:

@Transactional(rollbackFor=Exception.class) 
//指定回滚,遇到异常Exception时回滚
public void methodName() { 
throw new Exception("注释"); 
}

@Transactional(noRollbackFor=Exception.class)
//指定不回滚,遇到运行期例外(throw new RuntimeException("注释");)会回滚
public ItimDaoImpl getItemDaoImpl() { 
throw new RuntimeException("注释"); 
}

3、@Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。

4、@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。然而,请注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据,能够被可以识别 @Transactional 注解和上述的配置适当的具有事务行为的beans所使用。上面的例子中,其实正是 元素的出现 开启 了事务行为。

5、Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。你当然可以在接口上使用 @Transactional 注解,但是这将只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(将被确认为严重的)。因此,请接受Spring团队的建议并且在具体的类上使用 @Transactional 注解。

嵌套事务示例

  • Propagation.REQUIRED+Propagation.REQUIRES_NEW

@Service 
public class ServiceAImpl implements ServiceA {

    @Autowired private ServiceB serviceB;
    @Autowired private VcSettleMainMapper vcSettleMainMapper;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
    public void methodA() {
        String id = IdGenerator.generatePayId("A");
        VcSettleMain vc = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc);
        System.out.println("ServiceAImpl VcSettleMain111:" + vc);

        serviceB.methodB();  

        VcSettleMain vc2 = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc2);
        System.out.println("ServiceAImpl VcSettleMain22222:" + vc2);
    } 

    private VcSettleMain buildModel(String id) {
        VcSettleMain vc = new VcSettleMain();
        vc.setBatchNo(id);
        vc.setCreateBy("dxz");
        vc.setCreateTime(LocalDateTime.now());
        vc.setTotalCount(11L);
        vc.setTotalMoney(BigDecimal.ZERO);
        vc.setState("5"); return vc;
    }

}

ServiceB

@Service public class ServiceBImpl implements ServiceB {

    @Autowired private VcSettleMainMapper vcSettleMainMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false) 
    public void methodB() {
        String id = IdGenerator.generatePayId("B");
        VcSettleMain vc = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc);
        System.out.println("---ServiceBImpl VcSettleMain:" + vc);
    }
}

controller

@RestController
@RequestMapping("/demo")
public class Demo1 {
    @Autowired 
    private ServiceA serviceA; 
    /** * 嵌套事务测试 */ 
    @PostMapping(value = "/test1") 
    public String methodA() throws Exception {
        serviceA.methodA(); 
        return "ok";
    }
}

结果:

image

看数据库表记录:

image

这种情况下, 因为 ServiceB#methodB 的事务属性为 PROPAGATION_REQUIRES_NEW,ServiceB是一个独立的事务,与外层事务没有任何关系。如果ServiceB执行失败(上面示例中让ServiceB的id为已经存在的值),ServiceA的调用出会抛出异常,导致ServiceA的事务回滚。

并且, 在 ServiceB#methodB 执行时 ServiceA#methodA 的事务已经挂起了 (关于事务挂起的内容已经超出了本文的讨论范围)。

  • Propagation.REQUIRED+Propagation.REQUIRED

//ServiceA //...
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
public void methodA() { 

  //ServiceB //...
 @Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
  public void methodB(String id) { 
    //...
  }
}

--“1”可插入,“2”可插入,“3”不可插入:

结果是“1”,“2”,“3”都不能插入,“1”,“2”被回滚。

--“1”可插入,“2”不可插入,“3”可插入:

结果是“1”,“2”,“3”都不能插入,“1”,“2”被回滚。

  • Propagation.REQUIRED+无事务注解

//ServiceA //...
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
public void methodA() { 

  //ServiceB //...
  //没有加事务注解
  public void methodB(String id) {
     //..
  }
}

--“1”可插入,“2”可插入,“3”不可插入:

结果是“1”,“2”,“3”都不能插入,“1”,“2”被回滚。

2.4、内层事务被try-catch:

try-catch + Propagation.REQUIRED+Propagation.REQUIRED

//ServiceA //...
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
public void methodA() { 
  try {
    serviceB.methodB(id);
  } catch (Exception e) {
    System.out.println("内层事务出错啦。");
  }
} 

//ServiceB //...
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
public void methodB(String id) {
   //...
}

--“1”可插入,“2”不可插入,“3”可插入:

结果是“1”,“2”,“3”都不能插入,“1”被回滚。

事务设置为Propagation.REQUIRED时,如果内层方法抛出Exception,外层方法中捕获Exception但是并没有继续向外抛出,最后出现“Transaction rolled back because it has been marked as rollback-only”的错误。外层的方法也将会回滚。

其原因是:内层方法抛异常返回时,transacation被设置为rollback-only了,但是外层方法将异常消化掉,没有继续向外抛,那么外层方法正常结束时,transaction会执行commit操作,但是transaction已经被设置为rollback-only了。所以,出现“Transaction rolled back because it has been marked as rollback-only”错误。

  • try-catch + Propagation.REQUIRED+Propagation.NESTED

//ServiceA //...
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) 
public void methodA() { 
  try {
    serviceB.methodB(id);
  } catch (Exception e) {
    System.out.println("内层事务出错啦。");
  }
}

 //ServiceB //...
@Transactional(propagation = Propagation.NESTED, readOnly = false) 
public void methodB(String id) { 
  //...
}

--“1”可插入,“2”不可插入,“3”可插入:

结果是“1”,“3"记录插入成功,“2”记录插入失败。

说明:

当内层配置成 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? 从 Juergen Hoeller 的原话中我们可以找到答案, ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint(注意, 这是本文中第一次提到它, 潜套事务中最核心的概念), 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:

  • 内层失败,外层调用其它分支。
ServiceA { 
  // 事务属性配置为 PROPAGATION_REQUIRED 
  void methodA() { 
    try {  
      ServiceB.methodB();  
    } catch (SomeException) { 
      // 执行其他业务, 如 ServiceC.methodC(); 
   }  
}  

这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 不会产生脏数据(相当于此方法从未执行过), 该特性可用在某些特殊的业务中, PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。

  • 代码不做任何修改, 那么如果内部事务(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback。

三、嵌套事务总结

使用嵌套事务的场景有两点需求:

  • 需要事务BC与事务AD一起commit,即:作为事务AD的子事务,事务BC只有在事务AD成功commit时(阶段3成功)才commit。这个需求简单称之为“联合成功”。这一点PROPAGATION_NESTED和PROPAGATION_REQUIRED可以做到。
  • 需要事务BC的rollback不(无条件的)影响事务AD的commit。这个需求简单称之为“隔离失败”。这一点PROPAGATION_NESTED和PROPAGATION_REQUIRES_NEW可以做到。

分解下,可知PROPAGATION_NESTED的特殊性有:

1、使用PROPAGATION_REQUIRED满足需求1,但子事务BC的rollback会无条件地使父事务AD也rollback,不能满足需求2。即使对子事务进行了try-catch,父事务AD也不能commit。示例见2.4.1、trycatch+Propagation.REQUIRED+Propagation.REQUIRED

2、使用PROPAGATION_REQUIRES_NEW满足需求2,但子事务(这时不应该称之为子事务)BC是完全新的事务上下文,父事务(这时也不应该称之为父事务)AD的成功与否完全不影响BC的提交,不能满足需求1。

同时满足上述两条需求就要用到PROPAGATION_NESTED了。PROPAGATION_NESTED在事务AD执行到B点时,设置了savePoint(关键)。

当BC事务成功commit时,PROPAGATION_NESTED的行为与PROPAGATION_REQUIRED一样。只有当事务AD在D点成功commit时,事务BC才真正commit,如果阶段3执行异常,导致事务AD rollback,事务BC也将一起rollback ,从而满足了“联合成功”。

当阶段2执行异常,导致BC事务rollback时,因为设置了savePoint,AD事务可以选择与BC一起rollback或继续阶段3的执行并保留阶段1的执行结果,从而满足了“隔离失败”。

当然,要明确一点,事务传播策略的定义是在声明或事务管理范围内的(首先是在EJB CMT规范中定义,Spring事务框架补充了PROPAGATION_NESTED),编程式的事务管理不存在事务传播的问题。

四、PROPAGATION_NESTED的必要条件

上面大致讲述了潜套事务的使用场景, 下面我们来看如何在 spring 中使用 PROPAGATION_NESTED, 首先来看 AbstractPlatformTransactionManager

// Create a TransactionStatus for an existing transaction. 
private TransactionStatus handleExistingTransaction(  
        TransactionDefinition definition, Object transaction, 
        boolean debugEnabled) throws TransactionException {  
   
  ... 省略

 if (definition.getPropagationBehavior() == 
    TransactionDefinition.PROPAGATION_NESTED) {
   if (!isNestedTransactionAllowed()) { 
      throw new NestedTransactionNotSupportedException( "Transaction 
          manager does not allow nested transactions by default - " +  
          "specify 'nestedTransactionAllowed' property with value 'true'");  
   } 

    if (debugEnabled) {  
      logger.debug("Creating nested transaction with name 
        [" + definition.getName() + "]");  
     }

     if (useSavepointForNestedTransaction()) {
         // Create savepoint within existing Spring-managed transaction, 
         // through the SavepointManager API implemented by TransactionStatus. 
         // Usually uses JDBC 3.0 savepoints. 
        // Never activates Spring synchronization. 

        DefaultTransactionStatus status = newTransactionStatus(definition, 
          transaction, false, false, debugEnabled, null);  

        status.createAndHoldSavepoint(); 
      return status;  
    } else { 
      // Nested transaction through nested begin and commit/rollback calls. 
      // Usually only for JTA: Spring synchronization might get activated here 
      // in case of a pre-existing JTA transaction. 
      doBegin(transaction, definition); 

      boolean newSynchronization = (this.transactionSynchronization != 
        SYNCHRONIZATION_NEVER); 

      return newTransactionStatus(definition, transaction, true, 
        newSynchronization, debugEnabled, null);  
      }  
   }  
} 
  • 我们要设置 transactionManager 的 nestedTransactionAllowed 属性为 true, 注意, 此属性默认为 false!!! **
    再看 AbstractTransactionStatus#createAndHoldSavepoint() 方法
// Create a savepoint and hold it for the transaction. 
// @throws org.springframework.transaction.NestedTransactionNotSupportedException 
// if the underlying transaction does not support savepoints 
public void createAndHoldSavepoint() throws TransactionException {  
    setSavepoint(getSavepointManager().createSavepoint());  
} 

可以看到 Savepoint 是 SavepointManager.createSavepoint 实现的, 再看 SavepointManager 的层次结构, 发现其 Template 实现是JdbcTransactionObjectSupport, 常用的 DatasourceTransactionManager, HibernateTransactionManager 中的 TransactonObject 都是它的子类 :
JdbcTransactionObjectSupport 告诉我们必须要满足两个条件才能 createSavepoint :

  • java.sql.Savepoint 必须存在, 即 jdk 版本要 1.4+ **

  • Connection.getMetaData().supportsSavepoints() 必须为 true, 即 jdbc drive 必须支持 JDBC 3.0 **
    确保以上条件都满足后, 你就可以尝试使用 PROPAGATION_NESTED 了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 事务的嵌套概念 所谓事务的嵌套就是两个事务方法之间相互调用。spring事务开启 ,或者是基于接口的或者是基于类的...
    jackcooper阅读 5,274评论 0 10
  • 事务接口定义 在Spring中,事务是通过TransactionDefinition接口定义的。其中定义了访问事务...
    追梦人Plus阅读 4,837评论 0 12
  • 概念 为了保证对于数据库的每一步操作都是可靠的,即使出现了异常情况,也不至于破坏数据的完整性。 事务可以在不同的S...
    意大利大炮阅读 2,649评论 0 1
  • 1 事务 1.1 事务管理方式 spring支持编程式事务管理和声明式事务管理两种方式。 编程式事务管理使用Tra...
    鑫奕航阅读 7,809评论 0 1
  • 䒕䒕猪阅读 1,148评论 0 3

友情链接更多精彩内容