Transactional注解介绍和使用总结

2,001 阅读5分钟

在日常工作中经常会用到这个注解,但只会简单的使用,知其然而不知其所以然。 因此写这篇文章对@Transactional注解进行总结。

1、spring 事务管理

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

编程式事务管理即使用代码手动的进行事务的提交和回滚等操作。

声明式事务管理是建立在AOP之上的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

@Transactional注解就是一种声明式事务管理的常用实现方式。

2、@Transactional注解属性

image.png

Propagation

propagation 代表事务的传播行为,即多个嵌套事务的情形下,如何管理事务。共有以下6种传播行为:

  • Propagation.REQUIRED是传播行为的默认值,如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。*(使用@Transactional(propagation = Propagation.SUPPORTS)和不加@Transactional 有什么区别?)
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  • Propagation.REQUIRES_NEW: 重新创建一个新的事务,如果当前存在事务,就将这个事务挂起,直到新的事务提交。(Spring事务传播机制Propagation.REQUIRES_NEW详解及测试)
  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
  • Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

isolation

事务的隔离界别,是指若干个并发的事务之间的隔离程度,包括以下:

  • Isolation.DEFAULT默认值,表示使用数据库默认隔离级别。(mysql repeatable;sqlserver readcommit)
  • Isolation.READ_UNCOMMITTED: 读未提交,表示事务可以读取其他事务修改但还未提交的数据,该隔离级别可能会造成脏读
  • Isolation.READ_COMMITTED读已提交,表示一个事务只能读取另一个事务已经提交的数据,可以防止脏读,但可能会出现幻读和不可重复读
  • Isolation.REPEATABLE_READ可重复读,该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该隔离级别可以防止脏读和不可重复读,但可能会出现幻读。
  • Isolation.SERIALIZABLE:最高隔离界别,所有事务顺序执行,可以防止脏读、不可重复读、幻读。但性能也最差。 | 隔离级别 | 脏读 | 不可重复读 | 幻影读 | | :--------------: | :--: | :--------: | :----: | | READ-UNCOMMITTED | √ | √ | √ | | READ-COMMITTED | × | √ | √ | | REPEATABLE-READ | × | × | √ | | SERIALIZABLE | × | × | × |

这里只对isolation属性的几种隔离级别进行简单介绍,想了解隔离级别详细内容,请原理可以其他文章。

timeout

事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly

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

rollbackFor

用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

默认情况下,只对非受检异常(RunTimeExceptionError等))进行回滚,受检异常(包括SQLException)不进行回滚。

noRollbackFor

rollbackFor属性相反,制定不触发事务回滚的异常类型。

附Transactional注解源码(spring 5.2.8):

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

2、使用注意事项

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

虽然@Transactional 使用很简便,但日常使用中,还是需要注意一些细节,不然会导致@Transactional 失效,这里总结常见几种@Transactional 失效的情形:

2.1 常见的几种失效情形

@Transactional 应用在非public 修饰的方法上

原因:spring通过 AOP代理对事务进行管理时,会检查方法是否为public修饰符,不是 public则不会获取@Transactional 的属性配置信息。

同一个类中, 一个nan-transactional的方法去调用transactional的方法, 事务会失效

原因Spring采用动态代理(AOP)实现对事务的管理,在初始化扫描时,会为带有Transactional注解的类动态生成一个代理对象,当带有Transactional注解的方法被调用时,实际是通过代理类,但只有在代理对象之间进行调用时,可以触发切面逻辑。

而在同一个class中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象。所以Spring无法切到这次调用,也就无法通过注解保证事务性了。

ps: 同一个class中,调用方法带有Transactional注解,被调用方法不带用Transactional注解,事务是可以生效的

public void doTheThing() {   
 // ...    actuallyDoTheThing(); // Noncompliant 
 }
 @Transactional 
 public void actuallyDoTheThing() { 
 // ...  
 }  

解决办法:

  • 1、同一个类中,调用方法带上@Transactional注解
  • 2、通过代理对象调用

try catch 块中,异常没有被再次抛出

常见的一种失效情景,异常被catch了,没有抛出到切面层。

@Transactional
public void doSomething(){
    try{
        //
    }catch(exception){
       // 没有再次抛出异常   
    }
}

rollbackFor 属性设置错误

@Transactional注解默认只对非受检异常进行回滚,SqlException等异常属于受检异常,如果不在 rollbackFor属性中指定,当抛出受检异常时,事务也不会回滚。

参考