這事第一個版本的代碼邏輯,通過數據庫中是否有紀錄來防止重復創建
@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);