1. 程式人生 > 其它 >三種實現分散式鎖的方式(TODO)

三種實現分散式鎖的方式(TODO)

原文:https://blog.csdn.net/wuzhiwei549/article/details/80692278

一、為什麼要使用分散式鎖
我們在開發應用的時候,如果需要對某一個共享變數進行多執行緒同步訪問的時候,可以使用我們學到的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.基於資料庫實現排他鎖
方案1

表結構


獲取鎖


INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');
對method_name做了唯一性約束,這裡如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個操作可以成功。

方案2
表結構

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 '鎖定的方法名',
`state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`version` int NOT NULL COMMENT '版本號',
`PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
先獲取鎖的資訊
select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

佔有鎖
update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

如果沒有更新影響到一行資料,則說明這個資源已經被別人佔位了。

缺點:

1、這把鎖強依賴資料庫的可用性,資料庫是一個單點,一旦資料庫掛掉,會導致業務系統不可用。
2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在資料庫中,其他執行緒無法再獲得到鎖。
3、這把鎖只能是非阻塞的,因為資料的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的執行緒並不會進入排隊佇列,要想再次獲得鎖就要再次觸發獲得鎖操作。
4、這把鎖是非重入的,同一個執行緒在沒有釋放鎖之前無法再次獲得該鎖。因為資料中資料已經存在了。

解決方案:
1、資料庫是單點?搞兩個資料庫,資料之前雙向同步。一旦掛掉快速切換到備庫上。
2、沒有失效時間?只要做一個定時任務,每隔一定時間把資料庫中的超時資料清理一遍。
3、非阻塞的?搞一個while迴圈,直到insert成功再返回成功。
4、非重入的?在資料庫表中加個欄位,記錄當前獲得鎖的機器的主機資訊和執行緒資訊,那麼下次再獲取鎖的時候先查詢資料庫,如果當前機器的主機資訊和執行緒資訊在資料庫可以查到的話,直接把鎖分配給他就可以了。

2.基於redis實現

獲取鎖使用命令:

SET resource_name my_random_value NX PX 30000
方案:

try{
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
logger.info("cancelCouponCode是否獲取到鎖:"+lock);
if (lock) {
// TODO
redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功設定過期時間
return res;
}else {
logger.info("cancelCouponCode沒有獲取到鎖,不執行任務!");
}
}finally{
if(lock){
redisTemplate.delete(lockKey);
logger.info("cancelCouponCode任務結束,釋放鎖!");
}else{
logger.info("cancelCouponCode沒有獲取到鎖,無需釋放鎖!");
}
}

缺點:

在這種場景(主從結構)中存在明顯的競態:
客戶端A從master獲取到鎖,
在master將鎖同步到slave之前,master宕掉了。
slave節點被晉級為master節點,
客戶端B取得了同一個資源被客戶端A已經獲取到的另外一個鎖。安全失效!


3.基於zookeeper實現

讓我們來回顧一下Zookeeper節點的概念:


Zookeeper的資料儲存結構就像一棵樹,這棵樹由節點組成,這種節點叫做Znode。

Znode分為四種類型:

1.持久節點 (PERSISTENT)

預設的節點型別。建立節點的客戶端與zookeeper斷開連線後,該節點依舊存在 。

2.持久節點順序節點(PERSISTENT_SEQUENTIAL)

所謂順序節點,就是在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號:


3.臨時節點(EPHEMERAL)

和持久節點相反,當建立節點的客戶端與zookeeper斷開連線後,臨時節點會被刪除:




4.臨時順序節點(EPHEMERAL_SEQUENTIAL)

顧名思義,臨時順序節點結合和臨時節點和順序節點的特點:在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號;當建立節點的客戶端與zookeeper斷開連線後,臨時節點會被刪除。

Zookeeper分散式鎖的原理

Zookeeper分散式鎖恰恰應用了臨時順序節點。具體如何實現呢?讓我們來看一看詳細步驟:

獲取鎖

首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個客戶端想要獲得鎖時,需要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。


之後,Client1查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock1是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。


這時候,如果再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。


Client2查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock2是不是順序最靠前的一個,結果發現節點Lock2並不是最小的。

於是,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味著Client2搶鎖失敗,進入了等待狀態。


這時候,如果又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3。


Client3查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock3是不是順序最靠前的一個,結果同樣發現節點Lock3並不是最小的。

於是,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味著Client3同樣搶鎖失敗,進入了等待狀態。


這樣一來,Client1得到了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這恰恰形成了一個等待佇列,很像是Java當中ReentrantLock所依賴的

釋放鎖

釋放鎖分為兩種情況:

1.任務完成,客戶端顯示釋放

當任務完成時,Client1會顯示呼叫刪除節點Lock1的指令。


2.任務執行過程中,客戶端崩潰

獲得鎖的Client1在任務執行過程中,如果Duang的一聲崩潰,則會斷開與Zookeeper服務端的連結。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。


由於Client2一直監聽著Lock1的存在狀態,當Lock1節點被刪除,Client2會立刻收到通知。這時候Client2會再次查詢ParentLock下面的所有節點,確認自己建立的節點Lock2是不是目前最小的節點。如果是最小,則Client2順理成章獲得了鎖。


同理,如果Client2也因為任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。


最終,Client3成功得到了鎖。

方案:

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

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

https://github.com/apache/curator/


缺點:

效能上可能並沒有快取服務那麼高。因為每次在建立鎖和釋放鎖的過程中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能通過Leader伺服器來執行,然後將資料同不到所有的Follower機器上。

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

4.總結
下面的表格總結了Zookeeper和Redis分散式鎖的優缺點:

三種方案的比較

上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在複雜性、可靠性、效能等方面無法同時滿足,所以,根據不同的應用場景選擇最適合自己的才是王道。

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

資料庫 > 快取 > Zookeeper

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

Zookeeper >= 快取 > 資料庫

從效能角度(從高到低)

快取 > Zookeeper >= 資料庫

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

Zookeeper > 快取 > 資料庫


Refrence:
https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
https://www.toutiao.com/a6558681932786303501/?tt_from=weixin&utm_campaign=client_share×tamp=1528800534&app=news_article&utm_source=weixin&iid=34667892860&utm_medium=toutiao_ios&wxshare_count=1
————————————————
版權宣告:本文為CSDN博主「夏目 "」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/wuzhiwei549/article/details/80692278