这事第一个版本的代码逻辑,通过数据库中是否有纪录来防止重复创建

@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。然而在一次下班之前,又出现了这种情况。

image

查询日志发现,在第一个请求落地数据库,释放锁后;第二个请求获取锁进来之后,居然通过了判断逻辑。简单的说,就是第一次请求执行完代码之后,并没有写入数据库....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);

总结:确保事务执行完之后释放锁!