事务管理

事务管理是应用系统开发中必不可少的一部分,Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编程式声明式的两种方式:

  • 编程事务:指的是通过编码方式实现事务
  • 声明式事务:基于 AOP将具体业务逻辑与事务处理解耦(声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多)
    • 配置文件(xml)中做相关的事务规则声明
    • @Transactional 注解的方式

      编程式事务

      编程式事务:是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:
      1. try {
      2. //TODO something
      3. transactionManager.commit(status);
      4. } catch (Exception e) {
      5. er transactionManager.rollback(status);
      6. thrownew InvoiceApplyException("异常失败");//手动
      7. }

      声明式事务

      配置文件(xml)形式

      @Transactional 注解形式

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

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常
默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。

小结:

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

@Transactional 使用

注意:
SpringBoot项目会自动配置一个 DataSourceTransactionManager,所以我们只需在方法(或者类)加上 @Transactional 注解,就自动纳入 Spring 的事务管理了
测试代码:
@Transactional注解的方法addUser(),并且调用了另一个隔离级别为NESTED的addUser2()方法

  1. @Service
  2. public class UserService {
  3. // .. 省略其他
  4. @Transactional
  5. public void addUser() {
  6. userMapper.addItem("nA", "cA");
  7. // 调用的addUser2()方法是嵌套模式
  8. ((UserService) applicationContext.getBean(UserService.class)).addUser2();
  9. userMapper.addItem("nB", "cB");
  10. }
  11. @Transactional(propagation = Propagation.NESTED)
  12. public void addUser2() {
  13. userMapper.addItem("nC", "cC");
  14. }
  15. }

抛出异常之后,事务会自动回滚,数据不会插入到数据库。

执行时序图

2e5a628853e94c5ca0b71294728e0c97_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp

Spring事务的默认回滚机制

Spring的事务管理默认是针对 unchecked exception回滚,也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚,且必须抛出异常,若使用try-catch对其异常捕获则不会进行回滚!(Error异常和RuntimeException异常抛出时不需要方法调用throws或try-catch语句);而 checked exception则必须用try语句块进行处理或者把异常交给上级方法处理总之就是必须写代码处理它,所以必须在service捕获异常,然后再次抛出,这样事务方才起效。

JAVA 异常

  • Java标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类。
  • Throwable又派生出 Error 类和 Exception 类。

    错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

20180503233630408.png
注意:上图中有个错误 - ClassNotFoundException不属于运行时异常!

总体上我们根据Javac对异常的处理要求,将异常类分为2类:

  • 非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类

    javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。

  • 检查异常(checked exception):除了Error 和 RuntimeException的其它异常

    javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。

@Transactional 注解属性

  1. value 和 transactionManager 属性

    它们两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器

  2. propagation

  • 事务的传播行为**Propagation.REQUIRED**[默认值]
  • 可选参数:

    • Propagation.REQUIRED Propagation.NESTED

      如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务 例如: a方法和b方法都添加了注解,使用默认传播模式,则a方法内部调用b方法,会把两个方法的事务合并为一个事务。 注意: 这里又会存在问题,如果b方法内部抛了异常,而a方法catch了b方法的异常,那这个事务还能正常运行吗? 答案:是不行!会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,因为当ServiceB中抛出了一个异常以后,ServiceB会把当前的transaction标记为需要rollback。但是ServiceA中捕获了这个异常,并进行了处理,认为当前transaction应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException

    • Propagation.SUPPORTS

      如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行

    • Propagation.MANDATORY

      如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常

    • Propagation.REQUIRES_NEW

      重新创建一个新的事务,如果当前存在事务,暂停当前的事务。 这个属性可以实现: 类A中的a方法加上默认注解@Transactional(propagation = Propagation.REQUIRED),类B中的b方法加上注解@Transactional(propagation = Propagation.REQUIRES_NEW),然后在a方法中调用b方法操作数据库,再在a方法最后抛出异常,会发现a方法中的b方法对数据库的操作没有回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务。

    • Propagation.NOT_SUPPORTED

      以非事务的方式运行,如果当前存在事务,暂停当前的事务

    • Propagation.NEVER

      以非事务的方式运行,如果当前存在事务,则抛出异常

  1. isolation
  • 事务的隔离级别,默认值: Isolation.DEFAULT
  • 可选的值:
    • DEFAULT:使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
    • READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
    • READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
    • REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
      1. public enum Isolation {
      2. DEFAULT(-1),
      3. READ_UNCOMMITTED(1), // 读未提交
      4. READ_COMMITTED(2), // 读已提交
      5. REPEATABLE_READ(4), // 可重复读
      6. SERIALIZABLE(8); // 可串行化
      7. }
  1. timeout

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

  2. readOnly

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

  3. rollbackFor

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

  4. noRollbackFor

    抛出指定的异常类型,不回滚事务,也可以指定多个异常类型

@Transactional失效

不正确的使用事务Transactional注解,会导致事务失效

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

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

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

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。【运行程序不易被发现,事务不一致问题】

  1. @Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  1. @Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性,如果未指定 rollbackFor 属性则事务不会回滚。

  1. // 希望自定义的异常可以进行回滚
  2. @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)

注意:
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring 源码如下:

  1. private int getDepth(Class<?> exceptionClass, int depth) {
  2. if (exceptionClass.getName().contains(this.exceptionName)) {
  3. // Found it! return depth;
  4. }
  5. // If we've gone as far as we can go and haven't found it...
  6. if (exceptionClass == Throwable.class) {
  7. return -1;
  8. }
  9. return getDepth(exceptionClass.getSuperclass(), depth + 1);
  10. }
  1. 同一个类中方法调用,导致 @Transactional 失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用 Spring AOP 代理造成的,因为 只有当事务方法被 当前类以外的代码 调用时,才会由Spring生成的代理对象来管理

  1. //@Transactional
  2. @GetMapping("/test")
  3. private Integer A() throws Exception {
  4. CityInfoDict cityInfoDict = new CityInfoDict();
  5. cityInfoDict.setCityName("2");
  6. /**
  7. * B 插入字段为 3的数据
  8. */
  9. this.insertB();
  10. /**
  11. * A 插入字段为 2的数据
  12. */
  13. int insert = cityInfoDictMapper.insert(cityInfoDict);
  14. return insert;
  15. }
  16. @Transactional()
  17. public Integer insertB() throws Exception {
  18. CityInfoDict cityInfoDict = new CityInfoDict();
  19. cityInfoDict.setCityName("3");
  20. cityInfoDict.setParentCityId(3);
  21. return cityInfoDictMapper.insert(cityInfoDict);
  22. }
  1. 异常被你的 catch“吃了”导致 @Transactional 失效

这种情况是最常见的一种 @Transactional 注解失效场景,

  1. @Transactional
  2. private Integer A() throws Exception {
  3. int insert = 0;
  4. try {
  5. CityInfoDict cityInfoDict = new CityInfoDict();
  6. cityInfoDict.setCityName("2");
  7. cityInfoDict.setParentCityId(2);
  8. /**
  9. * A 插入字段为 2的数据
  10. */
  11. insert = cityInfoDictMapper.insert(cityInfoDict);
  12. /**
  13. * B 插入字段为 3的数据
  14. */
  15. b.insertB();
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能!
会抛出异常:

  1. org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候 try catch反倒会画蛇添足。

  1. 数据库引擎不支持事务

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

@Transactional 注意事项

  • 除了6种会使事务注解失效的场景,还需要注意锁
  • Transactional 和锁同时使用需要注意:
    • 由于Spring事务是通过AOP实现的,所以在方法执行之前会有开启事务,之后会有提交事务逻辑。而synchronized代码块执行是在事务之内执行的,可以推断在synchronized代码块执行完时,事务还未提交,其他线程进入synchronized代码块后,读取的数据不是最新的。所以必须使synchronized锁的范围大于事务控制的范围,把synchronized加到Controller层或者大于事务边界的调用层

      @Transactional注解原理解析

      Transactional注解的基本原理是:

      将对应的方法通过注解元数据,标注在业务方法或者所在的对象上,然后在业务执行期间,通过AOP拦截器反射读取元数据信息,最终将根据读取的业务信息构建事务管理支持。 不同的方法之间的事务传播保证在同一个事务内,是通过统一的数据源来实现的,事务开始时将数据源绑定到ThreadLocal中,后续加入的事务从ThreadLocal获取数据源来保证数据源的统一

  1. @Target({ElementType.METHOD, ElementType.TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Inherited
  4. @Documented
  5. public @interface Transactional {
  6. @AliasFor("transactionManager")
  7. String value() default "";
  8. //事务管理器名称
  9. @AliasFor("value")
  10. String transactionManager() default "";
  11. //事务传播模式
  12. Propagation propagation() default Propagation.REQUIRED;
  13. //事务隔离级别
  14. Isolation isolation() default Isolation.DEFAULT;
  15. //超时时间
  16. int timeout() default -1;
  17. //是否是只读事务
  18. boolean readOnly() default false;
  19. //需要回滚的异常类
  20. Class<? extends Throwable>[] rollbackFor() default {};
  21. //需要回滚的异常类名称
  22. String[] rollbackForClassName() default {};
  23. //排除回滚的异常类
  24. Class<? extends Throwable>[] noRollbackFor() default {};
  25. //排除回滚的异常类名称
  26. String[] noRollbackForClassName() default {};
  27. }

通过SpringBoot代码来分析实现过程:
待补充
https://juejin.cn/post/7048593598350098446
https://segmentfault.com/a/1190000022790265
https://www.cnblogs.com/kongbubihai/p/16082281.html
https://blog.csdn.net/acingdreamer/article/details/91873745