2 @Transcation注解

@Transcation注解保证的事务是数据库的操作,要与分布式事务(比如Seata)保证的rpc调用的事务区分开。即@Transcation注解作用的方法,方法体是插入表数据、删除表数据这些数据库表的操作,而分布式事务Seata保证的是调用若干服务的接口结果的事务性。

2.1 使用

@Transactional 可以作用在接口、类、类方法:

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息;
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息;
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。

@Transactional注解修饰的方法,保证了操作的原子性的。

2.3 注解失效的6种场景

在实际编码中,有时我们使用@Transactional注解并没有生效,总结一下可能是以下原因导致:

  1. @Transactional注解作用在了非public修饰的方法上;
  2. @Transactional 注解属性 propagation 设置错误,错误地设置成了如下三种之一:TransactionDefinition.PROPAGATION_SUPPORTS、TransactionDefinition.PROPAGATION_NOT_SUPPORTED 和T ransactionDefinition.PROPAGATION_NEVER;
  3. @Transactional 注解属性 rollbackFor 设置错误;
  4. 同一个类中没有被@Transactional修饰的方法调用了该类中另一个被@Transactional修饰的方法,导致@Transactional失效;
  5. 异常被你的 catch“吃了”导致@Transactional失效;
  6. 数据库引擎不支持事务。 其中1、4、5是比较容易出错的场景。

2.3.1 @Transactional注解作用在了非public修饰的方法上

如果@Transactional注解应用在非public 修饰的方法上,Transactional将会失效;protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错。

之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

2.3.2 @Transactional 注解属性 propagation 设置错误

propagation属性是Spring事务传播机制,支持7种事务传播设置,其中下面3种事务传播机制的设置,会使@Transactional失效:

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行; TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起; TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。 其实这种propagation属性设置错误导致@Transactional失效的概率也比较低,因为开发过程中我们基本会将propagation属性缺省,此时@Transactional的事务默认是开启的TransactionDefinition.PROPAGATION_REQUIRED,也是支持事务的。

2.3.3 @Transactional 注解属性 rollbackFor 设置错误

@Transactional注解中的rollbackFor参数可以指定能够触发事务回滚的异常类型。Spring默认抛出了非受查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。一般编码中会加个@Transactional(rollbackFor = Exception.class),意思是任何异常都会回滚。

2.3.4 同一个类中的方法调用了该类中另一个被@Transactional修饰的方法

这种失效场景是经常发生的。开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它有一个方法A,且方法A没有声明注解事务,方法A中再调用本类的方法B(不论方法B是用public还是private修饰),且B方法有声明注解事务,则外部调用方法A之后,方法B的事务是不会起作用的。

上述@Transactional注解失效的原因还是Spring的AOP导致的。原因:spring 在扫描bean的时候会扫描方法上是否包含@Transactional注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就不会启动transaction,我们看到的现象就是@Transactional注解无效。

为什么一个方法a()调用同一个类中另外一个方法b()的时候,b()不是通过代理类来调用的呢?可以看下面的例子(为了简化,用伪代码表示):

@Service
class A{
    @Transactinal
    method b() {...}
    
    method a() {    //标记1
        b();
    }
}
 
//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:
class proxy$A{
    A objectA = new A();
    method b() {    //标记2
        // 开启事务
        startTransaction();
        objectA.b();
    }
 
    method a() {    //标记3
        objectA.a();    //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
    }
}

当我们调用A的bean的a()方法的时候,也是被proxy 拦截,执行 A.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。

这个问题可以被推广到一般性:在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解是不会生效的。

2.3.5 异常被你的 catch“吃了”导致@Transactional失效

这种场景也是会经常发生的。被@Transactional注解修饰的方法中,如果存在try-catch子句,且在try块中存在需要保证原子性的操作,则此时如果第一个操作成功,第二个操作抛出异常,则第一个操作仍然会被执行,即此时@Transactional注解会失效。

@Transactional
private Integer A() throws Exception {
    int insert = 0;
    try {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName("2");
        cityInfoDict.setParentCityId(2);
        
        // 第一个操作
        insert = cityInfoDictMapper.insert(cityInfoDict);
    
        // 第二个操作,且insertB中会抛异常
        b.insertB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

此时这两个操作并不会保证原子性,即@Transactional注解失效。原因是:第二个b.insertB()操作抛出异常后,b对应的服务ServiceB表示当前事务需要回滚,但是在外层服务中由于手动捕获了这个异常并进行了处理,外层服务认为当前事务已经捕获并进行了处理,当前事务是可以进行正常提交的,因此会出现第一个操作被提交并成功执行了,而第二个操作由于抛异常并没有成功执行,表现为整体的方法的@Transactional注解失效。

2.3.6 数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

2.4 注解参数

@Transactional 注解属性说明:

参数名

说明

value

当在xml配置文件中配置多个TransactionManager的时候,可以指定使用哪个事务管理器

propagation

事务的传播行为,默认为Propagation.REQUIRED(表示启动事务)。PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务,如果没有事务,则以非事务的方式继续进行。 PROPAGATION_NOT_SUPPORTED:以非事务的方法运行,如果当前存在事务,则将事务挂起。PROPAGATION_NEVER:以非事务的方法运行,如果当前存在事务,则抛出异常。

isolation

事务的隔离等级,默认为Isolation.DEFAULT。必须返回 TransactionDefinition 接口上定义的ISOLATION_XXX 常量之一。只有结合PROPAGATION_REQUIRED 或者 PROPAGATION_REQUIRES_NEW 一起声明才有意义。

timeout

默认值为 -1(不超时),单位秒。表示事务必须在规定的时间内处理完成,否则超时。

readOnly

默认false。该事务是否只读。

rollbackFor

用于指定能够触发回滚的异常类型。多个类型以,(英文逗号)隔开。

rollbackForClassName

定义异常的名字,这些异常会触发回滚机制。多个类型以,(英文逗号)隔开。

noRollbackFor

抛出异常,不回滚。多个类型以,(英文逗号)隔开。

noRollbackForClassName

定义异常的名字,抛出异常,不回滚。多个类型以,(英文逗号)隔开。

2.4.1 timeout 属性

为了使一个应用程序很好地执行,它的事务不能运行太长时间。假设事务的运行时间变得格外的长,由于事务涉及对数据库表的锁定,所以长时间运行的事务会不必要地占用数据库资源。因此对事务有超时时间设定的需求。

@Transactional注解中的参数timeout即事务的超时时间,默认值为 -1。如果超过该时间但事务还没有完成,则自动回滚事务。

2.4.2 readOnly属性

readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

3 Spring事务的传播机制

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。

Spring中常用的事务传播机制有7种,如下:

  • PROPAGATION_REQUIRED: Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚;如果外层没有事务,新建一个事务执行。
  • PROPAGATION_REQUES_NEW: 该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可。
  • PROPAGATION_SUPPORT: 如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务。
  • PROPAGATION_NOT_SUPPORT: 该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,再恢复外层事务,无论是否异常都不会回滚当前的代码。
  • PROPAGATION_NEVER: 该传播机制不支持外层事务,即如果外层有事务就抛出异常。
  • PROPAGATION_MANDATORY: 与NEVER相反,如果外层没有事务,则抛出异常。
  • PROPAGATION_NESTED: 该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。 在Spring中的@Transactional注解中可以通过propagation参数来指定开启上面哪一种事务传播机制,比如:@Transactional(propagation=Propagation.REQUIRED),实际开发中propagation参数一般是缺省的,即默认开启的是PROPAGATION_REQUIRED机制。

4 Spring事务的隔离级别

1.4小节中介绍了sql中的事务隔离,Spring事务的本质是基于底层数据库事务,而数据库事务本质是通过数据库锁(表锁、行锁、乐观锁等)保证的,所以Spring事务可以理解为是对底层数据库事务的一层封装。

Spring中声明事务隔离级别是通过@Transactional注解中的isolation参数, 比如:@Transactional(isolation = Isolation.READ_UNCOMMITTED),实际开发中isolation参数一般也是缺省的,默认开启的是ISOLATION_DEFAULT级别,即数据库默认的事务隔离级别,MySql的默认事务隔离级别是REPEATABLE READ(不可重复读)。

以下是Spring中支持的5种事务隔离级别:

常量

解释

ISOLATION_DEFAULT

这是个 Spring 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。

ISOLATION_READ_UNCOMMITTED

事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。

ISOLATION_READ_COMMITTED

保证一个事务修改的数据提交后才能被另外一个事务读取。可以避免脏读,但不能避免不可重复读和幻读。

ISOLATION_REPEATABLE_READ

这种事务隔离级别可以避免脏读,不可重复读,但不能避免幻读。

ISOLATION_SERIALIZABLE

这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。可以避免脏读、不可重复读和幻读,但也是所有隔离级别中最慢的,因为它通常是通过完全锁定当前事务所涉及的数据库的表锁来完成的。

当Spring中的事务隔离级别与底层数据库事务隔离级别不同时,Spring中的代码逻辑还是按照Spring的事务隔离级别来。