线上问题排查,一次经典的错误代码案例

340 阅读3分钟

一 前言

事务可以保持数据的一致性,原子性,隔离性和持久性,在一个事务中一旦发生了报错,那么这个事务中所有的插入或者修改的操作都会回滚,但事实上在程序中要实现这种效果还要注意一些问题,下面通过一个线上就来对这个理论探讨进行落地。

二 问题背景

用户导入费用数据,但是发现发现在系统总查不到,于是进行了反馈

三 定位问题

通过日志,定位到了发生问题的代码类

@Transactional
public void saveHlagData(Map<OBL, List<StandardDetail>> oblMap, String operater, String fileName) {
    try {
        if (oblMap.size() > 0) {
            Iterator<Entry<OBL, List<StandardDetail>>> it = oblMap
                    .entrySet().iterator();
            while (it.hasNext()) {
                Entry<OBL, List<StandardDetail>> entry = it.next();
                OBL obl = entry.getKey();
                List<StandardDetail> detailList = entry.getValue();

                // 1 生成/更新初始提单数据
                oblService.saveOrUpdate(obl);

                // 校验客户代码是否存在,否则新增客户代码信息
                customerService.checkCustomerCodeOrSaveForHLAG(obl, operater);

                // 2 保存明细
                service.save(detailList);
                // 3 保存账单数据
                List<FeeSettlement> settlementList = this.getSettlementFromDetail(detailList);
                // 4 保存发票数据
                invoiceService.save(obl, settlementList);
                // 5 保存收款核销数据
                verificationService.save(obl, settlementList);
                // 6 记录保存成功的日志
                for (StandardDetail d : detailList) {
                    importLogService.saveForHLAG(d.getSapNo(), obl.getBlNo(),
                            obl.getTradeMode(), obl.getCarrierCode(), "", fileName, operater);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这个方法中一共有六个步骤,但是从日志上看走到第四步下面就不走了,而且程序也没有报错,为什么会这样呢,其实这就是一个经典问题

实际上走到第五部时程序报了异常,数据没有插入数据库,但是异常是运行时异常,并且在try catch里面,抛出异常时被catch到了,会走catch代码块里的方法,你可能会说,catch代码块中存在e.printStackTrace(),打印异常栈方法,应该会在日志中打印出来的。

其实这个理解是错误的e.printStackTrace()会在本地控制台打印出来,但是他不是会tomcat的catalina日志打印出来,所以

catch (Exception e) {
        e.printStackTrace();
}

本身就是错误的,而且是一个经典错误,应改为

catch (Exception e) {
    logger.error("费用导入报错,文件名[{}],错误信息[{}]",fileName,e);
}

说完这个,你可能还有一个疑问,既然程序报异常了,那事务为什么没有回滚呢,毕竟方法已经使用了@Transactional注解

因为被catch住的异常,如果不抛出或者不手动回滚那么事务是不会回滚的,所以代码应改为

catch (Exception e) {
    logger.error("费用导入报错,文件名[{}],错误信息[{}]",fileName,e);
    throw new RuntimeException();
}

这样捕获异常记录日志后又将异常抛出,但是这样事务就会回滚了吗? 答案是肯定的,因为抛出的是运行时异常

这里又是一个经典问题,其实@Transactional如果直接加在方法上,那么只能回滚运行时异常RuntimeExceprion(例如NullPointException),而非运行时异常是不会被回滚的。

所以更好的处理方式是我们要将注解改为

@Transactional(rollbackFor = Exception.class)
public void saveHlagData(Map<OBL, List<StandardDetail>> oblMap, String operater, String fileName) {

这样事务就会回滚了,还有另外一种解决方案,就是捕获异常后手动回滚

 catch (Exception e) {
    logger.error("费用导入报错,文件名[{}],错误信息[{}]",fileName,e);
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

这样也可以完成事务的回滚。