首頁>技術>

一、分散式鎖的概念

分散式鎖如果我們從概念上來看,它分為兩個維度,一個是鎖,一個是分散式。

鎖是什麼?

舉個粗俗一點例子,我們上廁所佔坑,一個坑一個門,一個門一個鎖,我們蹲坑就要拿鑰匙去開鎖,然後方便,再解鎖,然後給另外一個人方便。話糙理不糙,在java的世界裡,我們希望完成某項業務是原子的,獨立的,就得為這塊業務上一把鎖,鎖的作用就是把這塊業務範圍的程式碼“鎖”起來,所有想要進入該塊業務的執行緒都得在門外等著,等我處理完再給你處理。正如上廁所不可能多個人同時拿到鑰匙,一起開鎖,一起蹲坑一樣。java中提供了各式各樣的鎖,有樂觀鎖/悲觀鎖、獨享鎖/共享鎖、互斥鎖/讀寫鎖、公平鎖/非公平鎖、偏向鎖/輕量級鎖/重量級鎖、可重入鎖、分段鎖、自旋鎖等等,至於這些鎖的實現邏輯以及方法在這裡我就不多言了,有興起的同學可以度娘,一抓一大把。

分散式是什麼?

說分散式就得拿單體來說了,粗糙點一說,一個jvm虛擬機器算一個單體,所有請求都由這臺虛擬機器完成。那個分散式又是什麼呢?是不是多個單體就是分散式呢?你要是硬要這麼說,也沒問題,只是這也太粗糙的說法了。。。[無所謂的表情.gif] 下面也畫了一張略為粗糙的圖給大夥解釋一下分散式框架模型。

從上圖可以看出,使用者的請求透過nginx的轉發,有可能落到不同的jvm上,也就是說一個業務如果發生併發,有可能落到不同的jvm上,而java的鎖只適用於當前的jvm,並不能跨不同的jvm來保證一項業務的原子性。也就是說java提供的各種鎖,並不能解決分散式的業務原子性的問題。那我非得要解決這個問題,怎麼辦?根據我們程式設計的習慣,不同的方法有一樣的邏輯業務塊,我們習慣性地把同樣的業務塊提取出來,做成通用的方法給那些不同的方法呼叫。那我們把一個jvm提出來做鎖不就解決了分散式鎖的問題了嗎?此時的架構就會變成這樣

那用以上的圖是不是就可以解決分散式鎖的問題了?呃,低效能,低可用地完成了。。。是這種思路沒錯,但java鎖服務又形成了一個單體服務了,無法叢集化就談不上高可用了。那麼我們該如何解決這個問題呢?其實,我們的問題只是缺一個高效能,高可用的中介軟體做鎖而已,那哪些中介軟體或產品有這些功能呢?那就聊到分散式鎖的實現了

二、分散式鎖的實現

在聊分散式鎖的實現的前提下,我們先來了解一下,一個分散式鎖要能做到哪些事情才能算得上是一把好的分散式鎖?

1、在分散式系統環境下,一個方法在同一時間只能被 一臺機器的一個執行緒執行2、高效能的獲取鎖與釋放鎖3、高可用的獲取鎖與釋放鎖4、具備可重入性5、具備鎖失效機制,防止死鎖;6、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。(可選)7、具備阻塞鎖特性,未獲得鎖的時候需要等待下次獲鎖的時機(可選)基於以上的特性,我們看看能實現這些特殊的三種分散式鎖的實現。

1、基於資料庫(Mysql)實現分散式鎖

我們知道資料庫本身的事務是具有原子性的功能,而執行單條命令如:insert,update,delete都是自動提交事務的,那這幾條命令,我們選一條來做分散式鎖的功能就可以了。如果選update與delete,前提都是需要先insert資料的。所以我們直接選insert來實現該功能。insert之前,我們先建表。表結構如下:

CREATE TABLE `method_lock` (  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',  `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',  `desc` varchar(64) 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='鎖定中的方法';

注意紅色部分的語句,我們把method_name設定為unique,說明在這表裡的method_name不能存在同樣的值。就是當遇到多個相同的值同時插入的時候,僅且只有一條能插入成功,其它都失敗。那麼成功的那一條就能成功地獲取到鎖,剩餘失敗的就取不到鎖。而當業務執行完成後,我們要釋放鎖,就delete這條資料即可。看到這裡,整個獲取鎖-執行業務-釋放鎖的過程就完成了,是不是很簡明瞭?但這也顯而易見地出現了以下的問題:

1、不可重入,同一個事務在沒有釋放鎖之前無法再次獲取到該鎖2、沒有失效時間,一旦解鎖失敗則會一直保留著這把鎖3、非阻塞的,沒有獲得鎖的現場一旦失敗不會等待,要想再次獲得必須重新出發獲取鎖的操作4、強依賴資料庫的可用性,資料庫一旦掛掉會導致業務不可用

針對上面的問題,我們也可以透過其它手段來解決的

1、不可重入。新增請求Id與count欄位,同一請求Id可在update count+1,而不是插入2、沒有失效時間。新增失效時間欄位,再透過排程器對過期資料進行處理3、非阻塞。透過while迴圈來不斷呼叫嘗試獲取鎖(類似CAS,不過這個CAS的成本有點高,效能低下)4、資料庫可用性。資料庫主從設計,保證高可用

資料庫這種方式雖然說也是一種解決方案,但如果併發量稍微大那麼一點點,就有insert來嘗試獲取鎖,要知道mysql的UNIQUE判斷也是要耗效能的,加上那麼多的mysql報錯也是不友好的,所以總體而言,該方案的效能不怎的,一般也少人選擇並使用這種方案。

2、基於快取(Redis)實現分散式鎖

我們又知道,Redis是單執行緒高效能的記憶體資料庫,而它本身就有key過期的功能,也有命令(SETNX)支援 set if not exists。因此我們用setnx命令來完成鎖的功能

127.0.0.1:6379> setnx methodName test(integer) 1127.0.0.1:6379> get methodName"test"127.0.0.1:6379> setnx methodName test2(integer) 0

我們看到setnx確實是可以拿來獲取鎖,返回1就成功獲取到鎖,返回0證明之前有其它執行緒設定過該值,不能獲取到鎖。但現在有個問題,什麼時候過期呢?那還不容易,expire methodName 3 就可以設定過期了,但這樣又回來原子性的問題上了,兩條命令沒有"事務"不能保證執行的原子性。 解決這個問題有兩種方法,一個是使用Lua指令碼,使得同時包含setnx和expire兩個指令,Lua指令可以保證這兩個操作是原子的,所以能保證兩者要麼同時成功或者同時失敗。還有一種方法就是使用另一個命令

set key value [EX seconds][PX milliseconds][NX|XX]

* EX seconds: 設定過期時間,單位為秒

* PX milliseconds: 設定過期時間,單位為毫秒

* NX: 僅當key不存在時設定值

* XX: 僅當key存在時設定值

delete key

但使用這種方法依然是不安全的,至少不是絕對安全。怎麼說?舉個例子。執行緒1獲取到鎖了,失效時間為10s,也就是說redis最多能保證這10s內沒有其它執行緒能進到該方法,萬一執行緒1執行該方法超過10s呢?這樣其它執行緒就有機可乘了,因為此時redis因時間過期而使key失效,其它執行緒就可以獲得該鎖了,那此時就有兩個執行緒在處理同一件事了。這可怎麼辦呢?我們可以找一個"看門狗"來專門看看我們執行緒執行完沒,redis的key是不是快要超時了,如果在key即將超時時,而執行緒還沒執行完成,那"看門狗"幫我們把過期時間延長。而這個"看門狗"我們可以使用Redisson的tryAcquireOnceAsync來完成,至於這個怎麼實現"看門狗"的功能我這裡就不展開敘述了,大家可以度娘一下。

其實超時業務邏輯未執行完的問題,我們還可以把redis超時設定更長點,如果執行時間超過一個合理值,是需要檢查業務程式碼是否有問題,一般事務不會過長,過長的事務一般也不會選擇這種方式處理,可選擇佇列處理

解決了執行時間過長問題,我們把聚集點放回到redis叢集上,這裡還有個問題,如果我們在哨兵模式下,執行緒1在master拿到鎖了,但剛好此時master宕機了,這裡slave透過投票選舉升級為master,但由於slave沒有之前master的鎖資訊,執行緒2來問redis叢集拿鎖資訊時,redis因沒之前鎖的資訊,會給執行緒2得到該鎖,這樣就會導致執行緒1與執行緒2會同時持有同一把鎖,都會同時某業務上進行操作。雖然這問題不常見,但總會有可能發生,遇到這樣的問題要怎樣處理?

針對這個問題,Redis官方也提出了紅鎖(Redlock)的概念,簡單地說:超過半數的redis服務請求到鎖的時候,才算真正獲取到該,如果沒有,則不算真正獲取到該鎖,並且需要把其它redis服務上的鎖釋放掉(delete)

什麼意思呢?

如果redis集群裡是透過哨兵模式建立起來的,假設有5臺伺服器,如果我們都向這5臺伺服器請求鎖,而它們都能響應給你,說你能獲取到鎖,那在這5臺伺服器上,肯定也有了該鎖的資訊,即使任何一個slave因選舉升級成master,那它也必然帶有原鎖的資訊,執行緒2再來請求,也不會重新批發同一個鎖給它。而根據哨兵模式的演算法,選舉數過半的slave會升級成master,所以執行緒1在請求這5臺伺服器時,最少要保證3臺(N/2 + 1)或以上響應獲取鎖成功才能算得上真正獲取鎖成功。否則是失敗的,失敗了就要清除之前請求成功鎖的redis服務的key了。具體的獲取鎖的步驟如下:

1、獲取當前毫秒時間戳2、從這5個例項中依次嘗試獲取鎖,使用相同的key和隨機的value。在這一步驟中,當我們在每個例項裡請求鎖時,每個客戶端都要設定一個比鎖的釋放時間要小的超時時間。比如鎖的自動釋放時間是10s,那麼超時時間可以設定為5~50毫秒。這個可以阻止客戶從剩下的已經阻塞的例項裡面不斷的嘗試獲取鎖。如果一個例項不可用,我們應當嘗試儘快去連線下一個例項。3、客戶端計算獲取鎖花費的時間,即計算當前時間和第一步得到的時間的差值。當且僅當客戶端能在大部分例項(N/2 +1,這裡是N=5所以指的是3),並且總的獲取鎖時間時小於鎖的有效時間,這個鎖才認為是成功獲取了。4、如果成功獲取到鎖了,它的實際有效時間就被認為是初始的有效時間減去第3步計算出的花費時間5、如果客戶端因為某些原因獲取鎖失敗了。(比如沒有N/2+1的例項獲取到鎖或最終有效時間是負值),那麼此時就會嘗試將所有例項上的鎖進行釋放(即使某些例項並沒有鎖)

Redisson也是有紅鎖的實現演算法RedissonRedLock,但它對效能的影響很大,如非必要,一般不採取這種手段,可能會透過修改業務的設計或採用其他技術方案來解決這種極端

3、基於Zookeeper實現分散式鎖

接下來要介紹的是最後一個實現分散式鎖的方式,ZooKeeper。ZooKeeper大多數應用於配置中心,服務註冊與發現, 分散式協調/通知等等功能上,但它還有一個我們常用也重要的功能,分散式鎖。那在分散式鎖方面,Zookeeper是如何做到的呢?我們知道,Zookeeper有資料節點的概念,資料節點有永久性的也有臨時性的,另外 ,ZooKeeper允許使用者為每個節點新增一個特殊的屬性:SEQUENTIAL。一旦節點被標記上這個屬性,那麼這個節點被建立的時候,ZooKeeper會自動在其節點名後面追加一個整型數字,這個整型數字是一個由父節點維護的自增數字。也就意味著無論是持久節點還是臨時節點,都可以設定成有序的,即持久順序節點和臨時順序節點。

另外,Zookeeper還有一個重要的概念是事件監聽器Watcher,當節點發生變化時,會通知過所有訂閱該節點的客戶端。透過這些特性,我們要實現分散式鎖就容易了。以下是實現的步驟

1、建立臨時節點mylock,新增特殊屬性:SEQUENTIAL2、執行緒A想獲取鎖就在mylock目錄下建立臨時順序節點;3、獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前執行緒順序號最小,獲得鎖;4、執行緒B獲取所有節點,判斷自己不是最小節點,設定監聽最小的節點;5、執行緒A處理完,刪除自己的節點,執行緒B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖

這裡推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分散式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

至此,獲取鎖-處理業務-釋放鎖的事情就做完了。同樣地,一個方案不能做到十全十美,同樣會有一些問題產生的。這種方案會有什麼問題呢?

羊群效應

羊群效應這個名詞估計也不用我解釋,不管在生活上,職場上,股市上都會出現這個詞,“人從眾”。但在我們Zookeeper上的羊群效應又是什麼意思呢?

在上述的方案上,不知道大家有沒有發現,未獲取到鎖的節點都要訂閱獲取到鎖節點的變化,也就是說當最小節點釋放鎖的時候,所有訂閱該節點變化的客戶端都被驚醒了,一擁而至地搶鎖,搶到鎖的執行緒繼續處理業務,搶不到鎖的繼續沉睡,等待下一次覺醒。有沒有像你拿著一塊小麵包在觀賞魚塘邊扔下去的那一刻的感覺,看著密密麻麻的魚蜂擁而至,甭想有多酸爽了。這有點像我們網路程式設計中的驚群效應了。面對這個問題,我們又要怎樣解決呢?

這個效應無非就是大家都在監聽同一個節點導致的,那我們公平一點,先來後到,進來搶鎖的都要在mylock建立一個臨時節點,除去獲取到鎖的最小節點的其它節點,只需要監聽比自己小1的節點就可以了,如第二個節點監聽第一個節點什麼時候釋放鎖,第三個節點監聽第二個節點,如此類推,釋放鎖後,自然就不再再搶鎖了,下一個節點會自動拿到鎖資訊。當然,如果你希望實現不公平鎖,就自行編寫一段非順序拿監聽節點的邏輯即可,這裡就不展開了,只提供一下方向。

三、各種實現的對比

至此以上的三種實現分散式的鎖已經敘述完了,但有些同學還是不清楚什麼時候,什麼場景用什麼方案,我列一下各方面的優缺點供大家參考一下

以下是按其它維度來看

四、總結

沒有絕對完美的實現方式,具體要選擇哪一種分散式鎖,需要結合每一種鎖的優缺點和業務特點而定,如果結合了場景還是挑不出來的話,就選Zookeeper吧。

理由?你都挑不出來了,我就直接幫你選一個,免得你有選擇困難性[笑臉.gif]

至此,上面沒出現過一行程式碼,也希望大家感興趣的話,自己實現,可以留意區附上git地址,讓大家學習學習。

參考文章:

https://segmentfault.com/a/1190000024463575

https://blog.csdn.net/xlgen157387/article/details/79036337

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python讀寫檔案