spring @Transactional 事物失效原因

2022-07-06 08:36:33

前言

        spring事物分为申明式和编程式

声明事务的几种失效场景

以下两个方面来说一下事务为什么会失效?
@Transactional介绍
@Transactional失效场景

@Transactional介绍

  • @Transactional是声明式事务的注解,可以被标记在类上、接口、方法上。
  • 该注解中有很多值得深入了解的几种属性,我们来看一下。

transactionManager

  • 指定事务管理器,值为bean的名称,这个主要用于多事务管理器情况下指定。比如多数据源配置的情况下。

isolation

  • 事务的隔离级别,默认是Isolation.DEFAULT。
    • Isolation.DEFAULT:事务默认的隔离级别,使用数据库默认的隔离级别。

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

    • Isolation.READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。

    • Isolation.REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。

    • Isolation.SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻读。

propagation

  • 代表事务的传播行为,默认值为Propagation.REQUIRED。
    • Propagation.REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。比如A方法内部调用了B方法,此时B方法将会使用A方法的事务。

    • Propagation.MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。

    • Propagation.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

    • Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

    • Propagation.REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。比如A方法使用默认的事务传播属性,B方法使用REQUIRES_NEW,此时A方法在内部调用B方法,一旦A方法出现异常,A方法中的事务回滚了,但是B方法并没有回滚,因为A和B方法使用的不是同一个事务,B方法新建了一个事务。

    • Propagation.NESTED:支持当前事务,新增Savepoint点,也就是在进入子事务之前,父事务建立一个回滚点,与当前事务同步提交或回滚。
      子事务是父事务的一部分,在父事务还未提交时,子事务一定没有提交。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

timeout

  • 事务的超时时间,单位为秒。

readOnly

  • 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。如果一个事务只涉及到只读,可以设置为true。

rollbackFor 属性

  • 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。 默认是在RuntimeException和Error上回滚。

noRollbackFor

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

@Transactional失效场景

声明式事务失效的场景有很多,这里只是罗列一下几种常见的场景。

  1. 底层数据库引擎不支持事务 如果数据库引擎不支持事务,则Spring自然无法支持事务。如我们最常用的mysql,引擎MyISAM,是不支持事务操作的。需要改成InnoDB才能支持

  2. 在非public修饰的方法使用

    @Transactional注解使用的是AOP,在使用动态代理的时候只能针对public方法进行代理,源码依据在AbstractFallbackTransactionAttributeSource类中的computeTransactionAttribute方法中,如下:

    此处如果不是标注在public修饰的方法上并不会抛出异常,但是会导致事务失效。

  3. 异常被 " 踹死了 "

    这种情况小白是最容易犯错的,在整个事务的方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。伪代码如下:

    @Transactionalpublicvoidmethod(){try{//插入一条数据//更改一条数据}catch(Exception ex){return;}}
  4. 方法中调用同类的方法

    简单的说就是一个类中的A方法(未标注声明式事务)在内部调用了B方法(标注了声明式事务),这样会导致B方法中的事务失效。

    publicclassTest{publicvoidA(){//插入一条数据//调用B方法B();}
      
      @TransactionalpublicvoidB(){//插入数据}}

    为什么会失效呢?:其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
    如何解决呢?:这就涉及到注解失效的原因了,后续文章会介绍到,这里不过多介绍了。

  5. rollbackFor属性设置错误

    很容易理解,指定异常触发回滚,一旦设置错误,导致一些异常不能触发回滚,此时的声明式事务不就失效了吗。

  6. noRollbackFor属性设置错误

    这个和rollbackFor属性设置错误类似,一旦设置错误,也会导致异常不能触发回滚,此时的声明式事务会失效。

  7. propagation属性设置错误

    事务的传播属性在上面已经介绍了,默认的事务传播属性是Propagation.REQUIRED,但是一旦配置了错误的传播属性,也是会导致事务失效,如下三种配置将会导致事务失效:
    Propagation.SUPPORTS
    Propagation.NOT_SUPPORTED
    Propagation.NEVER

  8. 原始SSM项目,重复扫描导致事务失效

    在原始的SSM项目中都配置了context:component-scan并且同时扫描了service层,此时事务将会失效。
    按照Spring配置文件的加载顺序来说,会先加载Springmvc的配置文件,如果在加载Springmvc配置文件的时候把service也加载了,但是此时事务还没加载,将会导致事务无法成功生效。
    解决方法很简单,把扫描service层的配置设置在Spring配置文件或者其他配置文件中即可

总结:

  • 1.数据库引擎是否设置合理,比如我们最常用的mysql,引擎MyISAM,是不支持事务操作的。需要改成InnoDB才能支持

  • 2.private、final、static方法,事务不生效,入口方法必须是public ,spring的AOp特性决定的,spring认为private自己用的方法应该自己控制,不应该用事务切进去

  • 3.Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚(至于为什么spring要这么设计:因为spring认为Checked的异常属于业务的,coder需要给出解决方案而不应该直接扔该框架)

  • 4.最好不要把@trasaction注解到接口上:
    在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

  • 5.同类调用不生效(service方法中调用本类中的另一个方法,事务没有生效):在同一个类中一个‘无事务’的方法调用另一个‘有事务’的方法,事务是不会起作用的

    为什么会失效呢?:
     其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,
     但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
  • 6、确认你的类是否被代理了(因为spring的事务实现原理为AOP,只有通过代理对象调用方法才能被拦截,事务才能生效

  • 7、确保你的业务和事务入口在同一个线程里,否则事务也是不生效的

  • 8.如果使用的是rollbakfor的默认,已检查的异常(所有派生自Error和RuntimeException的类,都是未检查异常.其余的是已检查异常,

    比如nullPointException是未检查的,IllegalAccessException 是已检查的)不回滚,
    可设为rollbackFor={Exception.class}

  • 9.springboot @EnableTransactionManagement 是否开启在springboot1.4以后可以不写。框架在初始化的时候已经默认给我们注入了两个事务管理器的Bean(JDBC的DataSourceTransactionManager和JPA的JpaTransactionManager
    ),其实这就包含了我们最常用的Mybatis和Hibeanate了。
    当然如果不是AutoConfig的而是自己自定义的,请使用该注解开启事务

  • 作者:shengjia_b
  • 原文链接:https://blog.csdn.net/nbdebuyaobuyao/article/details/107998258
    更新时间:2022-07-06 08:36:33