分佈式鎖入門及常見實現方式介紹

前言

想寫這篇博客 是根據南國之前面試經歷 經常被問到zokeeper實現分佈式鎖的原理,之前太忙 也沒時間整理資料。通過今天這篇博文 對分佈式鎖以及實現方式做一個小的總結概括。
本篇博客屬於轉載,這裏我綜合了之前所看過的一些資料 通過自己的理解寫下這篇博客,在此感謝以下精彩好文。
1.分佈式鎖簡單入門以及三種實現方式介紹
2.分佈式鎖的幾種使用方式(redis、zookeeper、數據庫)

1.使用分佈式鎖的業務場景

對Java多線程瞭解的讀者 應該知道許多的併發 多線程的知識點。在應用開發中,如果需要對某一個共享變量進行多線程同步訪問的時候,我們可以用Synchronized關鍵字保證線程安全。但是我們要知道這是單機應用,也就是所有的請求都會分配到當前服務器的JVM內部,然後映射爲操作系統的線程進行處理!而這個共享變量只是在這個JVM內部的一塊內存空間!

舉例業務場景:一個業務服務器,一個數據庫。
要實現的操作:查詢用戶當前餘額,扣除當前餘額的3%作爲手續費
實現方式:

  • synchronized
  • lock(ReentranceLock)
  • db lock

但是當業務擴展,一個應用需要部署到幾臺機器上進行負載均衡時,大致如下圖:
在這裏插入圖片描述
上圖可以看到,變量A存在JVM1、JVM2、JVM3三個JVM內存中(這個變量A主要體現是在一個類中的一個成員變量,是一個有狀態的對象,例如:UserController控制器中的一個整形類型的成員變量),如果不加任何控制的話,變量A同時都會在JVM分配一塊內存,三個請求發過來同時對這個變量操作,顯然結果是不對的!即使不是同時發過來,三個請求分別操作三個不同JVM內存區域的數據,變量A之間不存在共享,也不具有可見性,處理的結果也是不對的!

爲了保證一個方法或屬性在高併發情況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。在單機環境中,Java中提供了很多併發處理相關的API。但是,隨着業務發展的需要,原單體單機部署的系統被演化成分佈式集羣系統後,由於分佈式系統多線程、多進程並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。

舉例業務場景:多個業務i服務器,一個數據庫。
實現的操作:查詢用戶當前月,扣除當前餘額的3%作爲手續費。
爲了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題

2. 分佈式鎖應當具備的條件

在分析分佈式鎖的三種實現方式之前,先了解一下分佈式鎖應該具備哪些條件:
1、在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
2、高可用的獲取鎖與釋放鎖;
3、高性能的獲取鎖與釋放鎖;
4、具備可重入特性;
5、具備鎖失效機制,防止死鎖;
6、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。

3. 分佈式鎖的3種方式

目前幾乎很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴我們“任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的範圍內即可。

在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。

  • 基於數據庫實現分佈式鎖;
  • 基於緩存(Redis等)實現分佈式鎖;
  • 基於Zookeeper實現分佈式鎖;

儘管有這三種方案,但是不同的業務也要根據自己的情況進行選型,他們之間沒有最好只有更適合!

4. 基於數據庫(MySQL)的實現方式

基於數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖
(1) 創建一個表:

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',
  `desc` varchar(255) NOT NULL COMMENT '備註信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

在這裏插入圖片描述
(2)向表中插入數據:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因爲我們對method_name做了唯一性約束,這裏如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那麼我們就可以認爲操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

(3) 成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:
delete from method_lock where method_name =‘methodName’;

當然這只是基於數據實現分佈式鎖的一種方法,還有其他方法來實現分佈式鎖。
上面這種簡單的實現有以下幾個問題:

  • 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。
  • 這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
  • 這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。
  • 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。
  • 這把鎖是非公平鎖,所有等待鎖的線程憑運氣去爭奪鎖。

當然,我們也可以有其他方式解決上面的問題。

  • 數據庫是單點?搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。
  • 沒有失效時間?只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
  • 非阻塞的?搞一個while循環,直到insert成功再返回成功。
  • 非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。
  • 非公平的?再建一張中間表,將等待鎖的線程全記錄下來,並根據創建時間排序,只有最先創建的允許獲取鎖

基於排他鎖實現的分佈式鎖
除了可以通過增刪操作數據表中的記錄以外,其實還可以藉助數據中自帶的鎖來實現分佈式的鎖。

我們還用剛剛創建的那張數據庫表。可以通過數據庫的排他鎖來實現分佈式鎖。 基於MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:

public boolean lock(){    
    connection.setAutoCommit(false);
    while(true){        
        try{            
            result = select * from methodLock where method_name=xxx for update;            
            if(result==null){                
                return true;           
            }        
        }catch(Exception e){

        }
        sleep(1000);
    }
    return false;
}

在查詢語句後面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖。

我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖,當獲取到鎖之後,可以執行方法的業務邏輯,執行完方法之後,再通過以下方法解鎖:
public void unlock(){ connection.commit(); }

通過connection.commit();操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖? for update語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。
鎖定之後服務宕機,無法釋放?使用這種方式,服務宕機之後數據庫會自己把鎖釋放掉。
但是還是無法直接解決數據庫單點、可重入和公平鎖的問題。

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

數據庫實現分佈式鎖的優點
直接藉助數據庫,容易理解。

數據庫實現分佈式鎖的缺點
會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。
操作數據庫需要一定的開銷,性能問題需要考慮。

5. 基於緩存的分佈式鎖

目前有很多成熟的緩存產品,包括Redis,memcached等。這裏以Redis爲例來分析下使用緩存實現分佈式鎖的方案。
(1)選用Redis四線分佈式鎖的原因:

  • Redis有很高的性能;
  • Redis命令對此支持比較好,實現起來比較方便;

(2)使用命令介紹:

  • SETNX:
    SETNX key val:當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不做,返回0。
  • expire
    expire key timeout:爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
  • delete
    delete key:刪除key

(3)實現思想:

  • 獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
  • 獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
  • 釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

6. 基於zookeeper實現的分佈式鎖

ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。
基於ZooKeeper實現分佈式鎖的步驟如下:
(1)創建一個目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
(3)獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

簡單來講,它是基於zookeeper臨時有序節點實現的分佈式鎖。
之類我們分析下zk能否解決之前提到的問題:

  • 鎖無法釋放?使用Zookeeper可以有效的解決鎖無法釋放的問題,因爲在創建鎖的時候,客戶端會在ZK中創建一個臨時節點,一旦客戶端獲取到鎖之後突然掛掉(Session連接斷開),那麼這個臨時節點就會自動刪除掉。其他客戶端就可以再次獲得鎖。
  • 非阻塞鎖?使用Zookeeper可以實現阻塞的鎖,客戶端可以通過在ZK中創建順序節點,並且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端可以檢查自己創建的節點是不是當前所有節點中序號最小的,如果是,那麼自己就獲取到鎖,便可以執行業務邏輯了。
  • 不可重入?使用Zookeeper也可以有效的解決不可重入的問題,客戶端在創建節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就可以了。如果和自己的信息一樣,那麼自己直接獲取到鎖,如果不一樣就再創建一個臨時的順序節點,參與排隊。
  • 單點問題?使用Zookeeper可以有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就可以對外提供服務。
  • 公平問題?使用Zookeeper可以解決公平鎖問題,客戶端在ZK中創建的臨時節點是有序的,每次鎖被釋放時,ZK可以通知最小節點來獲取鎖,保證了公平。

這裏推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
關於Curator的源碼分析,可參考10分鐘看懂!基於Zookeeper的分佈式鎖

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因爲需要頻繁的創建和刪除節點,性能上不如Redis方式。

備註:南國對於Redis知識點的空缺 使得在這篇博客裏對於基於緩存(Redis)的分佈式鎖實現 沒有做過多敘述,慚愧自己的技術棧太小。關於zookeeper的知識點 日後 有時間會再詳細敘述~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章