使用redis+lua腳本實現分佈式鎖

分佈式鎖的應用場景

當多個機器(多個進程)會對同一條數據進行修改時,並且要求這個修改是原子性的。這裏有兩個限定:

  • 多個進程之間的競爭,意味着JDK自帶的鎖失效;
  • 原子性修改,意味着數據是有狀態的,修改前後有依賴。

分佈式鎖的實現條件:

  • 高性能(加、解鎖時高性能)
  • 可以使用阻塞鎖與非阻塞鎖。
  • 不能出現死鎖。
  • 可用性(不能出現節點 down 掉後加鎖失敗)。

Redis使用分佈式鎖

本文將先介紹Redis的實現方式,當然Redis實現分佈式鎖有多種方案,本文介紹一種基於lua腳本的實現方案。後面筆者會介紹分佈式鎖的其他實現(基於 DB 的唯一索引和基於 ZK 的臨時有序節點)。

本方案中Redis的實現主要基於setnx 和給予一個超時時間(防止釋放鎖失敗)。
多個嘗試獲取鎖的客戶端使用同一個key做爲目標數據的唯一鍵,value爲鎖的期望超時時間點;
首先進行一次setnx命令,嘗試獲取鎖,如果獲取成功,則設置鎖的最終超時時間(以防在當前進程獲取鎖後奔潰導致鎖無法釋放)

這裏利用 Redis set key 時的一個 NX 參數可以保證在這個 key 不存在的情況下寫入成功。並且再加上 EX 參數可以讓該 key 在超時之後自動刪除。

注意:此處使用Jedis的如下方法,該命令可以保證 NX EX 的原子性。

一定不要把兩個命令(NX EX)分開執行,如果在 NX 之後程序出現問題就有可能產生死鎖。

String set(String key, String value, String nxxx, String expx, long time);

非阻塞鎖

public  boolean tryLock(String key, String request) {
    String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
    if (LOCK_MSG.equals(result)){
        return true ;
    }else {
        return false ;
    }
}

阻塞鎖

同時也可以實現一個阻塞鎖:

//一直阻塞
public void lock(String key, String request) throws InterruptedException {
    for (;;){
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
        if (LOCK_MSG.equals(result)){
            break ;
        }
 //防止一直消耗 CPU 	
        Thread.sleep(DEFAULT_SLEEP_TIME) ;
    }
}
 //自定義阻塞時間
 public boolean lock(String key, String request,int blockTime) throws InterruptedException {
    while (blockTime >= 0){
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
        if (LOCK_MSG.equals(result)){
            return true ;
        }
        blockTime -= DEFAULT_SLEEP_TIME ;
        Thread.sleep(DEFAULT_SLEEP_TIME) ;
    }
    return false ;
}

解鎖

解鎖也很簡單,其實就是把這個 key 刪掉就萬事大吉了,比如使用 del key 命令。

但現實往往沒有那麼 easy。

如果進程 A 獲取了鎖設置了超時時間,但是由於執行週期較長導致到了超時時間之後鎖就自動釋放了。這時進程 B 獲取了該鎖執行很快就釋放鎖。這樣就會出現進程 B 將進程 A 的鎖釋放了。

所以最好的方式是在每次解鎖時都需要判斷鎖是否是自己的。

這時就需要結合加鎖機制一起實現了。

加鎖時需要傳遞一個參數,將該參數作爲這個 key 的 value,這樣每次解鎖時判斷 value 是否相等即可。

爲了更好的健壯性,將該操作封裝爲一個lua腳本,這樣即可保證其原子性

public  boolean unlock(String key,String request){
    //lua script
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = null ;
    if (jedis instanceof Jedis){
        result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
    }else if (jedis instanceof JedisCluster){
        result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
    }else {
        //throw new RuntimeException("instance is error") ;
        return false ;
    }
    if (UNLOCK_MSG.equals(result)){
        return true ;
    }else {
        return false ;
    }
}

該方案的缺點

key 超時之後業務並沒有執行完畢但卻自動釋放鎖了

如運行在Redis的集羣,當進程1對master節點寫入了鎖,此時master節點宕機。slave節點提升爲master而剛剛寫入master的鎖還未同步,此時進程2也將能夠獲取鎖成功。

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