2

前言

Spring事务管理是Spring的重点,也是开发应用中不可或缺的技能。熟悉事务回滚机制及失效场景很是必要。

场景1:不使用@Transactional注解

此时事务肯定不会回滚
如下代码所示,在插入数据之后紧接着执行了一个会触发NPE(空指针异常)的操作。可以发现日志打印出了"新增用户信息时捕获到异常",但是数据还是正常插入到了MySQL中,即不会发生回滚。

    @Override
    public void insertUser(String userId, String username) {
        try {
             String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 手动触发一个NPE异常
            List<Integer> ids = null;
            ids.add(1);
        } catch (Exception e) {
            throw new RuntimeException("新增用户信息时捕获到异常");
        }
    }

场景2:抛出受检异常但注解没有指定rollbackFor

此时将不会回滚。实际上当我们使用@Transaction 时,默认为RuntimeException(也就是运行时异常,即非受检异常)异常才会回滚。而IOException属于受检异常,当注解里不设置rollbackFor = Exception.class时,该种情况就不会回滚事务。

    @Override
    @Transactional
    public void insertUser(String userId, String username) throws FileNotFoundException {
            String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 打开一个不存在的文件,将触发FileNotFoundException
            FileInputStream file=new FileInputStream("user.txt");
    }

场景3:添加了@Transactional但是catch{}语句中未抛出异常

此时也不会回滚事务
如下代码所示,虽然此时有注解了,但是catch体里没有抛出异常,此时事务也不会回滚。但是这个机制是什么呢?
实际上Spring的事务是通过AOP实现的,Spring事务只在发生未被捕获的RuntimeException时才回滚。Spring AOP异常捕获原理:被拦截的方法需显式抛出异常,并且不能经过任何处理,这样AOP代理才能捕获到方法的异常,进而进行回滚.

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertUser(String userId, String username) {
        try {
             String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 手动触发一个NPE异常
            List<Integer> ids = null;
            ids.add(1);
        } catch (Exception e) {
           
        }
    }

场景4:添加了@Transactional且在catch{}语句中显式的抛出异常

此时事务正常回滚。且在日志中能看到打印出错误日志。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertUser(String userId, String username) {
        try {
             String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 手动触发一个NPE异常
            List<Integer> ids = null;
            ids.add(1);
        } catch (Exception e) {
            // 发生异常时将异常抛出,Spring  AOP捕获到异常后会触发事务回滚 
            throw new RuntimeException("新增用户信息时捕获到异常");
        }
    }

场景5:添加了@Transactional且在catch{}语句中手动触发事务回滚

此时事务正常回滚

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertUser(String userId, String username) {
        try {
             String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 手动触发一个NPE异常
            List<Integer> ids = null;
            ids.add(1);
        } catch (Exception e) {
            // 发生异常时手动触发事务回滚 
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

可以看到,场景4和场景5都可以实现MySQL事务回滚。不同的是,场景3需要在控制层继续捕获这个异常并处理。而场景4是手动回滚,这样控制层就无需去处理异常了。具体在开发中采取哪种回滚方式可根据实际场景权衡斟酌。

但是你以为这就结束了么?事情当然没有这么单纯。
图片.png

场景6:同一个代理类中的方法调用,被调用方法有@Transactional注解

如下代码所示,这种情况事务会回滚吗?答案是不会
不仅不会,还会报出异常org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope。那么这是什么原因呢?事实上Spring中的@Transactional原理是通过TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截。像如下的代码,Spring AOP实际上是为每个@Service注解的类生成一个代理类,由于代理类的insertUser方法没有被注解修饰,Spring认为这里不用事务拦截器进行拦截,即便被调用类中使用了事务注解。

    @Override
    public void insertUser(String userId, String username) {
       addUser(userId, username);
    }

    @Transactional(rollbackFor = Exception.class)
    public void addUser(String userId, String username){
        try {
            String sql = "insert into `cps_user_info` (`user_id`,`user_name`) values (?,?);";
            jdbcTemplate.update(sql, userId, username);
            // 手动触发一个NPE异常
            List<Integer> ids = null;
            ids.add(1);
        } catch (Exception e) {
            // 发生异常时手动触发事务回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

这里我想纠正一个误区,很多人以为@Transactional(rollbackFor = Exception.class)只有作用在Service层才会作用,在控制层使用不会走代理,就不会起作用,其实这是错的。在接口层使用该注解,条件满足的话一样会触发事务回滚。

小结

Spring的事务基本是大厂面试很重视的知识点。不仅仅是事务的特性,事务传播行为这种理论面试题。对事务的实际应用也很重视。事实上少侠露飞在开发中发现很多人对于事务的使用就是加个@Transactional(rollbackFor = Exception.class)就完事的,但其实里面的逻辑可能会造成出现异常时事务并不会回滚。这还是很有风险的,极易造成数据的不一致。

我是少侠露飞,爱技术,爱分享。


少侠露飞
155 声望14 粉丝