分佈式鎖——Redis實現

分佈式鎖是在分佈式場景中,實現共享資源互斥訪問的一種方式。Java中synchronized或ReentrantLock只能保證在一個jvm中的最多隻有一個線程可以獲取資源的鎖,但是如果是分佈式場景,會有多個jvm中各自的線程都會競爭共享資源。這時synchronized或ReentrantLock就無能爲力,就需要使用分佈式鎖。

分佈式鎖的特點

  • 互斥性:同一時間只能有一個線程獲取鎖
  • 可重入性:一個線程獲取鎖之後,可以再次獲取鎖
  • 鎖超時:支持鎖超時,防止死鎖

分佈式鎖的實現方式

  • redis
  • zookeeper
  • 數據庫

本文使用第一種redis實現方式。

用Redis實現分佈式鎖

網上流傳的一種用redis實現分佈式鎖的方法,直接使用setnx和expire兩條命令,其實是錯誤,因爲這兩條命令的執行不是原子的,所以無法保證setnx加鎖後,expire設置鎖超時時間一定成功,這種情況就會出現死鎖。

一、使用set命令實現

從redis 2.6.12版本起,set命令增加了一種支持多個參數的格式

set key value [EX seconds] [PX miliseconds] [NX|XX]

EX: 以秒爲單位的過期時間

PX:以毫秒爲單位的過期時間

NX:if not exists的縮寫,表示當key不存在時,才設置值

XX:if exists的縮寫,表示當key存在時,才設置值

public class DistributedLock{

    private static final String SET_IF_NOT_EXISTS = "NX";

    private static final String EXPIRE = "EX";
    
    private static final String LOCK_SUCCESS = "OK";

    //返回true表示獲取鎖成功
    public boolean lock(String key, String sessionId, int expiredSeconds){
        return LOCK_SUCCESS.equals(jedis.set(key, sessionId, SET_IF_NOT_EXISTS, EXPIRE, expiredSeconds);
    }

    //返回true表示釋放成功
    public boolean releaseLock(String key, String value){
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L));
    }
}

這裏的key一般是資源的名稱,sessionId要求在分佈式場景中是唯一的,比如會話id,可以使用uuid實現。因爲在釋放鎖時會比較value的值,保證不會錯誤地釋放其他鎖。釋放鎖使用lua腳本,保證原子性。

這種方式適用於單節點redis的情況。如果是redis集羣,比如說A客戶端在Redis的master節點上拿到了鎖,但是這個加鎖的key還沒有同步到slave節點,master故障,發生故障轉移,一個slave節點升級爲master節點,B客戶端也可以獲取同個key的鎖,但客戶端A也已經拿到鎖了,這就導致多個客戶端都拿到鎖。這種情況需要其他方式處理。

二、使用lua腳本實現

使用lua腳本加鎖可以保證原子性,釋放鎖的方法與上面相同

public boolean getLockWithLua(String key, String uniqueId, int seconds) {
    String luaScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    //判斷是否成功
    return result.equals(1L);
}

本文參考以下博文,感謝原作者

https://juejin.im/post/5cc165816fb9a03202221dd5

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