SOFAJRaft-RheaKV 分佈式鎖實現剖析 | SOFAJRaft 實現原理

SOFAStackScalable Open Financial Architecture Stack)是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。html

SOFAJRaft-RheaKV 分佈式鎖實現剖析

SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。java

本文爲《剖析 | SOFAJRaft 實現原理》第七篇,本篇做者米麒麟,來自陸金所。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:SOFA:JRaftLab/,文末包含往期系列文章。git

SOFAJRaft :github.com/sofastack/s…github

前言

在分佈式部署、高併發、多線程場景下,咱們常常會遇到資源的互斥訪問的問題,最有效、最廣泛的方法是給共享資源或者對共享資源的操做加一把鎖。在 JDK 中咱們可使用 ReentrantLock 重入鎖或者 synchronized 關鍵字達成資源互斥訪問目的,可是因爲分佈式系統的分佈性(即多線程和多進程而且分佈在不一樣機器中),使得兩種鎖失去原有鎖的效果,須要用戶自定義來實現分佈式鎖。算法

本文重點圍繞分佈式鎖概覽、實現方式以及基於 SOFAJRaft 實現等方面剖析 SOFAJRaft-RheaKV 基於 SOFAJRaft 實現分佈式鎖原理,闡述如何使用 SOFAJRaft 組件提供分佈式鎖服務功能:數據庫

  • 什麼是分佈式鎖?分佈式鎖具有哪些條件?分佈式鎖有哪些實現方式?
  • RheaKV 基於 SOFAJRaft 如何實現分佈式鎖?解決分佈式鎖哪些問題?

分佈式鎖

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式,用於在分佈式系統中協調他們之間的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證一致性,在這種狀況下便須要使用到分佈式鎖。分佈式鎖經過共享標識肯定其惟一性,對共享標識進行修改時可以保證原子性和對鎖服務調用方的可見性。緩存

分佈式鎖概覽

Martin Kleppmann 是英國劍橋大學的分佈式系統研究員,以前和 Redis 之父 Antirez 關於 RedLock 紅鎖是否安全的問題激烈討論過。Martin 認爲通常咱們使用分佈式鎖有兩個場景:安全

  • 效率:使用分佈式鎖可以避免不一樣節點重複相同的工做致使浪費資源,譬如用戶付款以後有可能不一樣節點發出多條短信;
  • 正確性:添加分佈式鎖一樣避免破壞正確性事件的發生,若是兩個節點在同一條數據上面操做,譬如多個節點機器對同一個訂單操做不一樣的流程有可能致使該筆訂單最後狀態出現錯誤形成資金損失;

分佈式鎖須要具有的條件包括:網絡

  • 獲取鎖和釋放鎖的性能要好;
  • 判斷得到鎖是不是原子性的,不然可能致使多個請求都能獲取到鎖;
  • 網絡中斷或者宕機沒法釋放鎖時,鎖必須被清除;
  • 可重入一個線程中屢次獲取同一把鎖,譬如一個線程在執行帶鎖的方法,該方法調用另外一個須要相同鎖的方法,則該線程直接執行調用的方法,而無需從新得到鎖;
  • 阻塞鎖和非阻塞鎖,阻塞鎖即沒有獲取到鎖,則繼續等待獲取鎖;非阻塞鎖即沒有獲取到鎖,不繼續等待直接返回獲取鎖失敗;

分佈式鎖實現

分佈式 CAP 理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition Tolerance),最多隻能同時知足兩項。」,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。在不少場景中爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候須要保證一個方法在同一時間內只能被同一個線程執行。 分佈式鎖通常有三種實現方式:多線程

  • 基於數據庫實現分佈式鎖;
  • 基於緩存(Redis,Memcached,Tair)實現分佈式鎖;
  • 基於 ZooKeeper 實現分佈式鎖;

基於數據庫實現分佈式鎖

基於數據庫實現分佈式鎖的核心思想:在數據庫中建立一張表,表裏包含方法名等字段,而且在方法名字段上面建立惟一索引,執行某個方法須要使用此方法名向表中插入數據,成功插入則獲取鎖,執行結束則刪除對應的行數據釋放鎖。

基於緩存實現分佈式鎖

基於緩存一般選用 Redis 實現分佈式鎖,考慮到 Redis 有很是高的性能,Redis 命令對分佈式鎖支持友好,而且實現方便。基於單 Redis 節點的分佈式鎖在 Failover 的時候產生解決不了的安全性問題,Redlock 是 Redis 的做者 Antirez 提出的集羣模式 Redis 分佈式鎖,基於 N 個徹底獨立的 Redis 節點(一般狀況下 N 能夠設置成5),運行 Redlock 算法依次執行下面各個步驟完成獲取鎖的操做

  • 獲取當前時間(毫秒數);
  • 按順序依次向 N 個 Redis 節點執行獲取鎖的操做。此獲取操做包含隨機字符串 my_random_value,也包含過時時間(好比 PX 30000,即鎖的有效時間)。爲了保證在某個 Redis 節點不可用的時候算法可以繼續運行,獲取鎖的操做還有超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個 Redis 節點獲取鎖失敗之後應該當即嘗試下一個Redis 節點。這裏的失敗包含任何類型的失敗,好比該 Redis 節點不可用,或者該 Redis 節點上的鎖已經被其它客戶端持有(注:Redlock 原文中這裏只說起 Redis 節點不可用的狀況,但也應該包含其它的失敗狀況);
  • 計算整個獲取鎖的過程總共消耗了多長時間,計算方法是用當前時間減去第1步記錄的時間。若是客戶端從大多數 Redis 節點(>= N/2+1)成功獲取到了鎖,而且獲取鎖總共消耗的時間沒有超過鎖的有效時間(lock validity time),那麼這時客戶端才認爲最終獲取鎖成功;不然認爲最終獲取鎖失敗;
  • 若是最終獲取鎖成功了,那麼此鎖的有效時間應該從新計算,它等於最初鎖的有效時間減去第3步計算出來的獲取鎖消耗的時間;
  • 若是最終獲取鎖失敗(可能因爲獲取到鎖的 Redis 節點個數少於 N/2+1,或者整個獲取鎖的過程消耗的時間超過了鎖的最初有效時間),那麼客戶端當即向全部 Redis 節點發起釋放鎖的操做;

基於 ZooKeeper 實現分佈式鎖

ZooKeeper 是以 Paxos 算法爲基礎的分佈式應用程序協調服務,爲分佈式應用提供一致性服務的開源組件,其內部是分層的文件系統目錄樹結構,規定同一個目錄下只能有一個惟一文件名。基於 ZooKeeper 實現分佈式鎖步驟包括:

  • 建立一個鎖目錄 lock;
  • 但願得到鎖的線程 A 在 lock 目錄下建立臨時順序節點;
  • 當前線程獲取鎖目錄下全部的子節點,而後獲取比本身小的兄弟節點,若是不存在表示當前線程順序號最小,得到鎖;
  • 線程 B 獲取全部節點,判斷本身不是最小節點,設置監聽(Watcher)比本身次小的節點(只關注比本身次小的節點是爲了防止發生「羊羣效應」);
  • 線程 A 處理完刪除本身的節點,線程 B 監聽到變動事件判斷本身是否爲最小的節點,若是是則得到鎖;

RheaKV 分佈式鎖實現

RheaKV 是基於 SOFAJRaft 和 RocksDB 實現的嵌入式、分佈式、高可用、強一致的 KV 存儲類庫,RheaKV 提供 DistributedLock 實現可重入鎖,自動續租以及 Fencing Token 功能特性。DistributedLock 是可重入鎖,tryLock() 與 unlock() 必須成對出現。RheaKV 調用 getDistributedLock 接口獲取分佈式鎖實例,其中參數:

  • target 理解爲分佈式鎖的 key, 不一樣鎖的 key 不能重複,可是鎖的存儲空間是與其餘 KV 數據隔離的,因此只需保證 key 在 '鎖空間' 內的惟一性便可;
  • lease 必須包含鎖的租約(lease)時間,在鎖到期以前若是 watchdog 爲空那麼鎖會被自動釋放,即沒有 watchdog 配合的 lease 就是 timeout 的意思;
  • watchdog 表示自動續租的調度器,須要用戶自行建立並銷燬,框架內部不負責該調度器的生命週期管理,若是 watchdog 不爲空按期(lease 的 2/3 時間爲週期)主動爲當前的鎖不斷進行續租,直到用戶主動釋放鎖(unlock);
DistributedLock<byte[]> getDistributedLock(final byte[] target, final long lease, final TimeUnit unit);
DistributedLock<byte[]> getDistributedLock(final String target, final long lease, final TimeUnit unit);
DistributedLock<byte[]> getDistributedLock(final byte[] target, final long lease, final TimeUnit unit,
                                           final ScheduledExecutorService watchdog);
DistributedLock<byte[]> getDistributedLock(final String target, final long lease, final TimeUnit unit,
                                           final ScheduledExecutorService watchdog);
複製代碼

RheaKV 分佈式鎖 Example

DistributedLock<T> lock = ...;
if (lock.tryLock()) {
    try {
        // manipulate protected state
    } finally {
        lock.unlock();
    }
} else {
    // perform alternative actions
}
複製代碼

詳情請參考 github 倉庫中下面這個類:

com.alipay.sofa.jraft.example.rheakv.DistributedLockExample
複製代碼

Lock 流程

RheaKV 調用 tryLock(ctx) 方法嘗試設置分佈式鎖,其中入參 ctx 做爲當前的鎖請求者的用戶自定義上下文數據,若是鎖請求者成功獲取到鎖,其餘線程以及進程也可以看獲得鎖持有者的 ctx 上下文。RheaKV 嘗試構建鎖使用 DistributedLock 默認實現 DefaultDistributedLock 的 internalTryLock(ctx) 內部方法添加分佈式鎖:

  • 獲取分佈式鎖內部 key 和鎖獲取器 acquirer,調用 DefaultRheaKVStore 的 tryLockWith(key, keepLease, acquirer) 方法進行設置分佈式鎖;
  • 檢查 RheaKVStore 狀態是否爲已啓動或者已關閉,PlacementDriverClient 按照分佈式鎖 key 定位所對應的分區 region,根據分區 region 的 id 獲取 Leader 節點分區引擎 ,基於分佈式鎖 key 和鎖獲取器 acquirer 生成重試器 retryRunner 組建 Failover 回調 failoverClosure;
  • 判斷分區引擎 regionEngine 是否爲空,若是 regionEngine 爲空表示 Leader 節點不在本地則構建 KeyLockRequest 給 RheaKVStore 分區 Leader 節點發起異步 RPC 調用請求加鎖;若是 regionEngine 非空則確保當前分佈式鎖對應的分區 region 在合理 Epoch 期數範疇內,獲取分區引擎 regionEngine 底層 MetricsRawKVStore 嘗試添加鎖;
  • MetricsRawKVStore 使用基於 Raft 協議副本狀態機的 RaftRawKVStore 設置分佈式鎖,其算法依賴於如下假設:儘管跨進程存在非同步時鐘,但每一個進程中的本地時間仍以大體相同的速率流動,而且與鎖的自動釋放時間相比其錯誤較小。鎖獲取器 acquirer 設置默認時鐘爲鎖時間戳,申請基於分佈式鎖 key 的加鎖 KEY_LOCK 操做 KVOperation;
  • RheaKV 存儲狀態機 KVStoreStateMachine 按照操做類型爲 KEY_LOCK 批量調用 RocksRawKVStore 的tryLockWith(key, fencingKey, keepLease, acquirer, closure) 基於 RocksDB 執行加鎖操做。RocksRawKVStore 獲取讀寫鎖 readWriteLock 的讀鎖而且加讀鎖,查詢 RocksDB 分佈式鎖 key 的鎖持有者 prevBytesVal。建立分佈式鎖持有者構造器 builder,經過鎖持有者構造器構造鎖持有者 owner 而且回調 KVStoreClosure 返回其鎖持有者 owner,讀寫鎖 readWriteLock 的讀鎖進行解鎖:
    • 檢查此鎖持有者 prevBytesVal 是否爲空:
      • prevBytesVal 爲空表示無其餘鎖請求者持有此鎖即首次嘗試上鎖或者此鎖已刪除,鎖持有者構造器設置鎖持有者 id 爲鎖獲取器 acquirer 的 id 即表示將持有此鎖,指定新的截止時間戳,定義租約剩餘時間爲首次獲取鎖成功 FIRST_TIME_SUCCESS 即-1,按照 fencingKey 新建 fencing token,初始化鎖重入 acquires 爲1,設置鎖持有者上下文爲鎖獲取器 acquirer 的上下文,設置上鎖成功構建鎖持有者 owner 基於分佈式鎖 key 鍵值對方式插入 RocksDB 存儲;
    • 鎖持有者 prevBytesVal 非空檢查其鎖持有是否過時即便用序列化器讀取其以前鎖持有者 prevOwner,判斷距離鎖持有截止剩餘時間是否小於0:
      • 小於0表示鎖持有者已超出其租約,鎖持有者構造器設置鎖持有者 id 爲鎖獲取器 acquirer 的 id 即表示將持有此鎖,指定新的截止時間戳,定義租約剩餘時間爲新獲取鎖成功 NEW_ACQUIRE_SUCCESS 即-2,按照 fencingKey 新建 fencing token,初始化鎖重入 acquires 爲1,設置鎖持有者上下文爲鎖獲取器 acquirer 的上下文,設置上鎖成功構建鎖持有者 owner 基於分佈式鎖 key 鍵值對方式插入 RocksDB 存儲;
    • 鎖持有者未超出租約即剩餘時間大於或者等於0,檢查以前鎖持有者的鎖獲取器與當前鎖獲取器 acquirer 是否相同:
      • 鎖獲取器相同表示此分佈式鎖爲重入鎖,鎖持有者構造器設置鎖持有者 id 爲以前鎖持有者 id,更新截止時間戳保持續租,指定租約剩餘時間爲重入成功 REENTRANT_SUCCESS 即-4,保持鎖持有者 prevOwner 的 fencing token,修改鎖重入 acquires 自增1,更新鎖持有者上下文爲鎖獲取器 acquirer 的上下文,設置上鎖成功構建鎖持有者 owner 基於分佈式鎖 key 鍵值對方式插入 RocksDB 存儲;
      • 此鎖已存在且以前鎖持有者與當前鎖請求者不一樣表示非重入鎖,表示其餘鎖請求者在嘗試上已存在的鎖,鎖持有者構造器設置鎖持有者 id 爲以前鎖持有者 id,更新租約剩餘時間爲當前鎖持有者的租約剩餘時間,指定鎖持有者上下文爲鎖持有者 prevOwner 的上下文,設置上鎖失敗構建鎖持有者 owner;

捕獲

  • 檢查分佈式鎖持有者 owner 是否成功,獲取鎖持有者成功表示設置分佈式鎖成功,更新當前鎖持有者的鎖獲取器 acquirer 的 fencing token,獲取自動續租的調度器 watchdog 調用 scheduleKeepingLease(watchdog, internalKey, acquirer, period) 以租約 lease 的 2/3 時間爲調度週期給當前的鎖不斷續租保持租約;
  • 當成功上鎖後經過 getFencingToken() 接口獲取當前的 fencing token, 此爲單調遞增數字即其值大小表明鎖擁有者們先來後到的順序。在下面的時序圖中假設鎖服務自己是沒有問題的,它老是能保證任一時刻最多隻有一個客戶端得到鎖,客戶端1在得到鎖以後發生很長時間的 GC pause,在此期間其得到的鎖已過時,而客戶端2得到鎖。當客戶端1從 GC pause 中恢復過來時,它不知道本身持有的鎖已過時,依然向共享資源即下圖的存儲服務發起寫數據請求,而這時鎖實際上被客戶端2持有,所以兩個客戶端的寫請求有可能衝突即鎖的互斥做用失效,使用此 fencing token 解決下圖此問題:

衝突即鎖

(此圖來自 martin.kleppmann.com/2016/02/08/…

Unlock 流程

RheaKV 調用 unlock() 方法嘗試釋放分佈式鎖,使用分佈式鎖接口默認實現 DefaultDistributedLock 的 unlock() 方法嘗試釋放鎖:

  • 獲取分佈式鎖內部 key 和鎖獲取器 acquirer,調用 DefaultRheaKVStore 的 releaseLockWith(key, acquirer) 方法釋放分佈式鎖;
  • 檢查 RheaKVStore 狀態是否爲已啓動或者已關閉,根據分佈式鎖 key 查找所對應的分區 region,根據分區 region 的 id 獲取 Leader 節點分區引擎 regionEngine,基於分佈式鎖 key 和鎖獲取器 acquirer 建立重試器 retryRunner 構成 Failover 回調 failoverClosure;
  • 檢查分區引擎 regionEngine 是否爲空,假如 regionEngine 爲空則構建 KeyUnlockRequest 發起對RheaKVStore 分區 Leader 節點發起異步 RPC 調用請求解鎖;若是 regionEngine 非空則確保當前分佈式鎖 key 所在的分區 region 在合理 Epoch 期數範圍,獲取分區引擎 regionEngine 底層 MetricsRawKVStore 嘗試解除鎖;
  • MetricsRawKVStore 經過基於 Raft 協議副本狀態機的 RaftRawKVStore 解除分佈式鎖,申請基於分佈式鎖 key 的解鎖 KEY_LOCK_RELEASE 操做 KVOperation;
  • RheaKV 存儲狀態機 KVStoreStateMachine 按照操做類型爲 KEY_LOCK_RELEASE 批量調用 RocksRawKVStore 的 releaseLockWith(key, acquirer, closure) 基於 RocksDB 執行解鎖操做。RocksRawKVStore 獲取讀寫鎖 readWriteLock 的讀鎖而且加讀鎖,查詢 RocksDB 分佈式鎖 key 的鎖持有者 prevBytesVal。建立分佈式鎖持有者構造器 builder,經過鎖持有者構造器構造鎖持有者 owner 而且回調 KVStoreClosure 返回其鎖持有者 owner,讀寫鎖 readWriteLock 的讀鎖進行解鎖:
    • 檢查此鎖持有者 prevBytesVal 是否爲空:
      • prevBytesVal 爲空表示無其餘鎖請求者持有此鎖即此鎖不存在,鎖持有者構造器設置鎖持有者 id 爲鎖獲取器 acquirer 的 id 即表示將持有此鎖,指定 fencing token 爲鎖獲取器 acquirer 的 fencing token,定義鎖重入 acquires 爲0,設置解鎖成功構建鎖持有者 owner;
    • 鎖持有者 prevBytesVal 非空檢查使用序列化器讀取其以前鎖持有者 prevOwner,檢查以前鎖持有者的鎖獲取器與當前鎖獲取器 acquirer 是否相同:
      • 鎖獲取器相同表示此分佈式鎖爲重入鎖,鎖持有者構造器設置鎖持有者 id 爲以前鎖持有者 id,更新截止時間戳爲鎖持有者 prevOwner 的截止時間戳,保持鎖持有者 prevOwner 的 fencing token,修改鎖重入 acquires 爲以前鎖持有減1,更新鎖持有者上下文爲鎖持有者 prevOwner 的上下文,設置解鎖成功構建鎖持有者 owner,按照鎖重入 acquires 是否小於或者等於0基於分佈式鎖 key 刪除 RocksDB 鎖持有者(鎖重入 acquires 小於或者等於0)或者覆蓋 RocksDB 更新鎖持有者(鎖重入 acquires 大於0);
      • 鎖持有者 prevOwner 的鎖獲取器與當前鎖獲取器 acquirer 不一樣表示當前鎖獲取器不合理不能進行解鎖,鎖持有者構造器設置鎖持有者 id 爲以前鎖持有者 id 通知真正的鎖持有者,保持鎖持有者 prevOwner 的 fencing token,保持鎖重入 acquires 爲以前鎖持有,更新鎖持有者上下文爲鎖持有者 prevOwner 的上下文,設置解鎖失敗構建鎖持有者 owner;

捕獲1

  • 更新當前鎖持有者 owner,檢查鎖持有者的鎖獲取器是否爲當前鎖獲取器 acquirer,使用 tryCancelScheduling() 方法取消自動續租調度;

RheaKV 基於 DistributedLock 默認實現 DefaultDistributedLock 核心邏輯:

RheaKV 核心邏輯

carbon

總結

本文圍繞分佈式鎖原理,實現方式以及基於 SOFAJRaft 實現細節方面闡述 SOFAJRaft-RheaKV 分佈式鎖基本原理,剖析 SOFAJRaft-RheaKV 如何使用 SOFAJRaft 組件解決分佈式鎖實現問題,基於 DistributedLock 接口經過 Raft 分佈式一致性協議提供分佈式鎖服務。

參考資料

SOFAJRaft 源碼解析系列閱讀

公衆號:金融級分佈式架構(Antfin_SOFA)

相關文章
相關標籤/搜索