玩轉Redis-老闆帶你深入理解分散式鎖

zxiaofan發表於2020-04-05

前言

公司交給了萌新小猿一個光榮而艱鉅的專案,該專案需要使用分散式鎖,這可難道了小猿,只是聽說過分散式鎖很牛掰,其他就一概不知了,唉不懂就問唄,遂向老闆請教。

老闆:我們每天不都在經歷分散式鎖嗎,我來給你回憶回憶。
小猿:好勒,瓜子板凳已備好。

本文結構

  • 為什麼要使用分散式鎖
  • 分散式鎖有哪些特點
  • 分散式鎖流行演算法及其優缺點
    • 基本演算法
    • relock演算法
    • token演算法
    • 資料庫排它鎖、ZooKeeper分散式鎖、Google的Chubby分散式鎖
  • 總結

1、為什麼要使用分散式鎖

這個問題應該拆分成以下2個問題回答。

1.1、為什麼使用鎖

保證在同一時刻共享資源只能被一個客戶端訪問;
根據鎖用途分為以下兩種:

  • 共享資源只允許一個客戶端操作;
  • 共享資源允許多個客戶端操作;

1.1.1、僅允許一個客戶端訪問

共享資源的操作不具備冪等性。
常見於 資料的修改、刪除操作;

2004LockOneOp.png

在上面的例子中,

人物事件 系統含義
經理A-N 多個執行緒
碼農小猿-調高空調溫度 非冪等共享資源
祕書的允許 獲取鎖

1.1.2、允許多個客戶端操作

主要應用場景是:共享資源的操作具有冪等性;
如 資料的查詢。
既然都具有冪等性了,為什麼還需要分散式鎖呢,通常是為了效率或效能,避免重複操作(尤其是消耗資源的操作)。例如我們常見的快取方案。

2004LockMoreOp.png
在上面的例子中,

人物事件 系統含義
經理A-N 多個執行緒
碼農小猿-整理昨天的資料 冪等共享資源
祕書的允許 獲取鎖
自己存資料 快取

由於此處的資源是冪等的,通常會將這類資源做快取,這就是常見的鎖+快取架構。 常適用於 獲取較為消耗資源(時間、記憶體、CPU等)的冪等資源,如:

  • 查詢使用者資訊;
  • 查詢歷史訂單;

當然,如果資源僅在一段時間範圍內具有冪等性,這時候,架構就應該升級了:
鎖+快取+快取失效/失效重新獲取/快取定時更新

1.2、鎖為什麼需要分散式的?

還是以上面的快取方案為例,此處略作變化。

2004LockDistributed.png

人物事件 系統含義
系統A、B 彼此獨立的系統
碼農小猿-調高空調溫度 非冪等共享資源
李祕書的允許 獲取鎖
王祕書的允許 獲取鎖
李祕書、王祕書資訊絕對互通 單一鎖升級為分散式鎖

2、高階分散式鎖有哪些特點?

2.1、互斥性

  • 在任意時刻,僅允許有一個客戶端獲得鎖;

PS:如果多個客戶端都能同時獲得鎖,那鎖就沒意義了,共享資源的安全性也就無法保證了。

老闆:當我在會議室接待客戶A時,其他客戶只有等待,你需要等到我空閒了才能把其他人帶到我辦公室。
小猿:明白。
接待客戶(非冪等共享資源);等到老闆空閒(獲取鎖)。

2.2、可重入性

  • 客戶端A獲得了鎖,只要鎖沒有過期,客戶端A可以繼續獲得該鎖。 鎖在我這裡,我還要繼續使用,其他人不準搶。
    這種特性可以很好的支援【鎖續約】功能。

例如:客戶端A獲取鎖,鎖釋放時間為10S,即將到達10S時,客戶端A未完成任務,需要再申請5S。若鎖沒有可重入性,客戶端A將無法續約,導致鎖可能被其他客戶端搶走。

小猿:受教了,老闆3分鐘後你還有一場面試。
老闆:小猿啊,難得你這麼好學,我很欣慰,我們的交流時間延10分鐘吧,其他會議延後。

2.3、高效能

  • 獲取鎖的效率應該足夠高;
  • 總不能讓業務阻塞在獲取鎖上面吧?

小猿:好的,我已在釘釘申請將會議延長10分鐘了;
老闆:嗯,我已經接受會議邀請了;
小猿:老闆你真高效。

2.4、高可用

分散式、微服務環境下,必須保證服務的高可用,否則輕則影響其他業務模組,重則引發服務雪崩。

老闆:我手機24小時開機,有會議時聯絡不上我也可以聯絡我祕書。

2.5、支援阻塞和非阻塞式鎖

  • 獲取鎖失敗,是直接返回失敗,還是一直阻塞知道獲取成功? 不同的業務場景有不同的答案。 例如:
鎖阻塞性 示例
非阻塞式 常見的工單系統,員工A、B同時想操作訂單1(搶單)。當員工A獲得鎖並如願操作訂單1;員工B獲取鎖失敗,不能一直阻塞,應該告知失敗,讓員工B去做其他事,否則員工B就光明正大上班划水了。
阻塞式 打電話給老闆稽核方案,老闆在通話中(獲取鎖失敗),此時需要每隔一段時間就給老闆打電話,直到聯絡上老闆才行。誰讓老闆下了死命令今天必須稽核通過呢,嗚嗚嗚。

2.6、解鎖許可權

  • 客戶端僅能釋放(解鎖)自己加的鎖;

常見的解決方案是,給鎖加隨機數(或ThreadID)。

老闆:小猿啊,給你講了這麼多,都明白了嗎?
籠子裡的鸚鵡:明白啦,明白啦。
老闆:閉嘴,我問的是小猿,只有小猿自己有資格回答。

2.7、避免死鎖

  • 加鎖方異常終止無法主動釋放鎖; 常規做法是 加鎖時設定超時時間,如果未主動釋放鎖,則利用Redis的自動過期被動釋放鎖。

祕書破門而入:老闆,你們10分鐘的會議已經到點了,隔壁的李總已經等不及了;

老闆:一不留神就忘記時間了,我得去見李總了。

小猿:老闆,我們還沒聊完呢,,,

2.8、異常處理

  • 常見的異常情況有Redis當機、時鐘跳躍、網路故障等;

小猿:不管出現哪種情況,我獲取鎖都會失敗啊,這可怎麼辦呢?
PS:這就複雜了,需要根據具體的業務場景分析。對於必須同步處理的業務,則必須失敗告警,對於允許延遲處理的業務可以考慮記錄失敗資訊待其他系統處理。

3、分散式鎖流行演算法

3.1、基本方案SETNX

基於Redis的SETNX指令完成鎖的獲取;

3.1.1、獲取鎖 SET lock:resource_name random_value NX PX 30000

lock:resource_name:資源名字,加鎖物件的唯一標記;
random_value:通常儲存加鎖方的唯一標記,如“UUID+ThreadID”;
NX:key不存在才設定,即鎖未被其他人加鎖才能加鎖;
PX:鎖超時時間;

當然,此種加鎖方式是不支援“鎖重入性”的。

3.1.2、釋放鎖(LUA指令碼)

checkValueThenDelete:檢查解鎖方是否是加鎖方,是則允許解鎖,否則不允許解鎖;
虛擬碼是:

public class RedisTool {
    // 釋放鎖成功標記
    private static final Long RELEASE_LOCK_SUCCESS = 1L;

    /**
     * 釋放分散式鎖
     *
     * @param jedis     Redis客戶端
     * @param lockKey   鎖標記
     * @param lockValue 加鎖方標記
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String lockValue) {
        String script = "" +
                "if redis.call('get', KEYS[1]) == ARGV[1] then" +
                "    return redis.call('del', KEYS[1]) " +
                "else" +
                "    return 0 " +
                "end";
        // Collections.singletonList():用於只有一個元素的場景,減少記憶體分配
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
        if (RELEASE_LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
複製程式碼

3.2、Redlock演算法

此演算法由Redis作者antirez提出,作為一種分散式場景下的鎖實現方案;

3.2.1、Redlock演算法原理

【核心】大多數節點獲取鎖成功且鎖依舊有效;

  • Step1、獲取當前時間(毫秒數);
  • Step2、按序想N個Redis節點獲取鎖;
    • Step2.1、設定隨機字串random_value;
    • Step2.2、設定鎖過期時間;
    • Note1:獲取鎖需設定超時時間(防止某個節點不可用),且timeout應遠小於鎖有效時間(幾十毫秒級);
    • Note2:某節點獲取鎖失敗後,立即向下一個節點獲取鎖(任何型別失敗,包含該節點上的鎖已被其他客戶端持有);
  • Step3、計算獲取鎖的總耗時totalTime;
  • Step4、獲取鎖成功
    • 獲取鎖成功:客戶端從大多數節點(>=N/2+1)成功獲取鎖,且totalTime不超過鎖的有效時間;
    • 重新計算鎖有效時間:最初鎖有效時間減3.1計算的獲取鎖消耗的時間;
  • Step5、獲取鎖失敗
    • 獲取失敗後應立即向【所有】客戶端發起釋放鎖(Lua指令碼);
  • Step6、釋放鎖
    • 業務完成後應立即向【所有】客戶端發起釋放鎖(Lua指令碼);

2004Redlock.png

3.2.2、Redlock演算法優點

  • 可用性高,大多數節點正常即可;
  • 單Redis節點的分散式鎖在failover時鎖失效問題不復存在;

3.2.3、Redlock演算法問題點

  • Redis節點崩潰將影響鎖安全性
    A、節點崩潰前鎖未持久化,節點重啟後鎖將丟失;
    B、Redis預設AOF持久化是每秒刷盤(fsync)一次,最壞情況將丟失1秒的資料;
  • 需避免始終跳躍;
    A、管理員手動修改時鐘;
    B、使用[不會跳躍調整系統時鐘]的ntpd(時鐘同步)程式,對時鐘修改通過多次微調實現;
  • 客戶端阻塞導致鎖過期,導致共享資源不安全;
  • 如果獲取鎖消耗時間較長,導致效時間很短,是否應該立即釋放鎖?多段才算短?

3.3、帶fencing token的實現

分散式系統專家Martin Kleppmann討論提出RedLock存在安全性問題;

3.3.1、神仙之戰

Martin Kleppmann認為Redis作者antirez提出的RedLock演算法有安全性問題,雙方在網路上多輪探討交鋒。Martin指出RedLock演算法的核心問題點如下:

  • 鎖過期或者網路延遲將導致鎖衝突: A、客戶端A程式pause》鎖過期》客戶端B持有鎖》客戶端A恢復並向共享資源發起寫請求;
    B、網路延遲也會產生類似效果;
  • RedLock安全性對系統時鐘有強依賴;

3.3.2、fencing token演算法原理

  • fencing token是一個單調遞增的數字,當客戶端成功獲取鎖時隨同鎖一起返回給客戶端;
  • 客戶端訪問共享資源時帶上token;
  • 共享資源服務檢查token,拒絕延遲到來的請求;

3.3.3、fencing token演算法問題點

  • 需要改造共享資源服務;
  • 如果資源服務也是分散式,如何保證token在多個資源服務節點遞增;
  • 2個fencing token到達資源服務的順序顛倒,服務檢查將異常;
  • 【antirez】既然存在fencing機制保持資源互斥訪問,為什麼還需要分散式鎖且要求強安全性呢;

3.4、其他分散式鎖

3.4.1、資料庫排它鎖

  • 獲取鎖(select for update ,悲觀鎖);
  • 處理業務邏輯;
  • 釋放鎖(connection.commit());
  • 注意:InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行級鎖,否則會使用表級鎖。So 必須給lock_name加索引。

3.4.2、ZooKeeper分散式鎖

  • 客戶端建立znode節點,建立成功則獲取鎖成功;
  • 持有鎖的客戶端訪問共享資源完成後刪除znode;
  • znode建立成ephemeral(znode特性),保證建立znode的客戶端崩潰後,znode會被自動刪除;
  • 【問題】Zookeeper基於客戶端與Zookeeper某臺伺服器維護Session,Session依賴定期心跳(heartbeat)維持。Zookeeper長時間收不到客戶端心跳,就任務Session過期,這個Session所建立的所有ephemeral型別的znode節點都將被刪除。

3.4.3、Google的Chubby分散式鎖

  • sequencer機制(類似fencing token)緩解延遲導致的問題;
  • 鎖持有者可隨時請求一個sequencer;
  • 客戶端操作資源時將sequencer傳給資源伺服器;
  • 資源伺服器檢查sequencer有效性;
    • ①呼叫Chubby的API(CheckSequencer)檢查;
    • ②對比檢查客戶端、資源伺服器當前觀察到的sequencer(類似fencing token);
    • ③lock-delay:允許客戶端為持有鎖指定一個lock-delay延遲時間,Chubby發現客戶端失去聯絡時,在lock-delay時間內組織其他客戶端獲取鎖;

4、總結

4.1、我們該使用怎樣的分散式鎖演算法?

  • 技術都是為業務服務的,避免選擇“高大上”的炫技;
  • 依託業務場景,儘可能選擇最簡單的做法;
  • 最簡單的分散式鎖導致偶發性異常如何處理呢?
    • 建議增加額外的機制甚至人工介入保證業務準確性,通常這部分成本低於複雜的分散式鎖的開發、運維成本。

4.2、分散式鎖的另類玩法

  • “分而治之”經久不衰:
    • 如果共享資源本身可以拆分,那就分開處理吧。
    • 比如電商系統防止超賣,假設有10000個口罩將被秒殺,常規做法是一個鎖控制所有資源。另類玩法就是將10000個口罩交由20個鎖控制,整體效能瞬間提升幾十倍。
    • PS:此處超賣僅是舉例,真實場景下的秒殺超賣有更加複雜的場景,慎重。

敬請關注後續《玩轉Redis》系列文章。


祝君好運!
Life is all about choices!
將來的你一定會感激現在拼命的自己!
CSDN】【GitHub】【OSCHINA】【掘金】【語雀】【微信公眾號
歡迎訂閱zxiaofan的微信公眾號,掃碼或直接搜尋zxiaofan


相關文章