分佈式鎖之Redis實現

一.分佈式鎖簡介

    在分佈式系統之前,系統中的鎖還是單服務器上的鎖,比如鎖住一個進程中的多線程訪問同一資源。如使用synchronized來實現。隨着系統的發展,到後來分佈式應用,有可能同一資源被多臺服務器上的不同進程競爭,這種情況下,出現了今天討論的分佈式鎖

    目前主流的分佈式鎖實現方式主要有下面三種:

  • 基於數據庫的索引和行鎖,數據庫樂觀鎖
  • 基於Redis的單線程原子操作:setNX
  • 基於Zookeeper的臨時有序節點

    這裏主要介紹Redis的實現。鎖需要滿足下面的條件:

  • 互斥性,只有一個客戶端能佔有鎖,具有排他
  • 無死鎖,即使發生有客戶端無法解鎖,也能保證後續其他客戶端能加鎖,應該有超時或者異常情況下,釋放鎖

二.Redis實現

1.使用setnx實現

    先上代碼:

long now = System.currentTimeMillis();
// 使用 setNx 加鎖,保證操作的原子性
boolean result = SUCCESS.equals(jedis.setnx(lockName,String.valueOf(now + expire*1000)));
// 下面的if主要是解決死鎖問題
if(!result){
    String timestamp = jedis.get(lockName);
    // 如果設置過期時間失敗的話,再通過value的時間戳來和當前時間戳比較,防止出現死鎖
    if(timestamp!=null && Long.parseLong(timestamp)<now){
        // 通過 getSet 在發現鎖過期未被釋放的情況下,避免刪除了在這個過程中有可能被其餘的線程獲取到了鎖
        //鎖已過期,獲取上一個鎖的過期時間,並設置現在鎖的過期時間
        String oldValue = jedis.getSet(lockName,String.valueOf(now + expire*1000));
        if(oldValue!=null && oldValue.equals(timestamp)){
            result = true;
            jedis.expire(lockName,expire);
        }
    }
}
if(result){
    jedis.expire(lockName,expire);
}
return result;

    使用 setnx 實現加鎖,其中key是鎖,value是鎖的過期時間。加了時間戳比較,防止出現死鎖情況。
    細心的朋友可能也會發現還是存在下面的問題:

  • 時間戳同步問題。客戶端自己生成過期時間,所以需要強制要求分佈式下每個客戶端的時間必須同步
  • 當鎖過期的時候,如果多個客戶端同時執行 getSet ,最終只有一個客戶端可以加鎖,但是這個客戶端的鎖的過期時間可能被其他客戶端覆蓋
  • 鎖不具備擁有者標識,即任何客戶端都可以解鎖

    所以就有了第2種實現方式。

2.使用set "NX" "PX"實現

    實現如下:

public static final String SET_IF_NOT_EXIST = "NX";
public static final String SET_WITH_EXPIRE_TIME = "PX";
// "NX" 當key不存在時,我們進行set操作;若key已經存在,則不做任何操作
//"PX" 給key加一個過期時間
String result = jedis.set(lockName, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expire*1000);
if(LOCK_SUCCESS.equals(result)){
    return true;
}
return false;

3.解鎖實現

    解鎖的時候,主要思想就是在redis裏面執行一段Lua腳本。

    爲什麼使用Lua腳本?

    保證操作的原子性。eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,並且直到eval命令執行完成,Redis纔會執行其他命令。另外,開源Redisson中的分佈式鎖就是Lua實現,我後面會有一篇文章分析Redisson分佈式鎖的實現。

// KEYS[1] lockName 鎖名稱 ARGV[1] requestId
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockName), Collections.singletonList(requestId));
if (SUCCESS.equals(result)) {
    return true;
}
return false;

三.其他Redis實現

    Redisson提供了一種可重入的分佈式鎖的實現--RedissonLock。

    使用示例:

RLock dlock = client.getLock(lockName);
boolean result = false;
try {
    // expire 等待時間 leaseTime 超時時間
    result = dlock.tryLock(expire, 20, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    if (dlock.isLocked()) {
        dlock.unlock();
    }
}
return result;

    相關的實現和示例代碼放到了Github上,redis-usage.

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