Spring事务失效的几种场景

spring中的声明式事务,使得事务的操作方便了起来,但是不知道小伙伴们有没有遇到spring中事务失效的场景,会不会一头雾水,“我明明什么都已经配置好了啊,怎么事务还会失效呢”,别急,今天我们就来分析一下为什么spring事务会失效,也使得大家在以后的开发中可以少走一些弯路。

事务失效的几大场景和原因


1.检查异常spring的事务不会执行回滚操作

  • 原因:我们通常的理解:被@Transactional标注的方法,方法里调用的多个sql语句,如果sql语句之间抛出了异常,那么事务就会回滚到之前的状态,可是大家可能不知道,只有抛出的异常是非检查异常时,事务才会回滚。换句话说,如果抛出的异常的是检查异常,那么就不会执行事务回滚。

  • 解法1:  把抛出的异常封装成为一个非检查异常,比如运行时异常,那么事务就会正常的执行回滚。     

  • 解法2:  在@Transactional注解添加参数(rollbackFor = Exception.class),那么表明所有Exception类型的注解都会触发事务回滚。(这里为了方便,选择了一个比较大的范围,实际情况可以自己酌情考虑)

          


 2.业务方法内自己 try-catch 异常导致事务不能正确回滚  

  • 原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉。spring实现声明式事务的原理是依靠aop自动代理,也就是调用者调用代理对象Proxy的方法,然后这个方法调用目标对象的目标方法。因此目标对象的真实方法,也就是我们自己定义的方法,是在最内层的。那么此时,如果目标方法内部自己偷偷的用try catch处理了异常,此时上层并不知道方法出现了异常,也就无法回滚

  • 解法1:异常原样抛出

    • 在 catch 块添加 throw new RuntimeException(e);

  • 解法2:手动设置 TransactionStatus.setRollbackOnly()

    • 在 catch 块添加一段代码,实现手动设置回滚: TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

                


3. aop 切面顺序导致事务不能正确回滚

  • 原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常,参考情况二,调用者(代理对象Proxy)无法感知到目标方法出现异常,因此无法正常回滚。

  • 解法1、2:同情况2 中的解法:1、2

  • 解法3:调整切面顺序,在 MyAspect 上添加 @Order(Ordered.LOWEST_PRECEDENCE - 1) (不推荐)


 4. 非 public 方法导致的事务失效

  • 原因:这种情况比较好理解,声明式事务基于aop切面编程。Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的

  • 解法1:改为 public 方法

  • 解法2:添加 bean 配置如下(不推荐)

@Bean
public TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

5. 父子容器导致的事务失效

在springMVC环境下,很多人使用的是父子容器,关于父子容器,我之前有一篇博客讲解了父子容器,感兴趣的朋友可以去看一下,这里不在赘述。配置了父子容器之后,父容器是spring的容器,子容器是springMVC的web容器,一般来说,父容器放的是与web无关的bean,例如mapper, service等等。。。。这里我设置了两个配置类,WebConfig 对应子容器,AppConfig 对应父容器,其中在AppConfig中我配置了事务,也就是说,只有在父容器下的bean才会支持事务。

父容器的配置类:

@Configuration
@ComponentScan("com.ding.app.service")
@EnableTransactionManagement
// ...
public class AppConfig {
    // ... 有事务相关配置
}

子容器的配置类(为空):

@Configuration
@ComponentScan("com.ding.app")
// ...
public class WebConfig {
    // ... 无事务配置
}

一般来说,web环境下,是通过controller来调用service层的方法,通常我们会在控制器类中注入对应的service对象,可是,父容器里的bean,在子容器中也会存在,因此,会优先使用子容器的bean,可是我们上文说到,子容器的配置类并没有配置事务,因此无法使用事务

  • 原因:子容器扫描范围过大,把未加事务配置的 service 扫描进来

  • 解法1:各扫描各的,不要图简便

  • 解法2:不要用父子容器,所有 bean 放在同一容器, 这样一个配置类中配置了事务,这个配置类所关联的容器中所有的bean都具有事务了


6. 调用本类方法导致传播行为失效  

还是那句哈,spring声明式事务是依赖与aop,aop又是依赖于自动代理。可以说,如果一个bean配置了事务,那么我们从容器中得到的这个bean,并不是这个bean本身,而是具有代理增强功能的代理对象。

@Service
public class Service{

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

如上图,我们在一个事务方法bar()方法中,调用了也被事务注解标注的foo()方法,其中foo()方法的事务扩散是REQUIRED,而bar()方法的事务扩散机制是REQUIERES_NEW,关于事务扩散机制不理解的小伙伴可以查阅一下相关的资料.

按理说,foo()方法是支持事务的,那么foo()方法中调用bar()方法,按照bar()方法的事务扩散机制,bar()方法也是支持事务的,可是结果并不是。为什么呢?

还是那句话,代理对象,代理对象,代理对象!!重要的话说三遍,调用foo()方法的是代理,因此bar()方法具有事务,可是调用bar()方法的却不是事务了,而是这个真实对象service。

其实是这样的:

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        this.bar();
    }

不知道这样子大家是否明白了呢,因此,bar()方法被真实对象调用而不是被代理对象调用,肯定就没有事务属性了啊 

  • 原因:本类方法调用不经过代理,因此无法增强

  • 解法1:依赖注入自己(代理)来调用

  • 解法2:通过 AopContext 拿到代理对象,来调用

  • 解法3:通过 CTW,LTW 实现功能增强


7. @Transactional 没有保证原子行为  

 先看一段代码:

@Service
public class Service7 {

    private static final Logger logger = LoggerFactory.getLogger(Service.class);

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

    public int findBalance(int accountNo) {
        return accountMapper.findBalanceBy(accountNo);
    }
}

上面的代码中,我们主要是模拟了一个转账的业务,id为from账户(A账户)向id为to的账户(B账户)转amount金额的钱。

假设A账户里有1000块钱,B账户里也有1000块钱,需要A账户向B账户转1000块钱,正常来说操作过后的A账户余额为0,B账户的余额为2000。并且显然的是,余额不能为0,因此默认这个操作只能执行一次。所以我加了一条if判断。

这段代码看起来是没问题的,也确实没问题,但是只能说是在单线程的情况下没有问题,在一个并发的情况下的话,线程1和线程2同时访问A账户的余额,都发现A账户余额充足,那么他们都会去扣减金额,导致业务出现异常,A账户被转了两次钱!

相信到这里解决办法已经在小伙伴们的口中了,没错,加锁

@Transactional(rollbackFor = Exception.class)
    public synchronized void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

加锁完成后,大家可能觉得没有问题了,先不考虑性能问题,单说并发安全,实际上还是不安全的!

为什么呢?

大家可以看到synchronized锁的是整个方法,这个方法中有3条SQL语句,分别是,查询A余额,扣减A余额,增加B的余额。

但是事务里只有这3条SQL语句吗?显然是不的,事务提交commit这个步骤是事务里至关重要的一个步骤,但是很遗憾这个步骤并不在这个原始方法里,而是在代理对象增强后的方法里。所以,并不受这个锁的保护!

相信现在不用我继续说大家也知道问题所在了吧!

线程1拿到锁,执行完这3条SQL语句后,方法结束,锁释放,可是突然遇到问题了,迟迟没有提交,因此此时数据库中的数据是没有变化的!然后阻塞的线程2拿到了锁,也执行了三条SQL语句,随后立即提交了,此时数据库中的数据已经更新了。这时,线程1醒了,把最后的步骤commit完成了,因此,又是扣费了两次。

咋办呢?

  • 解法1:synchronized 范围应扩大至代理方法调用,在这个转账业务方法的外部加锁,这样整个事务都被锁住了,只有事务提交,也就是事务完成后,才能释放锁。

  • 解法2:使用 select … for update 替换 select    

        相信熟悉数据库锁的小伙伴知道这是啥了,没错,这就是“当前读”,关于当前读这个名词,大家可以看一下我之前写的博客,理解MySQL中的MVCC机制就能理解了。

简单来说,就是读取的过程中,给读取的这一行数据加了一把共享锁,这个共享锁只有在读的时候才不排斥,如果是另一个线程要来修改数据,那么对不起,等我读完之后在修改。可以看到,这样的读取到的永远是当前最新的数据。

扯多了,话题回到事务来,简单了解了共享锁后,那么可以很好理解了,这是在数据库层面,加了一个行锁,因此并发量可以得到保证,毕竟我锁的只是一条数据,不是整个表

读到这里,觉得博主讲的明白,对你有帮助的话,不妨点个赞吧!创作不易,如果有不同的看法和简介也欢迎在评论区指出!谢谢大家 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值