Zookeeper 和 Redis 實現分佈式鎖,附我的可靠性分析

作者:今天你敲代碼了嗎
鏈接:https://www.jianshu.com/p/b6953745e341

在分佈式系統中,爲保證同一時間只有一個客戶端可以對共享資源進行操作,需要對共享資源加鎖來實現,常見有三種方式:

高併發下數據庫鎖性能太差,本文不做探究。僅針對 Redis 和 Zookeeper 實現的分佈式鎖進行分析。

實現一個分佈式鎖應該具備的特性:

先上結論,Redis 在鎖時間限制和緩存一致性存在一定問題,Zookeeper 在可靠性上強於 Redis,只是效率相對較低,開發人員需要根據實際需求進行技術選型。

單機情況下:

1. Redis 單機實現分佈式鎖

1.1 Redis 加鎖

//SET resource_name my_random_value NX PX 30000
String result = jedis.set(key, value, "NX""PX", 30000);
if ("OK".equals(result)) {
       return true; //代表獲取到鎖
}
return false;

加鎖就一行代碼:jedis.set(String key, String value, String nxxx, String expx, int time),這個 set() 方法一共有五個形參:

1.2 Redis 解鎖

解鎖時,爲了防止客戶端 1 獲得的鎖,被客戶端 2 給釋放,需要採用的 Lua 腳本來釋放鎖:

final Long RELEASE_SUCCESS = 1L;
//採用Lua腳本來釋放鎖
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(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
    return true;
}
return false;

在執行這段 Lua 腳本的時候,KEYS[1] 的值爲 key,ARGV[1] 的值爲 value。原理就是先獲取鎖對應的 value 值,保證和客戶端傳進去的 value 值相等,這樣就能避免自己的鎖被其他人釋放。另外,採取 Lua 腳本操作保證了原子性。如果不是原子性操作,則有了下述情況出現:

1.3 Redis 加鎖過期時間設置問題

理想情況是客戶端 Redis 加鎖後,完成一系列業務操作,順利在鎖過期時間前釋放掉鎖,這個分佈式鎖的設置是有效的。但是如果客戶端在操作共享資源的過程中,因爲長期阻塞的原因,導致鎖過期,那麼接下來訪問共享資源就變得不再安全。

2. Zookeeper 單機實現分佈式鎖

2.1 Curator 實現 Zookeeper 加解鎖

使用 Apache 開源的 curator 可實現 Zookeeper 分佈式鎖。

可以通過調用 InterProcessLock 接口提供的幾個方法來實現加鎖、解鎖。

/**
* 獲取鎖、阻塞等待、可重入
*/
public void acquire() throws Exception;

/**
* 獲取鎖、阻塞等待、可重入、超時則獲取失敗
*/
public boolean acquire(long time, TimeUnit unit) throws Exception;

/**
* 釋放鎖
*/
public void release() throws Exception;

/**
* Returns true if the mutex is acquired by a thread in this JVM
*/
boolean isAcquiredInThisProcess();

2.2 Zookeeper 加鎖實現原理

Zookeeper 的分佈式鎖原理是利用了臨時節點 (EPHEMERAL) 的特性。其實現原理:

由於節點的臨時屬性,如果創建 znode 的那個客戶端崩潰了,那麼相應的 znode 會被自動刪除。這樣就避免了設置過期時間的問題。

2.3 GC 停頓導致臨時節點釋放問題

但是使用臨時節點又會存在另一個問題:Zookeeper 如果長時間檢測不到客戶端的心跳的時候 (Session 時間),就會認爲 Session 過期了,那麼這個 Session 所創建的所有的 ephemeral 類型的 znode 節點都會被自動刪除。

如上圖所示,客戶端 1 發生 GC 停頓的時候,Zookeeper 檢測不到心跳,也是有可能出現多個客戶端同時操作共享資源的情形。當然,你可以說,我們可以通過 JVM 調優,避免 GC 停頓出現。但是注意了,我們所做的一切,只能儘可能避免多個客戶端操作共享資源,無法完全消除。

集羣情況下:

3. Redis 集羣下分佈式鎖存在問題

3.1 集羣 Master 宕機導致鎖丟失

爲了 Redis 的高可用,一般都會給 Redis 的節點掛一個 slave, 然後採用哨兵模式進行主備切換。但由於 Redis 的主從複製(replication)是異步的,這可能會出現在數據同步過程中,master 宕機,slave 來不及同步數據就被選爲 master,從而數據丟失。具體流程如下所示:

3.2 Redlock 算法

爲了應對這個情形, Redis 作者 antirez 基於分佈式環境下提出了一種更高級的分佈式鎖的實現方式:Redlock

antirez 提出的 redlock 算法大概是這樣的:

在 Redis 的分佈式環境中,我們假設有 N 個 Redis master。這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制。我們確保將在 N 個實例上使用與在 Redis 單實例下相同方法獲取和釋放鎖。現在我們假設有 5 個 Redis master 節點 (官方文檔裏將 N 設置成 5,其實大等於 3 就行),同時我們需要在 5 臺服務器上面運行這些 Redis 實例,這樣保證他們不會同時都宕掉。

爲了取到鎖,客戶端應該執行以下操作:

redisson 已經有對 redlock 算法封裝,如下是調用代碼示例:

Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379""127.0.0.1:6389")
        .setMasterName("masterName")
        .setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 還可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
    isLock = redLock.tryLock();
    // 500ms拿不到鎖, 就認爲獲取鎖失敗。10000ms即10s是鎖失效時間。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    // 無論如何, 最後都要解鎖
    redLock.unlock();
}

3.3 Redlock 未完全解決問題

Redlock 算法細想一下還存在下面的問題:節點崩潰重啓,會出現多個客戶端持有鎖 假設一共有 5 個 Redis 節點:A, B, C, D, E。設想發生瞭如下的事件序列:

這樣,客戶端 1 和客戶端 2 同時獲得了鎖(針對同一資源)。

爲了應對節點重啓引發的鎖失效問題,redis 的作者 antirez 提出了延遲重啓的概念,即一個節點崩潰後,先不立即重啓它,而是等待一段時間再重啓,等待的時間大於鎖的有效時間。採用這種方式,這個節點在重啓前所參與的鎖都會過期,它在重啓後就不會對現有的鎖造成影響。這其實也是通過人爲補償措施,降低不一致發生的概率。時間跳躍問題

爲了應對始終跳躍引發的鎖失效問題,redis 的作者 antirez 提出了應該禁止人爲修改系統時間,使用一個不會進行 “跳躍” 式調整系統時鐘的 ntpd 程序。這也是通過人爲補償措施,降低不一致發生的概率。超時導致鎖失效問題 RedLock 算法並沒有解決,操作共享資源超時,導致鎖失效的問題。回憶一下 RedLock 算法的過程,如下圖所示

如圖所示,我們將其分爲上下兩個部分。對於上半部分框圖裏的步驟來說,無論因爲什麼原因發生了延遲,RedLock 算法都能處理,客戶端不會拿到一個它認爲有效,實際卻失效的鎖。然而,對於下半部分框圖裏的步驟來說,如果發生了延遲導致鎖失效,都有可能使得客戶端 2 拿到鎖。因此,RedLock 算法並沒有解決該問題。

4. Zookeeper 集羣下分佈式鎖可靠性分析

4.1 Zookeeper 的寫數據的原理

Zookeeper 在集羣部署中,Zookeeper 節點數量一般是奇數,且一定大等於 3。下面是 Zookeeper 的寫數據的原理:

那麼寫數據流程步驟如下:

還有一點,Zookeeper 採取的是全局串行化操作。

4.2 集羣模式下 Zookeeper 可靠性分析

下面列出 Redis 集羣下分佈式鎖可能存在的問題,判斷其在 Zookeeper 集羣下是否會存在:

集羣同步

總之,採用 Zookeeper 作爲分佈式鎖,你要麼就獲取不到鎖,一旦獲取到了,必定節點的數據是一致的,不會出現 redis 那種異步同步導致數據丟失的問題。時間跳躍問題 Zookeeper 不依賴全局時間,不存在該問題。超時導致鎖失效問題 Zookeeper 不依賴有效時間,不存在該問題。

5. 鎖的其他特性比較

redis 的讀寫性能比 Zookeeper 強太多,如果在高併發場景中,使用 Zookeeper 作爲分佈式鎖,那麼會出現獲取鎖失敗的情況,存在性能瓶頸。

Zookeeper 可以實現讀寫鎖,Redis 不行。

Zookeeper 的 watch 機制, 客戶端試圖創建 znode 的時候,發現它已經存在了,這時候創建失敗, 那麼進入一種等待狀態,當 znode 節點被刪除的時候,Zookeeper 通過 watch 機制通知它,這樣它就可以繼續完成創建操作(獲取鎖)。這可以讓分佈式鎖在客戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住,直到獲取到鎖爲止。這套機制,redis 無法實現。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hvvQfMtWCigYt5_lRHbimQ