【Redis場景5】集羣秒殺優化-分佈式鎖

集羣環境下的秒殺問題

前序

【Redis場景1】用戶登錄註冊

【Redis場景2】緩存更新策略(雙寫一致)

【Redis場景3】緩存穿透、擊穿問題

【Redis場景拓展】秒殺問題-全局唯一ID生成策略

【Redis場景4】單機環境下秒殺問題


在單機環境下的併發問題,我們可以使用相關鎖來解決;但是在集羣環境中,筆者測試通過Nginx做的反向代理和負載均衡,請求的時候鎖會出現失效的問題。

原因:我們部署多個服務(存在多個tomcat服務器),每個tomcat都有一個屬於自己的jvm.每個鎖在同容器中有效,但是跨容器後就無法實現互斥效果。

引出分佈式鎖:

  1. 分佈式就是指數據和程序可以不位於一個服務器上,而是分散到多個服務器,以網絡上分散分佈的地理信息數據及受其影響的數據庫操作爲研究對象的一種理論計算模型。
  2. 分佈式鎖提供了多個服務器節點訪問共享資源互斥的一種手段。

一個最基本的分佈式鎖需要滿足:

  • 互斥 :任意一個時刻,鎖只能被一個線程持有;
  • 高可用 :鎖服務是高可用的。並且,即使客戶端的釋放鎖的代碼邏輯出現問題,鎖最終一定還是會被釋放,不會影響其他線程對共享資源的訪問。
  • 可重入:一個節點獲取了鎖之後,還可以再次獲取鎖

分佈式鎖的實現:

  1. 基於redis中的SETNX 實現分佈式鎖
  2. 基於Zookeeper的節點唯一性和有序性實現互斥的分佈式鎖
  3. 基於MySQL本身的互斥鎖機制

基於Redis的分佈式鎖

基本實現

GitHub完整代碼:https://github.com/xbhog/hm-dianping/tree/20230211-xbhog-redisCloud

鎖接口實現:20230211-xbhog-redisCloud

/**
 * @author xbhog
 * @describe:
 * @date 2023/2/16
 */
public interface ILock {

    boolean tryLock(Long timeOutSec);

    void unLock();
}

加鎖解鎖實現類:

@Override
public boolean tryLock(Long timeOutSec) {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + keyName, threadId + "", timeOutSec, TimeUnit.SECONDS);
    //防止拆箱引發空值異常
    return Boolean.TRUE.equals(isLock);
}
@Override
public void unlock() {
    //通過del刪除鎖
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

鎖誤刪問題

img

現在有兩個鎖,線程1獲取鎖時,由於業務的阻塞超時釋放了,這是線程2開始操作,獲取鎖,在線程2執行業務期間,線程1業務在一段時間內不阻塞且業務完成,這是開始執行釋放鎖的操作,但是這是鎖是線程2,由此造成鎖的誤刪問題;

正確流程:

img

解決的方式:

修改之前的分佈式鎖實現,滿足:在獲取鎖時存入線程標示(可以用UUID表示) 在釋放鎖時先獲取鎖中的線程標示,判斷是否與當前線程標示一致

  • 如果一致則釋放鎖
  • 如果不一致則不釋放鎖

核心邏輯:在存入鎖時,放入自己線程的標識,在刪除鎖時,判斷當前這把鎖的標識是不是自己存入的,如果是,則進行刪除,如果不是,則不進行刪除。

處理流程:

img

代碼實現:

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(Long timeOutSec) {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + keyName, threadId + "", timeOutSec, TimeUnit.SECONDS);
    //防止拆箱引發空值異常
	return Boolean.TRUE.equals(isLock);
}
@Override
public void unLock() {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    //獲取當前分佈式鎖中的value
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + keyName);
    //鎖相同則刪除
    if(threadId.equals(id)){
        stringRedisTemplate.delete(KEY_PREFIX + keyName);
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章