这事第一个版本的代码逻辑,通过数据库中是否有纪录来防止重复创建
@Transactional
public Boolean createGroupTicket(String orderId){
//......code
//判断逻辑
if (!org.apache.commons.lang3.StringUtils.isEmpty(orderDO.getGroupTicketNo())) {
//已创建过组合工单
log.info("订单{}已创建过组合工单,工单号为{}", orderDO.getOrderId(), orderDO.getGroupTicketNo());
throw new RuntimeException("创建组合工单失败,已创建过组合工单");
}
//.......code
String ticketNo = compositeTicketService.createInstallTransferTicket(installmentTransferCreateDTO);
log.info("订单{}创建组合工单成功,组合工单工单号为:{}", orderDO.getOrderId(), ticketNo);
//将组合工单号落地
OrderDO orderParam = new OrderDO();
orderParam.setOrderId(orderDO.getOrderId());
orderParam.setGroupTicketNo(ticketNo);
orderOperateDAO.updateOrderByOnDemand(orderParam);
}
但是测试MM发现创建了2条纪录,查询日志发现:第一次请求在写入数据库之前,第二次请求已经执行过了判断逻辑,所以并没有查询到写入纪录,从而导致重复创建。
解决方案,加入分布式锁,第二个版本代码如下
@Transactional
public Boolean createGroupTicket(String orderId){
//防止出现并发情况重复创建
String lockKey = "GroupTicketCreate_" + orderId;
boolean locked = distributedLockService.lock(lockKey, 20);
if(!locked){
log.info("创建组合工单,加锁失败,orderId:{}",orderCustomerDO.getOrderId());
throw new RuntimeException("加锁失败");
}
log.info("加锁成功,lockKey:{}",lockKey);
//......code
//判断逻辑
//.....将组合工单号落地
distributedLockService.releaseLock(lockKey);
log.info("释放锁,lockKey:{}",lockKey);
}
经过此次修改,以为解决了问题,而且正常运行了2天,没有报出重复创建的bug。然而在一次下班之前,又出现了这种情况。
查询日志发现,在第一个请求落地数据库,释放锁后;第二个请求获取锁进来之后,居然通过了判断逻辑。简单的说,就是第一次请求执行完代码之后,并没有写入数据库....WTF。
查询sql日志如下:
20180201:18:19:26.373 [DubboServerHandler-172.17.40.222:20881-thread-132] [org.apache.ibatis.logging.jdbc.BaseJdbcLogger:139] DEBUG ==> Preparing: update cl_order set `order_id` = ?, `date_update` = ?, `group_ticket_no` = ? where order_id = ?
20180201:18:19:26.374 [DubboServerHandler-172.17.40.222:20881-thread-132] [org.apache.ibatis.logging.jdbc.BaseJdbcLogger:139] DEBUG ==> Parameters: 20279(Long), 2018-02-01 18:19:26.373(Timestamp), ZH2018020103201(String), 20279(Long)
20180201:18:19:26.390 [DubboServerHandler-172.17.40.222:20881-thread-132] [org.apache.ibatis.logging.jdbc.BaseJdbcLogger:139] DEBUG <== Updates: 1
20180201:18:19:26.429 [DubboServerHandler-172.17.40.222:20881-thread-135] [org.apache.ibatis.logging.jdbc.BaseJdbcLogger:139] DEBUG ==> Preparing: select a.group_ticket_no as group_ticket_no, a.status_code as statusCode ... from cl_order where order_id = ?
通过sql日志看出,线程132确实更新了数据库,紧接着线程135查询数据库并没有查出数据。结论是:线程132释放锁之后,数据库事务并没有立即提交成功;而线程135获得锁之后的查询逻辑执行在线程132的事务提交之前,打了个时间差,导致创建了2条数据。
终极解决方案,加锁的逻辑加在事务方法的外层。代码如下:
//防止出现并发情况重复创建
String lockKey = "GroupTicketCreate_" + orderId;
boolean locked = distributedLockService.lock(lockKey, 20);
if(!locked){
log.info("创建组合工单,加锁失败,orderId:{}",orderCustomerDO.getOrderId());
throw new RuntimeException("加锁失败");
}
log.info("加锁成功,lockKey:{}",lockKey);
//事务方法
bool flag = orderService.createGroupTicket(orderId);
//.....code
distributedLockService.releaseLock(lockKey);
log.info("释放锁,lockKey:{}",lockKey);