Spring中@Transactional失效

Spring中的声明式注解@Transactional很大程度的方便了开发者进行DB数据保存。但是在一些特殊情况下,可能会造成注解不是按想定的方式生效,这里说几种可能造成的几种情况。

常见的几种情况:

异常被捕获

这是一种比较简单不过稍不注意也可能会犯的情况。
Spring中事务提交还是回滚是根据调用的方法是否抛出异常来决定的,因此如果把异常捕获之后又不抛出的话,即使出了问题,事务还是会提交。

@Autowired
private ClassB b;

@Autowired
private ClassC c;

@Transactional
public void methodA(){
    try {
        b.methodB();
        c.methodC();
    } catch (Exception e) {
        
    }
}

上例中,想要的结果是b.methodB()c.methodC()同时提交或回滚,但是由于异常被捕获,即使在执行方法C的时候出现了异常,方法B的操作仍旧会生效。
(如果方法A是一次转账,方法B是转账中的加钱操作,方法C是减钱操作,B和C只执行其中一个的话会导致总金额就发生了变化)

@Transactional修饰了非public方法

这种也是有可能犯的一种情况。
@Transactional只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启AspectJ 代理模式。(默认代理模式CGlib)
CGlib是使用继承进行动态代理的,所以理论上protect方法无修饰符时应该也可以?没有测试,有空试一下。

同一个类中的方法调用

这是一种非常容易犯,又不容易察觉的情况。

@Component
public class A{
    @Transactional
    public void methodA() {
        methodB();
    }
    
    @Transactional
    public void methodB() {
        // do something
    }
}

上例中,methodB的事务是不生效的,因为这里是同一个类中的调用(更确切的说是同一个类同一个对象中),Spring事务的原理是调用时检查@Transactional注解,然后生成代理类进行事务管理,但是内部调用时不会生成代理类(或者说默认不会),因此也就无法进行事务管理。

这种情况有好几种解决方法,下面会说到。

非常见的情况

非常见是指对@Transactional进行了一些属性的配置导致不生效。

@Transactional 注解属性 rollbackFor 设置错误

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

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

@Transactional 注解属性 propagation 设置错误

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

  • PROPAGATION.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • PROPAGATION.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • PROPAGATION.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

propagation默认值PROPAGATION.REQUIRED:没有事务时创建事务。有事务了则加入该事务(相当于使用一个session)

同一个类中事务方法调用解决方法

方法拆分

这是一种最简单的方法,也就是把上面例子中的methodB拆分到一个单独的类里面,这样就是一般情况下的事务调用。


下面三种方法都是在methodB上添加@Transactional(propagation = Propagation.REQUIRES_NEW)之后进行的测试

使用AspectJ代理

具体操作就是,application.yml中需要配置spring.aop.auto :true,然后在启动类开启AspectJ代理,并暴露代理类:@EnableAspectJAutoProxy(exposeProxy = true)
这样的话就可以在调用的时候获取到代理类,并进行方法调用:

((TestClassA)AopContext.currentProxy()).insertB();

从ApplicationContext获取Bean

这个原理应该是和上面一样的,直从ApplicationContext中获取到当前Bean,然后再调用方法:

// applicationContext 可以自动注入
applicationContext.getBean(TestClassA.class).insertB();

注入自身

@Component
public class TestClassA {
    @Autowired
    private TestClassA testClassA;
}

用这种方法也可以使methodB的事务生效,但是需要注意的是,第一次生成的代理类和自动注入的代理类不是一个对象,也就是代码中thistestClassA不是同一个对象,并且testclassA中不会再次自动注入。

所以如果methodB插入数据时需要当前对象的属性,这种方法便不能再使用


总之,同类事务不生效是一种很容易疏忽的情况,具体怎么处理,还是要结合业务。

标签: Spring, 数据库

添加新评论