一,代码如下
@Transactional(rollbackFor = Exception.class)
public void updateDelivery(){
// 1.新增反馈记录
// 2.更新订单状态,及其他字段
// 3.新增变更履历
// 4.其他新增逻辑及与其他系统交互逻辑
}
二,问题
偶尔出现(概率极低)步骤2中订单更新失败,状态没有更新,最初听说只是状态这个字段没有更新成功,其实还有其他字段,是都没有更新,即没有进行更新操作。具体更新逻辑是当前状态是10,想更新成20,结果还是10。
三,猜想与分析
猜想1,执行代码时出现了异常,但是经过查看步骤1,3,4都执行成功了,直到方法最后,并没有出现异常
猜想2,业务逻辑中该状态变更不符合具体场景,由于猜想1中1,3执行成功,且经过代码分析,不存在这种问题
猜想3,更新时出现了死锁,这条数据在其他地方也在更新,且对方持有该数据锁(行锁),导致这里更新失败
具体可能出现同时更新的场景有两个
有一个job会查询状态是10的订单数据,会写入一个中间表,然后更新这条订单数据,job更新成功
并发导致,两个请求同时调用该接口,请求1想更新成10,请求2想更新成20,由于业务逻辑比较长,请求1持有该数据锁,请求2无法更新成功,可能出现了死锁
针对场景2具体再分析
实际上,两次请求应该都会更新成功,请求2会等待请求1事物结束,然后再去更新数据,也不会出现死锁,可以模拟下这种并发场景。
具体代码如下,请求1,进来,更新后休眠20s,保证事物不结束,保证持有该数据行锁(这个应该是这样),请求2进来,并没有出现异常,且状态也会正常更新。
@ApiOperation(value = "模拟更新订单主表异常-并发1", notes = "模拟更新订单主表异常-并发1")
@PostMapping(value = "updateDeliveryException1")
@Transactional(rollbackFor = Exception.class)
public PpDelivery updateDeliveryException1(@RequestBody PpDelivery ppDelivery) {
tiExceptionLogService.insertExceptionRecord(ppDelivery, "", "模拟事物问题");
for (int i = 0; i < 3; i++) {
Integer result = 0;
Boolean isException = false;
try {
result = ppDeliveryService.updatePpDelivery(ppDelivery);
Thread.sleep(20000);
if (result == 1) {
break;
}
} catch (Exception e) {
logger.error("ylToDPSOrderStatusFeedbackImpl update exception ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue), e);
tiExceptionLogService.insertExceptionRecord(ppDelivery, e.getMessage(), "更新订单主表失败,数据库执行新增异常");
isException = true;
}
if (result == 0 && Boolean.FALSE.equals(isException)) {
logger.error("ylToDPSOrderStatusFeedbackImp update failed ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue));
tiExceptionLogService.insertExceptionRecord(ppDelivery, String.valueOf(result), "更新订单主表失败,数据库执行新增未返回成功");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("ylToDPSOrderStatusFeedbackImpl interruptedException ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue));
}
}
return ppDelivery;
}
@ApiOperation(value = "模拟更新订单主表异常-并发2", notes = "模拟更新订单主表异常-并发2")
@PostMapping(value = "updateDeliveryException2")
@Transactional(rollbackFor = Exception.class)
public PpDelivery updateDeliveryException2(@RequestBody PpDelivery ppDelivery) {
tiExceptionLogService.insertExceptionRecord(ppDelivery, "", "模拟事物问题");
for (int i = 0; i < 3; i++) {
Integer result = 0;
Boolean isException = false;
try {
result = ppDeliveryService.updatePpDelivery(ppDelivery);
if (result == 1) {
break;
}
} catch (Exception e) {
logger.error("ylToDPSOrderStatusFeedbackImpl update exception ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue), e);
tiExceptionLogService.insertExceptionRecord(ppDelivery, e.getMessage(), "更新订单主表失败,数据库执行新增异常");
isException = true;
}
if (result == 0 && Boolean.FALSE.equals(isException)) {
logger.error("ylToDPSOrderStatusFeedbackImp update failed ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue));
tiExceptionLogService.insertExceptionRecord(ppDelivery, String.valueOf(result), "更新订单主表失败,数据库执行新增未返回成功");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("ylToDPSOrderStatusFeedbackImpl interruptedException ppDelivery: {}", JSON.toJSONString(ppDelivery, SerializerFeature.WriteMapNullValue));
}
}
return ppDelivery;
}
通过以上分析,如果排除场景2,很可能就是场景1了。且查询更新时间job中这条数据更新与最初代码中步骤3的时间完全问好
四,解决方案
调整job执行频率,由1min改成5分钟,避免同时取到订单数据进行更新
job逻辑中改循环查询更新为直接更新,减少整个方法执行时间,缩短大事物
五,疑问
查看数据库死锁日志,并没有发现出现死锁记录,show engin innodb status,如果这个信息准确,又是什么原因没有更新成功呢
job中并没有声明式事物,会存在大事物问题吗,代码如下
/** 订单状态存中间表定时器
*
*/
@JobHandler(value = "orderStatusToTiHandler")
@Component
public class OrderStatusToTiHandler extends IJobHandler {
@Autowired
private OrderStatusDispatchToAllService orderStatusDispatchToAllService;
@Autowired
private IPpDeliveryService ppDeliveryService;
@Autowired
private IBasCarrierService basCarrierService;
@Autowired
private ISysSettingService sysSettingService;
private static final Logger logger = LoggerFactory.getLogger(OrderStatusToTiHandler.class);
@Override
public ReturnT<String> execute(String s){
logger.info("订单状态存中间表定时任务开始运行运行");
List<String> statusNotIn = Arrays.asList("66","90", "99");
List<String> statusIn = Arrays.asList("6020", "6025", "6050", "6055", "100","6045");
PpDelivery ppDeliveryQurey = new PpDelivery();
ppDeliveryQurey.setInterfaceStatus("0");
ppDeliveryQurey.setSortName("delivery_no");
ppDeliveryQurey.setSortOrder("ASC");
List<PpDelivery> ppDeliveries = ppDeliveryService.queryAllByValuesAndStatus(ppDeliveryQurey, statusIn.toArray(new String[statusIn.size()]), statusNotIn.toArray(new String[statusNotIn.size()]));
if (ppDeliveries.size() <= 0) {
logger.info("订单状态存中间表定时任务运行成功");
return SUCCESS;
}
for (PpDelivery ppDelivery : ppDeliveries) {
if(StringUtils.isNotBlank(ppDelivery.getTmBasCarrierId())){
String receiver = this.judgeCarrierPlatform(ppDelivery.getTmBasCarrierId());
if(StringUtils.isNotBlank(receiver)){
orderStatusDispatchToAllService.orderStatusFeedBackToTi(ppDelivery.getTtPpDeliveryId(), receiver+"_" + ppDelivery.getCurrentStatus());
}
}
}
logger.info("订单状态存中间表定时任务运行成功");
return SUCCESS;
}
3,改声明式事物为编程式事物,有用吗
如果把最初的代码中,步骤2更新逻辑抽离出来,使用编程式事物,同时把整个方法声明式事物去掉,这种改造目的是缩短大事物,但是好像又是针对并发场景下大事物问题,但是上面已经验证并发场景下,会顺序执行,等待前面事物结束
以上分析可能存在不足,欢迎大佬指正,不胜感激