一 前言
事务可以保持数据的一致性,原子性,隔离性和持久性,在一个事务中一旦发生了报错,那么这个事务中所有的插入或者修改的操作都会回滚,但事实上在程序中要实现这种效果还要注意一些问题,下面通过一个线上就来对这个理论探讨进行落地。
二 问题背景
用户导入费用数据,但是发现发现在系统总查不到,于是进行了反馈
三 定位问题
通过日志,定位到了发生问题的代码类
@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();
}
这样也可以完成事务的回滚。