玩轉Redis-老闆帶你深刻理解分佈式鎖

前言

公司交給了萌新小猿一個光榮而艱鉅的項目,該項目須要使用分佈式鎖,這可難道了小猿,只是據說過度布式鎖很牛掰,其餘就一律不知了,唉不懂就問唄,遂向老闆請教。java

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

本文結構

  • 爲何要使用分佈式鎖
  • 分佈式鎖有哪些特色
  • 分佈式鎖流行算法及其優缺點
    • 基本算法
    • relock算法
    • token算法
    • 數據庫排它鎖、ZooKeeper分佈式鎖、Google的Chubby分佈式鎖
  • 總結

一、爲何要使用分佈式鎖

這個問題應該拆分紅如下2個問題回答。git

1.一、爲何使用鎖

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

  • 共享資源只容許一個客戶端操做;
  • 共享資源容許多個客戶端操做;

1.1.一、僅容許一個客戶端訪問

共享資源的操做不具有冪等性。
常見於 數據的修改、刪除操做;
面試

2004LockOneOp.png

在上面的例子中,redis

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

1.1.二、容許多個客戶端操做

主要應用場景是:共享資源的操做具備冪等性;
如 數據的查詢。
既然都具備冪等性了,爲何還須要分佈式鎖呢,一般是爲了效率或性能,避免重複操做(尤爲是消耗資源的操做)。例如咱們常見的緩存方案。算法

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

人物事件 系統含義
經理A-N 多個線程
碼農小猿-整理昨天的資料 冪等共享資源
祕書的容許 獲取鎖
本身存資料 緩存

因爲此處的資源是冪等的,一般會將這類資源作緩存,這就是常見的鎖+緩存架構。 常適用於 獲取較爲消耗資源(時間、內存、CPU等)的冪等資源,如:數據庫

  • 查詢用戶信息;
  • 查詢歷史訂單;

固然,若是資源僅在一段時間範圍內具備冪等性,這時候,架構就應該升級了:
鎖+緩存+緩存失效/失效從新獲取/緩存定時更新緩存

1.二、鎖爲何須要分佈式的?

仍是以上面的緩存方案爲例,此處略做變化。安全

2004LockDistributed.png

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

二、高級分佈式鎖有哪些特色?

2.一、互斥性

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

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

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

2.二、可重入性

  • 客戶端A得到了鎖,只要鎖沒有過時,客戶端A能夠繼續得到該鎖。 鎖在我這裏,我還要繼續使用,其餘人不許搶。
    這種特性能夠很好的支持【鎖續約】功能。

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

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

2.三、高性能

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

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

2.四、高可用

分佈式、微服務環境下,必須保證服務的高可用,不然輕則影響其餘業務模塊,重則引起服務雪崩。

老闆:我手機24小時開機,有會議時聯繫不上我也能夠聯繫我祕書。

2.五、支持阻塞和非阻塞式鎖

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

2.六、解鎖權限

  • 客戶端僅能釋放(解鎖)本身加的鎖;

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

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

2.七、避免死鎖

  • 加鎖方異常終止沒法主動釋放鎖; 常規作法是 加鎖時設置超時時間,若是未主動釋放鎖,則利用Redis的自動過時被動釋放鎖。

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

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

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

2.八、異常處理

  • 常見的異常狀況有Redis宕機、時鐘跳躍、網絡故障等;

小猿:無論出現哪一種狀況,我獲取鎖都會失敗啊,這可怎麼辦呢?
PS:這就複雜了,須要根據具體的業務場景分析。對於必須同步處理的業務,則必須失敗告警,對於容許延遲處理的業務能夠考慮記錄失敗信息待其餘系統處理。

三、分佈式鎖流行算法

3.一、基本方案SETNX

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

3.1.一、獲取鎖 SET lock:resource_name random_value NX PX 30000

lock:resource_name:資源名字,加鎖對象的惟一標記;
random_value:一般存儲加鎖方的惟一標記,如「UUID+ThreadID」;
NX:key不存在才設置,即鎖未被其餘人加鎖才能加鎖;
PX:鎖超時時間;

固然,此種加鎖方式是不支持「鎖重入性」的。

3.1.二、釋放鎖(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.二、Redlock算法

此算法由Redis做者antirez提出,做爲一種分佈式場景下的鎖實現方案;

3.2.一、Redlock算法原理

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

  • Step一、獲取當前時間(毫秒數);
  • Step二、按序想N個Redis節點獲取鎖;
    • Step2.一、設置隨機字符串random_value;
    • Step2.二、設置鎖過時時間;
    • Note1:獲取鎖需設置超時時間(防止某個節點不可用),且timeout應遠小於鎖有效時間(幾十毫秒級);
    • Note2:某節點獲取鎖失敗後,當即向下一個節點獲取鎖(任何類型失敗,包含該節點上的鎖已被其餘客戶端持有);
  • Step三、計算獲取鎖的總耗時totalTime;
  • Step四、獲取鎖成功
    • 獲取鎖成功:客戶端從大多數節點(>=N/2+1)成功獲取鎖,且totalTime不超過鎖的有效時間;
    • 從新計算鎖有效時間:最初鎖有效時間減3.1計算的獲取鎖消耗的時間;
  • Step五、獲取鎖失敗
    • 獲取失敗後應當即向【全部】客戶端發起釋放鎖(Lua腳本);
  • Step六、釋放鎖
    • 業務完成後應當即向【全部】客戶端發起釋放鎖(Lua腳本);

2004Redlock.png

3.2.二、Redlock算法優勢

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

3.2.三、Redlock算法問題點

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

3.三、帶fencing token的實現

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

3.3.一、神仙之戰

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

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

3.3.二、fencing token算法原理

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

3.3.三、fencing token算法問題點

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

3.四、其餘分佈式鎖

3.4.一、數據庫排它鎖

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

3.4.二、ZooKeeper分佈式鎖

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

3.4.三、Google的Chubby分佈式鎖

  • sequencer機制(相似fencing token)緩解延遲致使的問題;
  • 鎖持有者可隨時請求一個sequencer;
  • 客戶端操做資源時將sequencer傳給資源服務器;
  • 資源服務器檢查sequencer有效性;
    • ①調用Chubby的API(CheckSequencer)檢查;
    • ②對比檢查客戶端、資源服務器當前觀察到的sequencer(相似fencing token);
    • ③lock-delay:容許客戶端爲持有鎖指定一個lock-delay延遲時間,Chubby發現客戶端失去聯繫時,在lock-delay時間內組織其餘客戶端獲取鎖;

四、總結

4.一、咱們該使用怎樣的分佈式鎖算法?

  • 技術都是爲業務服務的,避免選擇「高大上」的炫技;
  • 依託業務場景,儘量選擇最簡單的作法;
  • 最簡單的分佈式鎖致使偶發性異常如何處理呢?
    • 建議增長額外的機制甚至人工介入保證業務準確性,一般這部分紅本低於複雜的分佈式鎖的開發、運維成本。

4.二、分佈式鎖的另類玩法

  • 「分而治之」經久不衰:
    • 若是共享資源自己能夠拆分,那就分開處理吧。
    • 好比電商系統防止超賣,假設有10000個口罩將被秒殺,常規作法是一個鎖控制全部資源。另類玩法就是將10000個口罩交由20個鎖控制,總體性能瞬間提高几十倍。
    • PS:此處超賣僅是舉例,真實場景下的秒殺超賣有更加複雜的場景,慎重。

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


祝君好運!
Life is all about choices!
未來的你必定會感激如今拼命的本身!
CSDN】【GitHub】【OSCHINA】【掘金】【語雀】【微信公衆號
歡迎訂閱zxiaofan的微信公衆號,掃碼或直接搜索zxiaofan

相關文章
相關標籤/搜索