Redis 實現分佈式鎖

在單節點狀況下,實現線程安全須要靠同步狀態來控制。而在分佈式應用中,使程序正確執行不被併發問題影響,就須要分佈式鎖來控制。html

在單節點中,須要用一個併發線程都能訪問到的資源的狀態變化來控制同步。在分佈式應用中,使用應用全部節點都能訪問到的 Redis 中的某個 key 來控制併發問題。java

單節點 Redis 分佈式鎖

setnx

setnx 指令會在 key 不存在的狀況下放入 redis,若是存在則不會設置。redis

>setnx lock:distributed true
OK
...
other code
...
>del lock:distributed
複製代碼

這種方式的問題在於,執行到 other code 時,程序出現異常,致使 del 指令不會被執行,key 沒有被釋放,這樣會陷入死鎖。算法

setnx then expire

爲了解決死鎖,乍一看可使用 expire 來給 key 設置超時時間。安全

>setnx lock:distributed true
OK
>expire lock:distributed 5
...
other code
...
>del lock:distributed
複製代碼

這種處理其實仍然有問題,由於 setnxexpire 不是原子操做, 執行 expire 語句以前可能發生異常。死鎖仍然會出現。bash

set and expire

爲了解決非原子性操做被中斷的問題,在 Redis 2.8 中加入了 setnxexpire 組合在一塊兒的原子指令。併發

>set lock:distributed true ex 5 nx
OK
...
other code
...
>del lock:distributed
複製代碼

這種方式保證了加鎖並設置有效時間操做的原子性,可是依然有問題。dom

假設咱們在加鎖與釋放鎖之間的業務代碼執行時間超過了設置的有效時間,此時鎖會由於超時被釋放。會致使兩種狀況:異步

  1. 其餘節點 B 獲取鎖以後,執行超時節點 A 執行完成,釋放了 B 的鎖。
  2. 其它節點獲取到了鎖,執行臨界區代碼時就可能會出現併發問題。

解決鎖被其餘線程釋放問題

由於在加鎖時,各個節點使用的同一個 key,因此會存在超時節點釋放了當前加鎖節點的鎖的狀況。這種狀況下,能夠給加鎖的 key 設置一個隨機值,刪除的時候須要判斷 key 當前的 value 是否是等於隨機值。分佈式

val = Random.nextInt();
if( redis.set(key,val,true,5) ){
	...
	other code
	...
	value = redis.get(key);
	if(val == value){
		redis.delete(key);
	}
}
複製代碼

上述代碼實現了根據隨機值刪除的邏輯,可是獲取 value 直到 delete 指令並不是是原子指令,仍然可能有併發問題。這時候須要使用 lua 腳本處理,由於 lua 腳本能夠保證連續多個指令原子執行。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
複製代碼

這種方式能夠避免鎖被其餘線程釋放的問題。

臨界區併發問題

臨界區代碼出現併發問題的本質是業務代碼執行時間大於鎖過時時間。

咱們能夠定時刷新加鎖時間,保證業務代碼在鎖過時時間內執行完成。

private volatile boolean isFlushExpiration = true;

while(redis.set(lock, val, NOT_EXIST, SECONDS, 20)){
    Thread thread = new Thread(new FlushExpirationTherad());
	thread.setDeamon(true);
  	thread.start();
    ...
    other code
    ... 
}

isFlushExpiration = false;
String deleteScript = "if redis.call("get",KEYS[1]) == ARGV[1] then" 
    + "return redis.call("del",KEYS[1])"
    + "else return 0 end";
redis.eval(deleteScript,1,key,val);
    

private class FlushExpirationTherad implements Runnable{
    @Override
	public void run(){
        while(isFlushExpiration){
            String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else return 0 end";
            redis.eval(checkAndExpireScript,1,key,val,"20");
            // 每隔十秒檢查是否完成
            Thread.sleep(10);
        }
    }    
}
複製代碼

這種實現是用一個線程按期監控客戶端是否執行完成。也能夠由服務端實現心跳檢測機制來保證業務完成(Zookeeper)。

因此實現單節點 Redis 分佈式鎖要關注三個關鍵問題:

  1. 獲取鎖與設置超時時間實現爲原子操做(Redis2.8 開始已支持)
  2. 設置隨機字符串保證釋放鎖時能保證只釋放本身持有的鎖(給對應的 key 設置隨機值)
  3. 判斷與釋放鎖必須實現爲原子操做(lua 腳本實現)

多節點 Redis 分佈式鎖

爲了保證項目的高可用性,項目通常都配置了 Redis 集羣,以防在單節點 Redis 宕機以後,全部客戶端都沒法得到鎖。

在集羣環境下,Redis 存在 failover 機制。當 Master 節點宕機以後,會開始異步的主從複製(replication),這個過程可能會出現如下狀況:

  1. 客戶端 A 獲取了 Master 節點的鎖。
  2. Master 節點宕機了,存儲鎖的 key 暫未同步到 Slave 上。
  3. Slave 節點升級爲 Master 節點。
  4. 客戶端 B 重新的 Master 節點上獲取到了同一資源的鎖。

在這種狀況下,鎖的安全性就會被打破,Redis 做者 antirez 針對此問題設計了 Redlock 算法。

Redlock 算法

Redlock 算法獲取鎖時客戶端執行步驟:

  1. 獲取當前時間(start)。
  2. 依次向 N 個 Redis 節點請求鎖。請求鎖的方式與從單節點 Redis 獲取鎖的方式一致。爲了保證在某個 Redis 節點不可用時該算法可以繼續運行,獲取鎖的操做都須要設置超時時間,須要保證該超時時間遠小於鎖的有效時間。這樣才能保證客戶端在向某個 Redis 節點獲取鎖失敗以後,能夠馬上嘗試下一個節點。
  3. 計算獲取鎖的過程總共消耗多長時間(consumeTime = end - start)。若是客戶端從大多數 Redis 節點(>= N/2 + 1) 成功獲取鎖,而且獲取鎖總時長沒有超過鎖的有效時間,這種狀況下,客戶端會認爲獲取鎖成功,不然,獲取鎖失敗。
  4. 若是最終獲取鎖成功,鎖的有效時間應該從新設置爲鎖最初的有效時間減去 consumeTime
  5. 若是最終獲取鎖失敗,客戶端應該馬上向全部 Redis 節點發起釋放鎖的請求。

在釋放鎖時,須要向全部 Redis 節點發起釋放鎖的操做,無論節點是否獲取鎖成功。由於可能存在客戶端向 Redis 節點獲取鎖時成功,但節點通知客戶端時通訊失敗,客戶端會認爲該節點加鎖失敗。

Redlock 算法實現了更高的可用性,也不會出現 failover 時失效的問題。可是若是有節點崩潰重啓,仍然對鎖的安全性有影響。假設共有 5 個 Redis 節點 A、B、C、D、E:

  1. 客戶端 A 獲取了 A、B、C 節點的鎖,但 D 與 E 節點的鎖獲取失敗。
  2. 節點 C 崩潰重啓,可是客戶端 A 在 C 上加的鎖沒有持久化下來,重啓後丟失
  3. 節點 C 重啓後,客戶端 B 鎖住了 C、D、E,獲取鎖成功。

在這種狀況下,客戶端 A 與 B 都獲取了訪問同一資源的鎖。

這裏第 2 步中節點 C 鎖丟失的問題可能由多種緣由引發。默認狀況下,RedisAOF 持久化方式是每秒寫一次磁盤(fsync),這狀況下就有可能丟失 1 秒的數據。咱們也能夠設置每次操做都觸發 fsync,這會影響性能,不過即便這樣設置,也有可能因爲操做系統的問題致使操做寫入失敗。

爲了解決節點重啓致使的鎖失效問題,antirez 提出了延遲重啓的概念,即當一個節點崩潰以後並不當即重啓,而是等待與分佈式鎖相關的 key 的有效時間都過時以後再重啓,這樣在該節點重啓後也不會對現有的鎖形成影響。

一些插曲

關於 Redlock 的安全性問題,在分佈式系統專家 Martin Kleppmann 和 Redis 的做者 antirez 之間發生過一場爭論,這個問題引起了激烈的討論。關於這場爭論的內容能夠關注 基於Redis的分佈式鎖到底安全嗎 這篇文章。 最後得出的結論是 Redlock 在效率要求的應用中是合理的,因此在 Java 項目中可使用 RedlockJava 版本 Redission 來控制多節點訪問共享資源。可是仍有極端狀況會形成 Redlock 的不安全,咱們應該知道它在安全性上有哪些不足以及會形成什麼後果。若是須要進一步的追求正確性,可使用 Zookeeper 分佈式鎖。

相關連接

相關文章
相關標籤/搜索