天天看點

分布式鎖小結

一、為什麼要使用分布式鎖

我們在開發應用的時候,如果需要對某一個共享變量進行多線程同步通路的時候,可以使用我們學到的Java多線程的18般武藝進行處理,并且可以完美的運作,毫無Bug!

注意這是單機應用,也就是所有的請求都會配置設定到目前伺服器的JVM内部,然後映射為作業系統的線程進行處理!而這個共享變量隻是在這個JVM内部的一塊記憶體空間!

後來業務發展,需要做叢集,一個應用需要部署到幾台機器上然後做負載均衡,大緻如下圖:

分布式鎖小結

上圖可以看到,變量A存在JVM1、JVM2、JVM3三個JVM記憶體中(這個變量A主要展現是在一個類中的一個成員變量,是一個有狀态的對象,例如:UserController控制器中的一個整形類型的成員變量),如果不加任何控制的話,變量A同時都會在JVM配置設定一塊記憶體,三個請求發過來同時對這個變量操作,顯然結果是不對的!即使不是同時發過來,三個請求分别操作三個不同JVM記憶體區域的資料,變量A之間不存在共享,也不具有可見性,處理的結果也是不對的!

如果我們業務中确實存在這個場景的話,我們就需要一種方法解決這個問題!

為了保證一個方法或屬性在高并發情況下的同一時間隻能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用Java并發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。在單機環境中,Java中提供了很多并發處理相關的API。但是,随着業務發展的需要,原單體單機部署的系統被演化成分布式叢集系統後,由于分布式系統多線程、多程序并且分布在不同機器上,這将使原單機部署情況下的并發控制鎖政策失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的通路,這就是分布式鎖要解決的問題!

二、分布式鎖應該具備哪些條件

在分析分布式鎖的三種實作方式之前,先了解一下分布式鎖應該具備哪些條件:

1、在分布式系統環境下,一個方法在同一時間隻能被一個機器的一個線程執行;

2、高可用的擷取鎖與釋放鎖;

3、高性能的擷取鎖與釋放鎖;

4、具備可重入特性;

5、具備鎖失效機制,防止死鎖;

6、具備非阻塞鎖特性,即沒有擷取到鎖将直接傳回擷取鎖失敗。

三、分布式鎖的三種實作方式

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

在很多場景中,我們為了保證資料的最終一緻性,需要很多的技術方案來支援,比如分布式事務、分布式鎖等。有的時候,我們需要保證一個方法在同一時間内隻能被同一個線程執行。

基于資料庫實作分布式鎖;

基于緩存(Redis等)實作分布式鎖;

基于Zookeeper實作分布式鎖;

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

四、基于資料庫的實作方式

基于資料庫的實作方式的核心思想是:在資料庫中建立一個表,表中包含方法名等字段,并在方法名字段上建立唯一索引,想要執行某個方法,就使用這個方法名向表中插入資料,成功插入則擷取鎖,執行完成後删除對應的行資料釋放鎖。

(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 method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');

1

因為我們對method_name做了唯一性限制,這裡如果有多個請求同時送出到資料庫的話,資料庫會保證隻有一個操作可以成功,那麼我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體内容。

(3)成功插入則擷取鎖,執行完成後删除對應的行資料釋放鎖:

delete from method_lock where method_name ='methodName';

注意:這隻是使用基于資料庫的一種方法,使用資料庫實作分布式鎖還有很多其他的玩法!

使用基于資料庫的這種實作方式很簡單,但是對于分布式鎖應該具備的條件來說,它有一些問題需要解決及優化:

1、因為是基于資料庫實作的,資料庫的可用性和性能将直接影響分布式鎖的可用性及性能,是以,資料庫需要雙機部署、資料同步、主備切換;

2、不具備可重入的特性,因為同一個線程在釋放鎖之前,行資料一直存在,無法再次成功插入資料,是以,需要在表中新增一列,用于記錄目前擷取到鎖的機器和線程資訊,在再次擷取鎖的時候,先查詢表中機器和線程資訊是否和目前機器和線程相同,若相同則直接擷取鎖;

3、沒有鎖失效機制,因為有可能出現成功插入資料後,伺服器當機了,對應的資料沒有被删除,當服務恢複後一直擷取不到鎖,是以,需要在表中新增一列,用于記錄失效時間,并且需要有定時任務清除這些失效的資料;

4、不具備阻塞鎖特性,擷取不到鎖直接傳回失敗,是以需要優化擷取邏輯,循環多次去擷取。

5、在實施的過程中會遇到各種不同的問題,為了解決這些問題,實作方式将會越來越複雜;依賴資料庫需要一定的資源開銷,性能問題需要考慮。

五、基于Redis的實作方式

1、選用Redis實作分布式鎖原因:

(1)Redis有很高的性能;

(2)Redis指令對此支援較好,實作起來比較友善

2、使用指令介紹:

(1)SETNX

SETNX key val:當且僅當key不存在時,set一個key為val的字元串,傳回1;若key存在,則什麼都不做,傳回0。

(2)expire

expire key timeout:為key設定一個逾時時間,機關為second,超過這個時間鎖會自動釋放,避免死鎖。

(3)delete

delete key:删除key

在使用Redis實作分布式鎖的時候,主要就會使用到這三個指令。

3、實作思想:

(1)擷取鎖的時候,使用setnx加鎖,并使用expire指令為鎖添加一個逾時時間,超過該時間則自動釋放鎖,鎖的value值為一個随機生成的UUID,通過此在釋放鎖的時候進行判斷。

(2)擷取鎖的時候還設定一個擷取的逾時時間,若超過這個時間則放棄擷取鎖。

(3)釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

4、 分布式鎖的簡單實作代碼:

5、基于ZooKeeper的實作方式

ZooKeeper是一個為分布式應用提供一緻性服務的開源元件,它内部是一個分層的檔案系統目錄樹結構,規定同一個目錄下隻能有一個唯一檔案名。基于ZooKeeper實作分布式鎖的步驟如下:

(1)建立一個目錄mylock;

(2)線程A想擷取鎖就在mylock目錄下建立 臨時順序 節點; // “臨時” 保證了即使A挂掉了也不會導緻死鎖, “順序” 保證了先 來的線程可以先擷取到鎖。

(3)擷取mylock目錄下所有的子節點,然後擷取比自己小的兄弟節點,如果不存在,則說明目前線程順序号最小,獲得鎖;

(4)線程B擷取所有節點,判斷自己不是最小節點,設定監聽比自己次小 1 的節點; // 隻監聽表自己小1 的節點, 而不是目前目錄, 避免 “羊群效應”  !!

(5)線程A處理完,删除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

這裡推薦一個Apache的開源庫Curator,它是一個ZooKeeper用戶端,Curator提供的InterProcessMutex是分布式鎖的實作,acquire方法用于擷取鎖,release方法用于釋放鎖。

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:因為需要頻繁的建立和删除節點,性能上不如Redis方式。

6、總結

上面的三種實作方式,沒有在所有場合都是完美的,是以,應根據不同的應用場景選擇最适合的實作方式。

在分布式環境中,對資源進行上鎖有時候是很重要的,比如搶購某一資源,這時候使用分布式鎖就可以很好地控制資源。

當然,在具體使用中,還需要考慮很多因素,比如逾時時間的選取,擷取鎖時間的選取對并發量都有很大的影響,上述實作的分布式鎖也隻是一種簡單的實作,主要是一種思想,以上包括文中的代碼可能并不适用于正式的生産環境,隻做入門參考!