分佈式鎖 原理及實現方式

1、原理html

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要經過一些互斥手段來防止彼此之間的干擾,以保證一致性,在這種狀況下,就須要使用分佈式鎖了。redis

在平時的實際項目開發中,咱們每每不多會去在乎分佈式鎖,而是依賴於關係型數據庫固有的排他性來實現不一樣進程之間的互斥,但大型分佈式系統的性能瓶頸每每集中在數據庫操做上。算法

 

在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。數據庫

 

其實秒殺類場景最主要的是執行秒殺操做要單線程的,提到單線程,確定會想到synchronized關鍵字,可是他有兩個致命缺點:一、沒法作到細粒度控制,二、只適合單點不適用集羣。
因此大多數項目只能採用分佈式鎖的實現方式。
 
 
針對分佈式鎖的實現,目前比較經常使用的有如下幾種方案:
    基於數據庫實現分佈式鎖
    基於緩存(redis,memcached,tair)實現分佈式鎖
    基於Zookeeper實現分佈式鎖
 
分佈式鎖主要有基於緩存如redis、基於zookeeper、基於數據庫的實現。
 

在分析這幾種實現方案以前咱們先來想一下,咱們須要的分佈式鎖應該是怎麼樣的?(這裏以方法鎖爲例,資源鎖同理)apache

    

能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。緩存

  

能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。服務器

    這把鎖要是一把可重入鎖(避免死鎖)網絡

    這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)session

    有高可用的獲取鎖和釋放鎖功能併發

    獲取鎖和釋放鎖的性能要好

 

 

 

 

2、實現方式

基於數據庫實現分佈式鎖

基於數據庫表

要實現分佈式鎖,最簡單的方式可能就是直接建立一張鎖表,而後經過操做該表中的數據來實現了。

當咱們要鎖住某個方法或資源時,咱們就在該表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。

建立這樣一張數據庫表:

當咱們想要鎖住某個方法時,執行如下SQL:

由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。

當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:

上面這種簡單的實現有如下幾個問題:

一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。

二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。

三、這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。

四、這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。

固然,咱們也能夠有其餘方式解決上面的問題。

  • 數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。
  • 沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。
  • 非阻塞的?搞一個while循環,直到insert成功再返回成功。
  • 非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。

基於數據庫排他鎖

除了能夠經過增刪操做數據表中的記錄之外,其實還能夠藉助數據中自帶的鎖來實現分佈式的鎖。

咱們還用剛剛建立的那張數據庫表。能夠經過數據庫的排他鎖來實現分佈式鎖。 基於MySql的InnoDB引擎,可使用如下方法來實現加鎖操做:

在查詢語句後面增長for update,數據庫會在查詢過程當中給數據庫表增長排他鎖(這裏再多提一句,InnoDB引擎在加鎖的時候,只有經過索引進行檢索的時候纔會使用行級鎖,不然會使用表級鎖。這裏咱們但願使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引必定要建立成惟一索引,不然會出現多個重載方法之間沒法同時被訪問的問題。重載方法的話建議把參數類型也加上。)。當某條記錄被加上排他鎖以後,其餘線程沒法再在該行記錄上增長排他鎖。

咱們能夠認爲得到排它鎖的線程便可得到分佈式鎖,當獲取到鎖以後,能夠執行方法的業務邏輯,執行完方法以後,再經過如下方法解鎖:

經過connection.commit()操做來釋放鎖。

這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。

  • 阻塞鎖? for update語句會在執行成功後當即返回,在執行失敗時一直處於阻塞狀態,直到成功。
  • 鎖定以後服務宕機,沒法釋放?使用這種方式,服務宕機以後數據庫會本身把鎖釋放掉。

可是仍是沒法直接解決數據庫單點和可重入問題。

這裏還可能存在另一個問題,雖然咱們對method_name 使用了惟一索引,而且顯示使用for update來使用行級鎖。可是,MySql會對查詢進行優化,即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣執行計劃的代價來決定的,若是 MySQL 認爲全表掃效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。若是發生這種狀況就悲劇了。。。


還有一個問題,就是咱們要使用排他鎖來進行分佈式鎖的lock,那麼一個排他鎖長時間不提交,就會佔用數據庫鏈接。一旦相似的鏈接變得多了,就可能把數據庫鏈接池撐爆

總結

總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。

數據庫實現分佈式鎖的優勢

直接藉助數據庫,容易理解。

數據庫實現分佈式鎖的缺點

會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。

操做數據庫須要必定的開銷,性能問題須要考慮。

使用數據庫的行級鎖並不必定靠譜,尤爲是當咱們的鎖表並不大的時候。


基於緩存實現分佈式鎖

相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。並且不少緩存是能夠集羣部署的,能夠解決單點問題。

目前有不少成熟的緩存產品,包括Redis,memcached以及咱們公司內部的Tair。

這裏以Tair爲例來分析下使用緩存實現分佈式鎖的方案。關於Redis和memcached在網絡上有不少相關的文章,而且也有一些成熟的框架及算法能夠直接使用。

基於Tair的實現分佈式鎖其實和Redis相似,其中主要的實現方式是使用TairManager.put方法來實現。

以上實現方式一樣存在幾個問題:

一、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在tair中,其餘線程沒法再得到到鎖。

二、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。

三、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在tair中已經存在。沒法再執行put操做。

固然,一樣有方式能夠解決。

  • 沒有失效時間?tair的put方法支持傳入失效時間,到達時間以後數據會自動刪除。
  • 非阻塞?while重複執行。
  • 非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。

可是,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數據庫實現分佈式鎖一樣存在


總結

可使用緩存來代替數據庫來實現分佈式鎖,這個能夠提供更好的性能,同時,不少緩存服務都是集羣部署的,能夠避免單點問題。而且不少緩存服務都提供了能夠用來實現分佈式鎖的方法,好比Tair的put方法,redis的setnx方法等。而且,這些緩存服務也都提供了對數據的過時自動刪除的支持,能夠直接設置超時時間來控制鎖的釋放。

使用緩存實現分佈式鎖的優勢

性能好,實現起來較爲方便。

使用緩存實現分佈式鎖的缺點

經過超時時間來控制鎖的失效時間並非十分的靠譜。


基於Zookeeper實現分佈式鎖

基於zookeeper臨時有序節點能夠實現的分佈式鎖。

大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。

來看下Zookeeper能不能解決前面提到的問題。

  • 鎖沒法釋放?使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。

  • 非阻塞鎖?使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。

  • 不可重入?使用Zookeeper也能夠有效的解決不可重入的問題,客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。

  • 單點問題?使用Zookeeper能夠有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。

能夠直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務。

Curator提供的InterProcessMutex是分佈式鎖的實現。acquire方法用戶獲取鎖,release方法用於釋放鎖。

使用ZK實現的分佈式鎖好像徹底符合了本文開頭咱們對一個分佈式鎖的全部指望。可是,其實並非,Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並無緩存服務那麼高。由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能經過Leader服務器來執行,而後將數據同不到全部的Follower機器上。

其實,使用Zookeeper也有可能帶來併發問題,只是並不常見而已。考慮這樣的狀況,因爲網絡抖動,客戶端可ZK集羣的session鏈接斷了,那麼zk覺得客戶端掛了,就會刪除臨時節點,這時候其餘客戶端就能夠獲取到分佈式鎖了。就可能產生併發問題。這個問題不常見是由於zk有重試機制,一旦zk集羣檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。屢次重試以後還不行的話纔會刪除臨時節點。(因此,選擇一個合適的重試策略也比較重要,要在鎖的粒度和併發之間找一個平衡。)


總結

使用Zookeeper實現分佈式鎖的優勢

有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。

使用Zookeeper實現分佈式鎖的缺點

性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。


三種方案的比較

上面幾種方式,哪一種方式都沒法作到完美。就像CAP同樣,在複雜性、可靠性、性能等方面沒法同時知足,因此,根據不一樣的應用場景選擇最適合本身的纔是王道。

從理解的難易程度角度(從低到高)

數據庫 > 緩存 > Zookeeper

從實現的複雜性角度(從低到高)

Zookeeper >= 緩存 > 數據庫

從性能角度(從高到低)

緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低)

Zookeeper > 緩存 > 數據庫

 
 
 
 
 
 
 
 
 
參考:https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
相關文章
相關標籤/搜索