记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

时间:2022-10-13 08:04:44

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

遇到的坑

背景

最近一次开发中遇到Springboot中事务失效的问题,具体场景如下,此场景事务失效的原因较为常见,主要是记录一下其他失效情况:

导入了一个比较复杂的xml文件数据然后转化为java实体类接收,接收后需要处理数据进行入库,然后需要操作到4个表中的数据,但是发现执行过程中又出现异常,但是数据正常入库了。

代码贴一下

@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {

    private final SnowPlayerService playerService;
    private final SnowEventService eventService;
    private final SnowScoreService scoreService;
    private final SnowScoreItemService itemService;
    
    public JsonResult importXml(MultipartFile file) {
        // 利用输入流获取XML文件内容
        StringBuffer buffer;
        try (BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
            buffer = new StringBuffer();
            String line;
            while ((line = br.readLine()) != null) {
                buffer.append(line);
            }
            // XML转为JAVA对象
            OdfBody odfBody = (OdfBody) XmlBuilder.xmlStrToObject(OdfBody.class, buffer.toString());
            // 处理数据入库
            handlerIntoDB(odfBody);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return JsonResult.SUCCESS;
    }


    private void handlerIntoDB(OdfBody odfBody) {
        // 赛事信息
        SnowEventEntity eventEntity = new SnowEventEntity(odfBody);
        // 保存赛事
        eventService.save(eventEntity);
        // 制造一个异常
        int i = 5 / 0;

        // 处理具体信息
        List<Result> resultList = odfBody.getCompetition().getResultList();
        if (CollectionUtils.isNotEmpty(resultList)) {
            // 各种入库操作
        }    
    }

}

问题复现

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

捕获到异常,正常打印了堆栈信息,但是前面的数据操作入库了。

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

以上代码相信各位一眼就看出来了,原因其实就是使用了try-catch异常捕获,然后把异常给吃掉了,只打印了堆栈信息,改进一下:

解决

public JsonResult importXml(MultipartFile file) {
    // 利用输入流获取XML文件内容
    StringBuffer buffer;
    try (BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
        buffer = new StringBuffer();
        String line;
        while ((line = br.readLine()) != null) {
            buffer.append(line);
        }
        // XML转为JAVA对象
        OdfBody odfBody = (OdfBody) XmlBuilder.xmlStrToObject(OdfBody.class, buffer.toString());
        // 处理数据入库
        handlerIntoDB(odfBody);
    } catch (Exception e) {
        e.printStackTrace();
        throw new BusinessException("数据入库失败");
    }
    return JsonResult.SUCCESS;
}

捕获异常后不要将异常吃掉,直接手动抛出,让事务捕获到异常,进行数据回滚,且可以全局捕获异常并将错误信息返回给客户端。

效果

捕获到异常

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

数据也没有入库了

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

记录下其他失效场景,避免入坑

总览图

记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)

SpringBoot中使用事务一个注解@Transactional即可解决,但是也要注意会导致事务失效的一些场景。

  • 使用事务的方法的访问级别是private的,spring要求被代理方法必须是public的,而且@Transactional声明在方法上时,开发工具基本上都会编译报错

  • 方法使用了final修饰,Spring事务底层使用了Aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现了事务功能。而如果使用final修饰了,那他的代理类就无法重写该方法,无法添加事务功能

  • 如果某个方法是static的,同样无法通过动态代理,变成事务方法。

  • 内部方法调用:在同一个类中,一个非事务方法直接调用本类的事务方法时,事务将会失效;而如果一个事务方法调用本类或其他类的非事务/事务方法时,调用的其他方法都会被添加事务。

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
        
        public void add(UserModel userModel) {
            userMapper.insertUser(userModel);
            updateStatus(userModel);
        }
    
        @Transactional
        public void updateStatus(UserModel userModel) {
    		....
        }
    }
    /*
    	以上,add方法是一个非事务方法,但是调用的是本类的事务方法,如果直接调用,那么将会直接调用本类的updateStatus方法,不会去走代理类的代理方法,会造成事务失效。
    	解决方式有很多(方法1比较简单):
    		1.可以将add()方法也声明为事务方法(用@Transactional修饰)
    		2.在上文的UserService类中注入自己,不会有循环依赖问题的
    		...
    	
    */
    
  • 如果类没有反转到spring容器(未被spring管理),那么声明@Transactional是无用的,无法生成代理类

  • 假如是使用了多线程,开启不同的线程任务去操作数据库,那么这样的话,只能保证单个线程中的事务是有效的,也就是多个不同的线程中获取到的数据库连接是不一样的,从属于不同的事务中。

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Transactional
        public void add(UserModel userModel) throws Exception {
            userMapper.insertUser(userModel);
            new Thread(() -> {
    			doOtherThing();
            }).start();
        }
        
        @Transactional
        public void doOtherThing() {
            System.out.println("保存表数据");
        }
    }
    

    我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

    这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的

  • 注意数据库的存储引擎,innodb才支持事务

  • 异常不要自己吃掉

  • 如果捕获了异常,请正确抛出异常,在spring的事务中,默认只会回滚RuntimeExceptionError,对于受查异常是不会进行回滚的。

  • 自定义的回滚异常:其实也就是声明遇到某种类型的异常时才进行回滚,而程序报错时基本是抛出SqlExceptionDuplicateKeyException等异常,像我们自定义的异常一般情况不会被抛出。

    @Transactional(rollbackFor = Exception.class)
    public class BusinessXmlServiceImpl {
        ..
    }
    
    // @Transactional中有个rollcackFor属性,默认是UncheckedException,也就是非受查异常(包含RuntimeException和Error
    // 阿里开发规范中建议:要求开发者重新指定该参数,声明为Exception或Throwable即可。)
    

特殊情况

一般情况下,我们希望事务中所有的操作的原子性,也就是所有的操作单元也么都执行成功,要么都执行失败。但是如果遇到这样的场景时:事务中有两个数据库操作,我们希望就算后一个执行失败了,但是前一个操作如果执行成功了不希望回滚,感觉有点难遇到这样的需求,打破了原子性,但是也可能遇到。

@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {

    private final SnowPlayerService playerService;
    private final SnowScoreService scoreService;
    
    public void add(SnowPlayerEntity playerEntity, SnowScoreEntity scoreEntity) { 
        playerService.save(playerEntity);
        scoreService.save(scoreEntity);
    }
}

上块代码中,就是通过一个事务方法调用其他的非事务方法,由于事务的传播性,其他非事务方法都会被加到事务中,确保执行的原子性

如果要打破原子性,满足上文不会滚后一个原子操作,也非常简单,那么这么写即可。

@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {

    private final SnowPlayerService playerService;
    private final SnowScoreService scoreService;
    
    public void add(SnowPlayerEntity playerEntity, SnowScoreEntity scoreEntity) {

        playerService.save(playerEntity);
        try {
            scoreService.save(scoreEntity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

偷偷把后一个方法的异常吃掉即可。O(∩_∩)O哈哈~

简单介绍几种失效和回滚失败的场景,详细可以参考下面文章。

参考

详细可参考文章Spring boot事务不生效