分佈式鎖與冪等性問題

分佈式系統由獨立的服務器通過網絡鬆散耦合組成。在這個系統中每個服務器都是一臺獨立的主機,服務器之間通過內部網絡連接。分佈式系統有以下幾個特點:

  • 可擴展性:可通過橫向水平擴展提高系統的性能和吞吐量。
  • 高可靠性:高容錯,即使系統中一臺或幾臺故障,系統仍可提供服務。
  • 高併發性:各機器並行獨立處理和計算。
  • 廉價高效:多臺小型機而非單臺高性能機。

互斥性(鎖)

分佈式鎖條件

(1):存儲空間
鎖是一個抽象的概念,鎖的實現,需要依存於一個可以存儲鎖的空間。在多線程中是內存,在多進程中是內存或者磁盤。更重要的是,這個空間是可以被訪問到的。多線程中,不同的線程都可以訪問到堆中的成員變量;在多進程中,不同的進程可以訪問到共享內存中的數據或者存儲在磁盤中的文件。但是在分佈式環境中,不同的主機很難訪問對方的內存或磁盤。這就需要一個都能訪問到的外部空間來作爲存儲空間

  • 基於數據庫做分佈式鎖(行鎖、version樂觀鎖),如quartz集羣架構中就有所使用
數據庫表,字段爲鎖的ID(唯一標識),鎖的狀態(0表示沒有被鎖,1表示被鎖)。

僞代碼爲:
      lock = mysql.get(id);
      while(lock.status == 1) 
       {
         sleep(100);
        }
        mysql.update(lock.status = 1);
        doSomething();
        mysql.update(lock.status = 0);


存在問題:
問題1:鎖狀態判斷原子性無法保證 
  從讀取鎖的狀態,到判斷該狀態是否爲被鎖,需要經歷兩步操作。如果不能保證這兩步的原子性,就可能導致不止一個請求獲取到了鎖,這顯然是不行的。因此,我們需要保證鎖狀態判斷的原子性。
問題2:網絡斷開或主機宕機,鎖狀態無法清除 
問題3:無法保證釋放的是自己上鎖的那把鎖 
假設持有鎖的主機A在臨界區遇到網絡抖動導致網絡斷開,分佈式鎖及時的釋放掉了這把鎖。之後,另一個主機B佔有了這把鎖,但是此時主機A網絡恢復,退出臨界區時解鎖。由於都是同一把鎖,所以A就會將B的鎖解開。此時如果有第三個主機嘗試搶佔這把鎖,也將會成功獲得。因此,我們需要在解鎖時,確定自己解的這個鎖正是自己鎖上的。

進階條件,在實際的系統環境中,還會對分佈式鎖有更高級的要求。

1:可重入:線程中的可重入,指的是外層函數獲得鎖之後,內層也可以獲得鎖,ReentrantLock和synchronized都是可重入鎖;衍生到分佈式環境中,一般仍然指的是線程的可重入,在絕大多數分佈式環境中,都要求分佈式鎖是可重入的。
2:驚羣效應(Herd Effect):在分佈式鎖中,驚羣效應指的是,在有多個請求等待獲取鎖的時候,一旦佔有鎖的線程釋放之後,如果所有等待的方都同時被喚醒,嘗試搶佔鎖。但是這樣的情況會造成比較大的開銷,那麼在實現分佈式鎖的時候,應該儘量避免驚羣效應的產生。:
3:公平鎖和非公平鎖:不同的需求,可能需要不同的分佈式鎖。非公平鎖普遍比公平鎖開銷小。但是業務需求如果必須要鎖的競爭者按順序獲得鎖,那麼就需要實現公平鎖。:
4:阻塞鎖和自旋鎖:針對不同的使用場景,阻塞鎖和自旋鎖的效率也會有所不同。阻塞鎖會有上下文切換,如果併發量比較高且臨界區的操作耗時比較短,那麼造成的性能開銷就比較大了。但是如果臨界區操作耗時比較長,一直保持自旋,也會對CPU造成更大的負荷。
  • 緩存如Redis、Tair、Memcached、MongoDB
Redis的分佈式緩存特性使其成爲了分佈式鎖的一種基礎實現。通過Redis中是否存在某個鎖ID,則可以判斷是否上鎖。爲了保證判斷鎖是否存在的原子性,保證只有一個線程獲取同一把鎖,Redis有SETNX(即SET if Not eXists)和GETSET(先寫新值,返回舊值,原子性操作,可以用於分辨是不是首次操作)操作

爲了防止主機宕機或網絡斷開之後的死鎖,Redis沒有ZK那種天然的實現方式,只能依賴設置超時時間來規避

除此以外還可以用SETNX+EXPIRE來實現。Redisson是一個官方推薦的Redis客戶端並且實現了很多分佈式的功能。它的分佈式鎖就提供了一種更完善的解決方案
  • 專門的分佈式協調服務Zookeeper
ZooKeeper中有一種節點叫做順序節點,假如我們在/lock/目錄下創建3個節點,ZK集羣會按照發起創建的順序來創建節點,節點分別爲/lock/0000000001/lock/0000000002/lock/0000000003。

ZK中還有一種名爲臨時節點的節點,臨時節點由某個客戶端創建,當客戶端與ZK集羣斷開連接,則該節點自動被刪除。EPHEMERAL_SEQUENTIAL爲臨時順序節點。

根據ZK中節點是否存在,可以作爲分佈式鎖的鎖狀態,以此來實現一個分佈式鎖:
1:客戶端調用create()方法創建名爲“/dlm-locks/lockname/lock-”的臨時順序節點。
2:客戶端調用getChildren(“lockname”)方法來獲取所有已經創建的子節點。
3:客戶端獲取到所有子節點path之後,如果發現自己在步驟1中創建的節點是所有節點中序號最小的,那麼就認爲這個客戶端獲得了鎖。
4:如果創建的節點不是所有節點中需要最小的,那麼則監視比自己創建節點的序列號小的最大的節點,進入等待。直到下次監視的子節點變更的時候,再進行子節點的獲取,判斷是否獲取鎖。

釋放鎖的過程相對比較簡單,就是刪除自己創建的那個子節點即可,不過也仍需要考慮刪除節點失敗等異常情況。

(2):唯一標識
不同的共享資源,必然需要用不同的鎖進行保護,因此相應的鎖必須有唯一的標識。在多線程環境中,鎖可以是一個對象,那麼對這個對象的引用便是這個唯一標識。多進程環境中,信號量在共享內存中也是由引用來作爲唯一的標識。因此,在分佈式環境中,只要給這個鎖設定一個名稱,並且保證這個名稱是全局唯一的,那麼就可以作爲唯一標識。
(3):至少兩種狀態
爲了給臨界區加鎖和解鎖,需要存儲兩種不同的狀態。如ReentrantLock中的status,0表示沒有線程競爭,大於0表示有線程競爭;信號量大於0表示可以進入臨界區,小於等於0則表示需要被阻塞。因此只要在分佈式環境中,鎖的狀態有兩種或以上:如有鎖、沒鎖;存在、不存在等,均可以實現

冪等性問題

所謂冪等,簡單地說,就是對接口的多次調用所產生的結果和調用一次是一致的。
那麼我們爲什麼需要接口具有冪等性呢?設想一下以下情形:

1:在App中下訂單的時候,點擊確認之後,沒反應,就又點擊了幾次。在這種情況下,如果無法保證該接口的冪等性,那麼將會出現重複下單問題。
2:在接收消息的時候,消息推送重複。如果處理消息的接口無法保證冪等,那麼重複消費消息產生的影響可能會非常大。

GTIS

它是一個輕量的重複操作關卡系統,它能夠確保在分佈式環境中操作的唯一性。我們可以用它來間接保證每個操作的冪等性。它具有如下特點:

  • 高效:低延時,單個方法平均響應時間在2ms內,幾乎不會對業務造成影響;
  • 可靠:提供降級策略,以應對外部存儲引擎故障所造成的影響;提供應用鑑權,提供集羣配置自定義,降低不同業務之間的干擾;
  • 簡單:接入簡捷方便,學習成本低。只需簡單的配置,在代碼中進行兩個方法的調用即可完成所有的接入工作;
  • 靈活:提供多種接口參數、使用策略,以滿足不同的業務需求。

實現原理

  • 基本原理
    GTIS的實現思路是將每一個不同的業務操作賦予其唯一性。這個唯一性是通過對不同操作所對應的唯一的內容特性生成一個唯一的全局ID來實現的。基本原則爲:相同的操作生成相同的全局ID;不同的操作生成不同的全局ID。
    生成的全局ID需要存儲在外部存儲引擎中,數據庫、Redis亦或是Tair等均可實現。考慮到Tair天生分佈式和持久化的優勢,目前的GTIS存儲在Tair中。其相應的key和value如下:
key:將對於不同的業務,採用APP_KEY+業務操作內容特性生成一個唯一標識trans_contents。然後對唯一標識進行加密生成全局ID作爲Keyvaluecurrent_timestamp + trans_contents,current_timestamp用於標識當前的操作線程。

判斷是否重複,主要利用Tair的SETNX方法,如果原來沒有值則set且返回成功,如果已經有值則返回失敗。

  • 內部流程
    GTIS的內部實現流程爲:

1:業務方在業務操作之前,生成一個能夠唯一標識該操作的transContents,傳入GTIS;
2:GTIS根據傳入的transContents,用MD5生成全局ID;
3:GTIS將全局ID作爲key,current_timestamp+transContents作爲value放入Tair進行setNx,將結果返回給業務方;
4:業務方根據返回結果確定能否開始進行業務操作;
5:若能,開始進行操作;若不能,則結束當前操作;
6:業務方將操作結果和請求結果傳入GTIS,系統進行一次請求結果的檢驗;
7:若該次操作成功,GTIS根據key取出value值,跟傳入的返回結果進行比對,如果兩者相等,則將該全局ID的過期時間改爲較長時間;
8:GTIS返回最終結果。

  • 實現難點
    GTIS的實現難點在於如何保證其判斷重複的可靠性。由於分佈式環境的複雜度和業務操作的不確定性,在上一章節分佈式鎖的實現中考慮的網絡斷開或主機宕機等問題,同樣需要在GTIS中設法解決。這裏列出幾個典型的場景:
    1:如果操作執行失敗,理想的情況應該是另一個相同的操作可以立即進行。因此,需要對業務方的操作結果進行判斷,如果操作失敗,那麼就需要立即刪除該全局ID;
    2:如果操作超時或主機宕機,當前的操作無法告知GTIS操作是否成功。那麼我們必須引入超時機制,一旦長時間獲取不到業務方的操作反饋,那麼也需要該全局ID失效;
    3:結合上兩個場景,既然全局ID會失效並且可能會被刪除,那就需要保證刪除的不是另一個相同操作的全局ID。這就需要將特殊的標識記錄下來,並由此來判斷。這裏所用的標識爲當前時間戳。
  • 使用說明
    使用時,業務方只需要在操作的前後調用GTIS的前置方法和後置方法,如下圖所示。如果前置方法返回可進行操作,則說明此時無重複操作,可以進行。否則則直接結束操作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章