Redis - 實現分佈式鎖的階段演進

①演進階段一

獲得鎖就執行業務邏輯,沒有獲得鎖就繼續調用這個方法形成一個自旋,就類似於synchronized
在這裏插入圖片描述

僞代碼:
public void getData(){
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
    if(lock){
        // 執行業務..
        
        // 刪除鎖
        redisTemplate.delete("lock");  
    }else{
         // 休眠一段時間
        // 繼續調用getData,等待鎖的釋放
        getData();
    }
}
存在問題:

setnx佔好了位,業務代碼異常或者程序在頁面過程中宕機。沒有執行刪除鎖邏輯,這就造成了死鎖

解決方法:

給鎖設置一個過期時間,即使代碼異常或是程序宕機,鎖都會因爲過期時間到了自動刪除。

②演進階段二

解決階段一的問題。
在這裏插入圖片描述

僞代碼:
public void getData(){
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
    if(lock){
        // 設置過期時間
        redisTemplate.expire("lock",180,TimeUnit.SECONDS);
        
        // 執行業務..
        
        // 刪除鎖
        redisTemplate.delete("lock");  
    }else{
        // 休眠一段時間
        // 繼續調用getData,等待鎖的釋放
        getData();
    }
}
存在問題:

獲得鎖之後,正要去設置過期時間,這是服務宕機/斷電,這個時間還沒設置上去,又導致了死鎖。

解決方法:

這就需要保證設置value和過期時間是原子性操作,這就需要使用Redis的setnx ex命令。
原來將鍵key設定爲指定的“字符串”值,如果 key 已經保存了一個值,那麼這個操作會直接覆蓋原來的值,並且忽略原始類型。
在2.6.12版本開始,redis爲SET命令增加了一系列選項:

EX seconds              設置鍵key的過期時間,單位時秒
PX milliseconds         設置鍵key的過期時間,單位時毫秒
NX                      只有鍵key不存在的時候纔會設置key的值
XX                      只有鍵key存在的時候纔會設置key的值
命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX]

③演進階段三

解決階段二的問題。
在這裏插入圖片描述

僞代碼:
public void getData(){
    // 設置了過期時間 - 180s
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
    if(lock){
        // 執行業務...
        
        // 刪除鎖
        redisTemplate.delete("lock");  
    }else{
        // 休眠一段時間
        // 繼續調用getData,等待鎖的釋放
    }
}
存在問題:

如果由於業務時間很長,鎖自己過期了,我們直接刪除,有可能把別人正在持有的鎖刪除了。

場景:

比如我們執行一個業務,線程1拿到了Redis鎖,Redis鎖過期時間是180s,我們這個業務卻執行了300s,也就是說,我們執行到180s的時候,鎖已經過期了,那另外一個線程2拿到了鎖,等我們線程1業務執行完的時候,線程2業務還在執行中,線程1要去刪鎖,這時刪除的鎖其實是線程2的。

解決方法:

使用隨機的大字符串作爲value值,刪除前先對比value值是否相同,相同就刪除。

④演進階段四

解決階段三的問題。
在這裏插入圖片描述

僞代碼:
public void getData(){
    // 使用UUID生成不重複值
    String uuid = UUID.randomUUID().toString();
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        // 執行業務...
        
        // 判斷uuid是否相同
        String str = redisTemplate.get("lock");
       if(uuid.equals(str){ // 相同,就刪除鎖
           redisTemplate.delete("lock"); 
       }
    }else{
        // 休眠一段時間
        // 繼續調用getData,等待鎖的釋放
    }
}
解決方法:

保證對比value值和刪除value值是一個原子性操作,使用Redis+Lua腳本來完成。

⑤演進階段五

解決階段四的問題,最終形態。
在這裏插入圖片描述

僞代碼:
public void getData(){
    // 使用UUID生成不重複值
    String uuid = UUID.randomUUID().toString();
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        try{
            // 執行業務...
        }finally{
            // Lua腳本 - 2個參數 key和value
           String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
           Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
       }
    }else{
        // 休眠一段時間
        // 繼續調用getData,等待鎖的釋放
    }
}
存在問題:

鎖的過期時間,如果業務沒執行完,鎖應該續期,最簡單的解決方法就是把鎖的過期時間設置大一點。

分佈式鎖框架

分佈式鎖框架Redisson,基於Redis實現的各種分佈式鎖。
Redisson的GitHub地址

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