孫玄:基於CAP模型設計企業級真正高可用的分佈式鎖

 

孫玄:畢業於浙江大學,現任轉轉公司首席架構師,技術委員會主席,大中後臺技術負責人(交易平臺、基礎服務、智能客服、基礎架構、智能運維、數據庫、安全、IT
等方向);前58集團技術委員會主席,高級系統架構師;前百度資深研發工程師;

 

【架構之美】微信公衆號作者;擅長系統架構設計,大數據,運維、機器學習等技術領域;代表公司多次在業界頂級技術大會 CIO
峯會、Artificial、Intelligence、Conference、A2M、QCon、ArchSummit、SACC、SDCC、CCTC、DTCC、Top100、Strata
+、Hadoop World、WOT、GITC、GIAC、TID等發表演講,併爲《程序員》雜誌撰稿 2 篇。

1、CAP 定律剖析

2000 年 Eric Brewer 教授提出 CAP 猜想,2 年後 CAP 猜想被 Seth Gilbert 和Nancy Lynch 從理論上證明。CAP 是 Consitency(強一致性)、Availability(可用性)、Partition tolerance(網絡分區容忍性)三個不同維度的組合體,如圖 1 所示:
圖1 CAP定律
在分佈式系統中,CAP 定律中的三者只能同時滿足二者(如圖 1 所示):CP、AP、AC 模型。進一步分析,AC 模型並不真正的存在,脫離 P(分佈式環境)談AC 都是耍流氓。

我們以多機房數據庫同步更新的場景來分析下爲什麼 CAP 定律中三者不能同時滿足,如圖 2 所示,用戶通過機房一的數據訪問層寫入數據到 MySQL 主庫,並通過網絡把此數據同步到機房二的 MySQL 從庫。

在此場景下,CAP 對應的含義爲:C 爲機房一的 MySQL 數據庫主節點更新,那麼機房二的 MySQL 數據從節點也要更新;A 爲必須保證 MySQL 主從兩個數據節點都是可用的;P 爲當機房一和機房二主從節點出現網絡分區,必須保證系統對外可用。

對於機房一的寫請求,一旦機房一和機房二出現網絡分區(即網絡斷開),此時寫請求無法成功寫入到機房二的 MySQL 從庫,就會導致寫請求無法返回,即 A(可用性)無法滿足。大家可以思考一個問題,假設機房一和機房二的網絡終究會恢復,用戶側也能夠容忍機房網絡恢復的時間一直等待,那麼 CAP 定律是否同時滿足?

圖2 CAP定律多機房數據同步場景驗證

2、業務場景驅動

我們來看三個典型的業務場景:

業務場景一:在秒殺的場景下,只允許用戶購買一件商品;

業務場景二:用戶下單成功後會產生下單消息,在訂單消息響應應答模式下會發送多條消息到 MQ 中,下游在對 MQ 中訂單消息進行消費時,需要對此訂單消息進行去重;

業務場景三:在用戶對商品下單後,訂單狀態變爲待支付,在某一時刻用戶正在對該訂單做支付操作,商家對該訂單進行改價操作,如何保證操作的數據一致性。

通過業務需求剖析業務背後的本質,是架構師需要具備的核心能力之一。這三個業務場景背後共性的本質是什麼?

業務場景一需要對用戶進行併發控制,也就是需要對用戶 ID 進行串行化操作處理,防止用戶重複下單;

業務場景二需要對訂單消息中的訂單 ID 進行串行化操作處理,防止下游對訂單消息重複消費;

業務場景三需要對訂單 ID 進行串行化操作處理,防止出現數據的不一致性。

既然三個場景都需要對共享資源進行串行化處理,問題轉化爲鎖處理的問題。如果是單機環境通過本地鎖的方式可以優雅解決(如圖 3),在分佈式的環境下,服務冗餘部署多份,不同的請求由不用的冗餘服務來處理,本地鎖將不能很好的工作,需要分佈式鎖進行處理(如圖 4)。

圖3 本地鎖
圖4 分佈式鎖

3、分佈式鎖本質

提到分佈式鎖,大家能夠想到基於 Redis 來實現,鎖的本質是對共享資源串行化處理。Redis 內部採用唯一線程的串行化處理請求恰好滿足鎖的使用場景。那麼基於 Redis 如何具體實現分佈式鎖?我們從具體命令和架構兩個維度進行分析。

在具體命令實現側,有兩種方式,第一採用 Redis SetNX(Set if Not eXsits)命令,此命令在指定的 Key 不存在時,爲 Key 設置指定的值。SETNX Key Value 命令設置成功返回 1,設置失敗,返回 0。

比如在業務場景一,用戶 ID 爲 1009,此時 Key 爲 1009,Value 可以隨意填寫,例如 100。當兩個服務同時調用調用 SETNT 1009 100 命令申請鎖時,Redis 保證只有一個服務能夠成功申請到鎖。對於鎖我們需要加上鎖使用的時間,確保鎖的公平性,最壞情況下,其他服務能夠有機會申請到這把鎖。

爲了達到這個目的,需要對鎖進一步添加過期時間,使用 EXPIRE Key seconds 命令,設置生存時間,比如爲用戶 ID1009 設置 10S 的生存時間:EXPIRE 1009 10。

由於 SETNX 命令和 EXPIRE 命令是兩條命令,需要保證他們同時執行成功。Redis 提供了事務的處理方式:採用【MULTI;多個執行命令;EXEC;】LUA 腳本執行語句組。業務場景一可以如下進行事務處理:

<span style="color:#000000"><code>MULTI;
SETNX 1009 100;
EXPIRE 1009 10;
EXEC;
</code></span>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

上述具體命令實現較爲複雜,在 Redis 2.6.12 及以上的版本,可以採用 Set Key Value NX PX milliseconds 命令方式,此命令在指定的 Key 不存在時,爲 Key 設置指定的值,並設置生存時間。

其中 NX 參數表示指定的 Key 不存在時,再設置指定的值;PX 參數表示生存時間,單位爲毫秒。業務場景一採用 Set 命令,具體調用命令如下:Set 1009 100 NX PX 10000。

在架構設計側,第一種方式採用 Redis 單機方式(如圖 5),當服務 S1 和服務 S2 同時申請鎖(Set 1009 100 NX PX 10000)簡稱爲 L1 時,Redis 單機會按照接受到請求先後順序的處理方式,保證 S1 申請到鎖 L1,S2 申請鎖 L1 失敗。(假設S1先申請鎖L1,S2後申請鎖L1)。

Redis 單機模式存在單點隱患,一旦 Redis 宕機,內存中的鎖全部丟失,Redis 再次啓動,假如此時服務 S1 還在使用鎖 L1,服務 S2 又再次申請鎖 L1,就會申請成功,此時就會出現同一時刻 2 個服務同時都拿到同一把鎖的尷尬局面。
圖 5 分佈式鎖Redis單機模式
第一種方式問題在於 Redis 的單機模式,通過使用 Redis 集羣的主從模式來解決Redis 單機模式的數據丟失問題。

第二種方式如同 6 所示,在業務場景一,當服務 S1 和服務 S2 同時在 Redis 主節點申請鎖 L1 時,服務 S1 申請到鎖 L1。通過 Redis 主從集羣的數據同步機制會異步同步給 Redis 從節點,Redis 從節點也擁有了鎖 L1。假設 Redis 主節點掛掉,由於 Redis 集羣的 Sentinal 的哨兵監控和主從切換機制,此時 Redis 集羣的從節點會提升爲新 Redis 集羣的主節點,繼續對外提供鎖申請服務,使得鎖申請服務繼續正常進行。

大家思考一個極端的場景,如圖 7 所示,服務 S1 剛在 Redis 集羣主節點申請到鎖 L1,鎖 L1 還未同步到 Redis 集羣從節點,此時 Redis 集羣主節點掛掉,根據Redis 集羣的 Sentinal 哨兵機制,會把從節點提升爲新 Redis 集羣的主節點,而服務 S2 繼續在新 Redis 集羣的主節點申請鎖 L1,那麼服務 S2 就會成功申請到鎖 L1。則再次出現服務 S1 和服務 S2 在此時同時申請到鎖 L1 的情況。

那爲何會造成這樣的情況?我們從架構的本質(CAP 模型)來深入分析下原因。

圖6 分佈式鎖Redis集羣主從模式
圖7 分佈式鎖Redis集羣主從模式主節點故障
我們要保證同一把分佈式鎖的申請在同一時刻只能有一個服務拿到此鎖,因此從 CAP 模型底層分析,分佈式鎖是 CP 模型。

而 Redis 集羣的主從模式是 AP 模型。也就是說從架構設計哲學層面來看,分佈式鎖選用 Redis 集羣的主從模式就是不優雅的,從而導致了上述一系列問題的出現。

但是,當在百度裏搜索分佈式鎖,有很多的實現方案是基於 Redis 集羣,爲什麼會是這樣?

我們繼續深入分析,一切脫離場景談架構都是耍流氓,特別是脫離業務場景。

業務場景分爲 2 類:追求數據強一致性場景、追求數據最終一致性場景。

數據強一致性場景:比如金融、電商交易等,使用分佈式鎖時需要使用 CP 模型,不然就會出現支付去重失敗等重大問題,此時公司離破產只差用戶一個大請求;

數據最終一致性場景:比如微信發消息等,在使用分佈式鎖時使用AP模型較優雅,比如對用戶發送消息(今晚有空嗎?約個飯)的去重,極端情況下使用分佈式鎖去重失敗,也就是消息發送到對方 2 次,反而會增加彼此之間的感情,本來要拒絕邀請的,由於收到 2 次邀請消息,結果就不好意思拒絕了。

4、分佈式鎖設計與實踐

分佈式鎖存儲選型至關重要,以下對比了 Redis、ZooKeeper、etcd 等存儲模型,如圖8 所示。

通過以上的分析,對於數據一致性要求高的業務場景需使用 CP 型的存儲模型。

Zookeeper 多鎖實現使用創建臨時節點和 Watch 機制,在執行效率、擴展能力以及社區活躍度等方面低於 etcd,因爲選用基於 etcd 作爲分佈式鎖的存儲模型。

圖8 分佈式鎖存儲模型選型對比
分佈式鎖的架構設計如圖 8 所示,由 etcd 存儲集羣、分佈式鎖客戶端、監控平臺等三部分構成。

其中 etcd 集羣負責鎖的申請、續租、釋放等操作處理,分佈式鎖客戶端通過 HTTP API對 etcd 集羣進行操作,從而使得微服務調用方能夠申請鎖、對鎖進行續租、對鎖探活、鎖操作的監控等。

在部署層面,etcd 集羣至少需要部署 3 臺,分佈式鎖客戶端以 SDK 的方式嵌入到微服務中。

5、總結

從架構設計哲學層面分析,分佈式鎖本質上是 CP 模型。一切脫離場景談架構設計都是耍流氓,因此我們需要針對業務場景的不同,選用優雅的分佈式鎖實現,在追求數據強一致性的業務場景中,選用 CP 存儲模型,在追求數據最終一致性的業務場景中,選用 AP 存儲模型。

發佈了50 篇原創文章 · 獲贊 99 · 訪問量 102萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章