記錄一次並發導致數據重復創建的問題


這事第一個版本的代碼邏輯,通過數據庫中是否有紀錄來防止重復創建

@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);

總結:確保事務執行完之后釋放鎖!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM