今天分析spring 声明式事务 @Transactional :

事务管理在系统开发中是不可缺少的一部分,Spring提供了很好事务管理机制,主要分为编程式事务和声明式事务两种。

而声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或配置的XML 来管理。

A、@Transactional 注解直接添加到需要的方法和类上面

//默认配置下 Spring 只会回滚运行时、未检查的异常(继承自 RuntimeException 的异常)或者 Error。
// checked 的异常或数据库操作的异常不回滚
@Transactional

public void testTra() {

     // 事务相关操作

}

B、xml方式配置开启:

<tx:annotation-driven />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

1、声明式事务的优点:

通过上面的例子,其实我们可以很容易的看出来,声明式事务帮助我们节省了很多代码,他会自动帮我们进行事务的开启、提交以及回滚等操作,把程序员从事务管理中解放出来。

声明式事务管理使用了 AOP 实现的,本质就是在目标方法执行前后进行拦截。 在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。

使用这种方式,对代码没有侵入性,方法内只需要写业务逻辑就可以了。

但是,声明式事务也有一定的弊端。

2、声明式事务的粒度问题:

首先,声明式事务有一个局限,那就是他的最小粒度要作用在方法上(也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法)。

因为声明式事务是通过注解的(有些时候还可以通过配置实现),这就会导致一个问题,那就是这个事务有可能被开发者忽略。

事务被忽略了有什么问题呢?

首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。

我们知道,这些操作如果被包在事务中,有两个问题:

1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了。

2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。

有些时候,即使没有在事务中进行远程操作,但是有些人还是可能会不经意的进行一些内存操作,如运算。或者如果遇到分库分表的情况,有可能不经意间进行跨库操作。

但是如果是编程式事务的话,业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。这样有人改这段代码的时候,就会强制他考虑要加的代码是否应该方法事务内。

3、因此,使用声明式事务时要特别小心,保证团队里的每个人都知道,并熟悉其原理,否则 可能会存在潜在的故障;同时 建议大家使用编程式事务,通过规则和机制保证系统的稳定性。

声明式事务用不对容易失效:

除了事务的粒度问题,还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码,但是如果没用对,也很容易导致事务失效。

如以下几种场景就可能导致声明式事务失效:

1、@Transactional 应用在非 public 修饰的方法上(只有@Transactional 注解应用到 public 方法,才能进行事务管理),错误事例如下:

// protected 或   private 修饰的方法,事务不生效
 @Transactional(rollbackFor=Exception.class)
    private void saveRelation(){
        //批量保存接口
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口
        catalogInfoMapper.insertSelective(catalogInfo);
        //抛出异常
        if(serviceDataRelation.getServiceId()>25){
            throw new RuntimeException("出现异常");
        }
    }

 目前如果非 public 修饰的方法上加上事务, idea 有红线提示

java 声明式事务 spring 声明式事务_java

原因:这是因为在使用 Spring AOP 代理时,Spring 在调用 TransactionInterceptor 在目标方法执行前后进行拦截之前,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource(Spring 通过这个类获取 @Transactional 注解的事务属性配置属性信息)的 computeTransactionAttribute 方法。 

/**
* 这个方法会检查目标方法的修饰符是不是 public,若不是 public,就不会获取@Transactional 的属性配置 
  信息,最终会造成不会用 TransactionInterceptor 来拦截该目标方法进行事务管理。
*/
protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;}

2、@Transactional 注解属性 propagation 设置错误,设置事务的传播属性:

事物传播行为介绍: 

  @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 默认方式 Propagation.REQUIRED,如果设置错误,事务可能失效。

错误方式:

注意下面三种 propagation 可以不启动事务。本来期望目标方法进行事务管理,但若是错误的配置这三种 propagation,事务将不会发生回滚。

  1. TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  2. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  3. TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3、@Transactional 注解属性 rollbackFor 设置错误,某些场景错误事例

/**
* @Transactional 默认回滚设置是 (rollbackFor=RuntimeException.class),如果出现的不是RuntimeException或其子类类型的异常,则不会抛出。
/ 
@Transactional
    pubilc void saveRelation(){
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口...省略其他业务代码
        catalogInfoMapper.insertSelective(catalogInfo);
       
    }

4、同一个类中方法调用,导致@Transactional失效,多种情况均分析一下:

A、生效情况:

//在方法入口处加事务(必须加)
    @Transactional(rollbackFor=Exception.class)
    @Override
    public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
        //调用其他方法操作
        saveRelation(setCatalogModel);
    }

public的方法如下:

//这里不加事务(加上也没有副作用)   
pubilc void saveRelation(){
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口...省略其他业务代码
        catalogInfoMapper.insertSelective(catalogInfo);
       
    }

B、生效情况:

//在方法入口处加事务(必须加)
    @Transactional(rollbackFor=Exception.class)
    @Override
    public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
         
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
        //调用其他方法操作
        saveRelation(setCatalogModel);
    }

public 方法:

//这里不加事务(加上也没有副作用)   
pubilc void saveRelation(){
        
         //单个保存接口...省略其他业务代码
        catalogInfoMapper.insertSelective(catalogInfo);
       
    }

C、注意:不生效情况:

//在方法入口处没有加事务
    @Override
    public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
        //调用其他方法操作
        saveRelation(setCatalogModel);
    }

 public的方法如下:

//这里加事务(此处加不生效)  
 @Transactional(rollbackFor=Exception.class) 
pubilc void saveRelation(){
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口...省略其他业务代码
        catalogInfoMapper.insertSelective(catalogInfo);
       
    }

原因:@Transactional注解底层使用的是动态代理来进行实现的,如果在调用本类中的方法,此时不添加@Transactional注解,而是在调用类中使用thisi调用本方法中的另外一个方法添加了@Transactional注解,此时调用的方法上的@Transactional注解是不起作用的。

5、异常被catch捕获导致@Transactional失效

A、调用字方法 失效场景:

//在方法入口处加事务(必须加)
    @Transactional(rollbackFor=Exception.class)
    @Override
    public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
        //调用其他方法操作
        saveRelation(setCatalogModel);
    }

public方法:

//这里业务有加 catch  
pubilc void saveRelation(){
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口...省略其他业务代码
        
        try{ 
            catalogInfoMapper.insertSelective(catalogInfo);
            int a = 1/0;//抛异常的点
        }catch (Exception e){
            e.printStackTrace();
        }
       
    }

B、本方法失效场景:

//在方法入口处加事务 
    @Transactional(rollbackFor=Exception.class)
    @Override
    public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
        //批量保存接口...省略其他业务代码
        serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
         //单个保存接口...省略其他业务代码
        
        try{ 
            catalogInfoMapper.insertSelective(catalogInfo);
            int a = 1/0;//抛异常的点
        }catch (Exception e){
            e.printStackTrace();
        }
    }

分析:事务的回滚是方法发生异常时进行的,在aop的异常通知中进行拦截,回滚;如果方法中捕获了异常,是不会被aop的异常通知拦截到的。

如果使用了try catch捕获异常,需要在catch中抛出一个异常或者在catch中通过

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() // 设置手动回滚。

 另外,如果你系统做了异常的统一捕获(自定义切面异常),那么就禁止使用 声明式事务吧!

6、数据库引擎不支持事务

有些数据库不支持事务,那么无论你怎么加都不会生效。而以上几个事例是采用的mysql 数据库,肯定是支持事务的。

 但是我还是那句话,我们确实无法保证所有人的能力都很高,也无法要求所有开发者都能不出错。我们能做的就是,尽量可以通过机制或者规范,来避免或者降低这些问题发生的概率。

其实,如果大家有认真看过阿里巴巴出的那份Java开发手册的话,其实就能发现,其中的很多规约并不是完完全全容易被人理解,有些也比较生硬,但是其实,这些规范都是从无数个坑里爬出来的开发者们总结出来的。本文只是建议大家日后在使用事务的时候,能够考虑到本文中提到的观点,然后自行选择。