Transaction rolled back because it has been marked as rollback-only;关于声明式事务的一些坑。

目录

背景:

问题1:

问题2:

回顾:


背景:

最近遇到一个项目要求。客户导入excel文件,扫描整个excel如果存在错误数据,即全部回退,要求返回整个excel表中具体哪一行的错误数据。

问题1:

我采用的是easyexcel(看不懂的小伙伴先去了解easyexcel哦),由于数据量太大。并使用了监听器(listener)进行缓存5000条数据分批上传。并在controlller层添加了事务的声明注解。


    @PostMapping("/importData")
    @Transactional(rollbackFor = Exception.class)
    public JsonResult importData(@RequestParam(value = "file") MultipartFile file) throws IOException {
        //返回给前端展示的数据对象
        ImportResultVO importResultVO = new ImportResultVO();
        //监听器
        VendorMasterListener vendorMasterListener = new VendorMasterListener(vendorMasterService,importResultVO);
        EasyExcel.read(file.getInputStream(), VendorMasterImportRequest.class, vendorMasterListener).sheet().headRowNumber(2).doRead();
        //如果有错误数据即返回错误信息
        if (vendorMasterListener.getErrorList().size()>0){
            return JsonResult.validError().setData(vendorMasterListener.getImportResultVO());
        }else {
        //反之没有错误数据即返回成功信息
          return JsonResult.ok().setData(vendorMasterListener.getImportResultVO());
        }

 listener的缓存数据(每5000条存一次)

随之而来的问题就是,如果直接抛出异常回退,就无法返回给客户需要的每条错误的结果,会在错误的那一条直接返回,并不能在扫完整个excel表后返回。

举例:导入10000条数据,前5000条顺利导入,在第5001条导入的时候发现错误数据,那么只能回滚这5001-10000之内的导入操作,而前5000条将没办法回滚。

于是我采取想到在listener类最后手动回滚。即在

public void doAfterAllAnalysed(AnalysisContext context)最后进行回滚。

public void doAfterAllAnalysed(AnalysisContext context) {
        importResultVO.setTotalNum(totalSize);
        importResultVO.setSuccessNum(totalSize-errorList.size());
        importResultVO.setErrorNum(errorList.size());
        importResultVO.setImportDetailsVOList(errorList);
        if (errorList.size()>0){
            importResultVO.setResult(false);
            //手动设置回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }else {
            importResultVO.setResult(true);
            saveData();
            log.info("所有数据解析完成!");
        }

注意这里我并没有直接抛异常,而是直接在检查到有错误的数据回滚所有操作。(可以理解为我手动捕获异常进行处理,如下图:

try {
     throw new RuntimeException("存在错误数据");
} catch (RuntimeException e) {
     //手动设置回滚
     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
//和下面直接回滚产生的结果是一样的
     //手动设置回滚
     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

如果这里直接抛异常到controller层且未捕获,

1、如果controller层也没有捕获异常(即try-catch)的行为,那么返回给前端就不是错误的数据列表,而是直接抛出的异常。

2、如果controller层捕获异常,那么声明式事务的注解将无法回滚之前listener的导入。

即同一个事务controller有事务,listener无事务(实际还是controller的事务)。
这样如果listener中抛异常,被controller捕获,则最终listener里面的操作不会回滚,最终会被一起提交。

所以我直接进行回滚。

问题2:

尽管这样做会在所有数据扫描完后回滚,然而此时又出现一个新的问题:回滚已标记异常(Transaction rolled back because it has been marked as rollback-only

之所以会出现回滚已标记一般是嵌套事务没用正确。所以这里难道是存在嵌套事务?

我将外层controller和listener层比拟为嵌套事务(我不认为这是真正的嵌套事务)进行解释:

当内层事务listener抛出异常e,在内层事务结束时,事务被标记为“rollback-only”。如果外层controller事务捕捉了异常e,那么外层事务方法还会继续执行代码,直到外层事务也结束时,spring发现事务已经被标记为“rollback-only”;即报已标记回滚错误。

其实这里的外层controller和listener层是同一个事务;

这里我更偏向用这个解释(之所以用嵌套事务比拟,是为了后面的解决办法做铺垫):

因为controller和listener层是用同一个事务,在listener层方法执行的时候这个事务就标记为rollback-only,然后controller层方法继续使用该事务,然后又执行事务提交的操作,所以最后会抛异常。

而我的目的 就是希望listener内层事务发现问题回滚,但不影响外层事务继续执行代码提交。

于是想到了:PROPAGATION_NESTED 

PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
    public JsonResult importData(@RequestParam(value = "file") MultipartFile file) throws IOException {
        ImportResultVO importResultVO = new ImportResultVO();
        VendorMasterListener vendorMasterListener = new VendorMasterListener(vendorMasterService,importResultVO);
        EasyExcel.read(file.getInputStream(), VendorMasterImportRequest.class, vendorMasterListener).sheet().headRowNumber(2).doRead();
    }

回顾:

当时在这里试了很久都没有找到真正的原因,我始终认为这里不是真正的嵌套事务。(但是最后采用了嵌套事务的解决方案)这也是我一直奇怪或者模糊的点。如果有大佬希望可以帮忙解惑!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值