MyBatis一級緩存引起的無窮遞歸


引言:

  最近在項目中參與了一個領取優惠劵的活動,當多個用戶領取同一張優惠劵的時候,使用了數據庫鎖控制並發,起初的設想是:如果多個人同時領一張劵,第一個到達的人領取成功,其它的人繼續查找是否還有剩余的劵,如果有,繼續領取,否則領取失敗。在實現中,我一開始使用了遞歸的方式去查找劵,實際的測試中發現出現了無窮遞歸,通過degug和查閱資料才發現這是由於mybatis的一級緩存引起的,以下將這次遇到的問題和大家分享討論。

1.涉及到的知識點

Mybatis緩存:

一級緩存:默認開啟,sqlSession級別緩存,當前會話中有效,執行sqlSession commit()、close()、clearCache()操作會清除緩存。[1]

二級緩存:需要手工開啟,全局級別緩存,與mapper namespace相關。[1]

並發控制機制:

悲觀鎖:假定會發生並發沖突屏蔽一切可能違反數據完整性的操作。[2]

樂觀鎖:假設不會發生並發沖突,只在提交操作時檢查是否違反數據完整性。[2] 樂觀鎖不能解決臟讀的問題。

樂觀鎖適用於寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。

2.代碼

  以下是一個領取優惠劵的輔助方法-隨機抽取一張優惠碼,調用這個輔助方法的public方法開啟了事務(開啟了sqlSession)。實際測試的過程中發現,當數據庫中只有一張優惠劵並且同時被多個用戶領取時,會出現無窮遞歸。代碼如下:

 1 /**
 2      * 隨機抽取一張優惠碼
 3      * 
 4      * @param codePrefix
 5      *            優惠碼前綴
 6      * @return 優惠碼 9      */
10     private String randExtractOneTicketCode(String mobile, String codePrefix) {
11         List<String> notExchangeCodeList = yzTicketCodeDaoExt.getTicketCodeList(codePrefix,
12                 MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE);
13         logger.info("領取優惠劵>>>優惠劵可用數量{}",CollectionUtils.size(notExchangeCodeList));
14         if (CollectionUtils.isEmpty(notExchangeCodeList)) {
15             logger.warn("領取優惠劵>>>優惠劵{}已領完", codePrefix);
16             throw new YzRuntimeException(MobileServiceConstants.TICKET_NOT_REMAINDER);
17         }
18 
19         int randomIndex = random.nextInt(notExchangeCodeList.size()); // 隨機的索引
20         String ticketCode = notExchangeCodeList.get(randomIndex); // 隨機選擇的優惠碼
21         YzTicketCode ticketCodeObj = yzTicketCodeDaoExt.getByCode(ticketCode);
22         if (ticketCodeObj == null
23                 || ticketCodeObj.getStatus() != MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE) {
24             // 如果優惠劵已被使用
25             logger.info("領取優惠劵>>>優惠劵碼{}不存在或已被使用",ticketCode);
26             return randExtractOneTicketCode(String mobile, String codePrefix);  //遞歸查找
27         }
28         /*
29          * 更新優惠碼狀態
30          */
31         ticketCodeObj.setExchangeTime(Calendar.getInstance().getTime());
32         ticketCodeObj.setStatus(MobileServiceConstants.TICKET_CODE_STATUS_HAD_EXCHANGED);
33         ticketCodeObj.setMobile(mobile);
34         int updateCnt = yzTicketCodeDaoExt.update4Receive(ticketCodeObj);
35         if(updateCnt <= 0){
36             //樂觀鎖,沒有影響到行,表明更新失敗,可能是該劵不存在或已被使用
37             logger.info("領取優惠劵>>>優惠劵碼{}不存在或已被使用",ticketCode);
38             return randExtractOneTicketCode(String mobile, String codePrefix);  //遞歸查找,發現這里出現了循環遞歸
39         };
40         return ticketCode;
41     }

  通過debug發現,第38行出現了循環遞歸,原因是第11行執行的查詢結果被mybatis一級緩存緩存了,導致每次查詢的結果都是第一次查詢的結果(有一張劵可以被領取),但實際上這張劵已經被其它用戶領取了,從而發生了無窮遞歸。

 3.解決方案

1)編程式事務,通過transactionManager來獲取sqlSession,然后通過sqlSession的clearCache()方法來清除一級緩存。

2)由於項目中使用了Spring申明式事務,並且並發量不高,考慮到減少復雜度,選擇了簡單的方法,直接提示用戶系統繁忙。

/**
     * 隨機抽取一張優惠碼
     * 
     * @param codePrefix
     *            優惠碼前綴
     * @return 優惠碼
     * @throws YzRuntimeException
     *             如果沒有可用的優惠劵
     */
    private String randExtractOneTicketCode(String mobile, String codePrefix) {
        List<String> notExchangeCodeList = yzTicketCodeDaoExt.getTicketCodeList(codePrefix,
                MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE);
        logger.info("領取優惠劵>>>優惠劵可用數量{}",CollectionUtils.size(notExchangeCodeList));
        if (CollectionUtils.isEmpty(notExchangeCodeList)) {
            logger.warn("領取優惠劵>>>優惠劵{}已領完", codePrefix);
            throw new YzRuntimeException(MobileServiceConstants.TICKET_NOT_REMAINDER);
        }

        int randomIndex = random.nextInt(notExchangeCodeList.size()); // 隨機的索引
        String ticketCode = notExchangeCodeList.get(randomIndex); // 隨機選擇的優惠碼
        YzTicketCode ticketCodeObj = yzTicketCodeDaoExt.getByCode(ticketCode);
        if (ticketCodeObj == null
                || ticketCodeObj.getStatus() != MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE) {
            // 如果優惠劵已被使用
            logger.info("領取優惠劵>>>優惠劵碼{}不存在或已被使用",ticketCode);
            throw new YzRuntimeException(MobileServiceConstants.TICKET_SYSTEM_BUSY);
        }
        /*
         * 更新優惠碼狀態
         */
        ticketCodeObj.setExchangeTime(Calendar.getInstance().getTime());
        ticketCodeObj.setStatus(MobileServiceConstants.TICKET_CODE_STATUS_HAD_EXCHANGED);
        ticketCodeObj.setMobile(mobile);
        int updateCnt = yzTicketCodeDaoExt.update4Receive(ticketCodeObj);
        if(updateCnt <= 0){
            //樂觀鎖,沒有影響到行,表明更新失敗,可能是該劵不存在或已被使用
            logger.info("領取優惠劵>>>優惠劵碼{}不存在或已被使用",ticketCode);
            throw new YzRuntimeException(MobileServiceConstants.TICKET_SYSTEM_BUSY);
        };
        return ticketCode;
    }

總結:

  現在項目大多使用集群的方式,使用java提供的並發機制已經無法控制並發,常用的是數據庫鎖和Redis提供的並發控制機制,上面代碼中使用了數據庫的樂觀鎖,樂觀鎖相比於悲劇鎖而言,需要編寫外部算法,錯誤的外部算法和異常恢復容易導致未知的錯誤,需要謹慎的設計和嚴格的測試。

參考文檔:

[1]http://www.mamicode.com/info-detail-890951.html

[2]Concurrent Control http://en.wikipedia.org/wiki/Concurrency_control

 


免責聲明!

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



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