基於數據庫,緩存,zooKeeper的分佈式鎖介紹(進來收藏吧)

分佈式鎖介紹

  在計算機系統中,鎖作為一種控制併發的機制無處不在。

  單機環境下,操作系統能夠在進程或線程之間通過本地的鎖來控制併發程序的行為。而在如今的大型複雜系統中,通常採用的是分佈式架構提供服務。

  分佈式環境下,基於本地單機的鎖無法控制分佈式系統中分開部署客戶端的併發行為,此時分佈式鎖就應運而生了。




一個可靠的分佈式鎖應該具備以下特性:

  1.互斥性:作為鎖,需要保證任何時刻只能有一個客戶端(用戶)持有鎖

  2.可重入: 同一個客戶端在獲得鎖後,可以再次進行加鎖

  3.高可用:獲取鎖和釋放鎖的效率較高,不會出現單點故障

  4.自動重試機制:當客戶端加鎖失敗時,能夠提供一種機制讓客戶端自動重試




基於數據庫,緩存,zooKeeper的分佈式鎖介紹(進來收藏吧)

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


為了保證一個方法或屬性在高併發情況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。在單機環境中,Java中提供了很多併發處理相關的API


由於分佈式系統多線程、多進程並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題



基於數據庫實現分佈式鎖;

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

(1)創建一個表:

<code>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='鎖定中的方法';/<code>
基於數據庫,緩存,zooKeeper的分佈式鎖介紹(進來收藏吧)

(2)想要執行某個方法,就使用這個方法名向表中插入數據:

<code>INSERT INTO method_lock (method_name, desc) VALUES ('Name', '測試');/<code>

因為我們對method_name做了唯一性約束,這裡如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那麼我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

(3)成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:

<code>delete from method_lock where method_name ='Name';/<code>

注意:這只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有很多其他的方法

缺陷:

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


改進:

  1. 數據庫是單點?搞兩個數據庫,數據庫之間雙向同步。一旦掛掉快速切換到備庫上;
  2. 沒有失效時間?在數據庫表中加個字段,記錄每把鎖的過期時間,再做一個定時任務,定期清理數據庫中的超時數據;
  3. 非阻塞的?搞一個while循環,直到insert成功再返回;
  4. 非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接獲得鎖。




總結:

1、因為是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;
2、不具備可重入的特性,因為同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
3、沒有鎖失效機制,因為有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;




基於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進行鎖釋放。




基於ZooKeeper的實現方式:

ZooKeeper節點的類型分為以下幾類:

1. 持久節點:節點創建後就一直存在,直到有刪除操作來主動刪除該節點

2. 臨時節點:臨時節點的生命週期和創建該節點的客戶端會話綁定,即如果客戶端會話失效(客戶端宕機或下線),這個節點自動刪除

3. 時序節點:創建節點是可以設置這個屬性,ZooKeeper會自動為給定的節點加上一個數字後綴,作為新的節點名。數字後綴的範圍是整型的最大值

4. 臨時性時序節點:

同時具備臨時節點與時序節點的特性,主要用於分佈式鎖的實現


zookeeper實現分佈式鎖的原理就是多個節點同時在一個指定的節點下面創建臨時會話順序節點,誰創建的節點序號最小,誰就獲得了鎖,並且其他節點就會監聽序號比自己小的節點,一旦序號比自己小的節點被刪除了,其他節點就會得到相應的事件,然後查看自己是否為序號最小的節點,如果是,則獲取鎖。

基於數據庫,緩存,zooKeeper的分佈式鎖介紹(進來收藏吧)

(1)創建一個目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
(3)獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。



<code>從理解的難易程度角度(從低到高): 數據庫 > 緩存 > Zookeeper

從實現的複雜性角度(從低到高): Zookeeper >= 緩存 > 數據庫

從性能角度(從高到低): 緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低): Zookeeper > 緩存 > 數據庫/<code>

記錄學習,每天進步一點點的橘子大王。

喜歡就關注我吧。


基於數據庫,緩存,zooKeeper的分佈式鎖介紹(進來收藏吧)


分享到:


相關文章: