天天看點

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

為什麼寫這篇文章?

目前網上大部分的基于zookeeper,和redis的分布式鎖的文章都不夠全面。要麼就是特意避開叢集的情況,要麼就是考慮不全,讀者看着還是一臉迷茫。坦白說,這種老題材,很難寫出新創意,部落客内心戰戰兢兢,如履薄冰,文中有什麼不嚴謹之處,歡迎批評。

部落客的這篇文章,不上代碼,隻講分析。

(1)在redis方面,有開源redisson的jar包供你使用。

(2)在zookeeper方面,有開源的curator的jar包供你使用

因為已經有開源jar包供你使用,沒有必要再去自己封裝一個,大家出門百度一個api即可,不需要再羅列一堆實作代碼。

需要說明的是,Google有一個名為Chubby的粗粒度分布鎖的服務,然而,Google Chubby并不是開源的,我們隻能通過其論文和其他相關的文檔中了解具體的細節。值得慶幸的是,Yahoo!借鑒Chubby的設計思想開發了zookeeper,并将其開源,是以本文不讨論Chubby。至于Tair,是阿裡開源的一個分布式K-V存儲方案。我們在工作中基本上redis使用的比較多,讨論Tair所實作的分布式鎖,不具有代表性。

是以,主要分析的還是redis和zookeeper所實作的分布式鎖。

文章結構

本文借鑒了兩篇國外大神的文章,redis的作者antirez的《Is Redlock safe?》以及分布式系統專家Martin的《How to do distributed locking》,再加上自己微薄的見解進而形成這篇文章,文章的目錄結構如下:

(1)為什麼使用分布式鎖

(2)單機情形比較

(3)叢集情形比較

(4)鎖的其他特性比較

正文

先上結論:

zookeeper可靠性比redis強太多,隻是效率低了點,如果并發量不是特别大,追求可靠性,首選zookeeper。為了效率,則首選redis實作。

為什麼使用分布式鎖?

使用分布式鎖的目的,無外乎就是保證同一時間隻有一個用戶端可以對共享資源進行操作。

但是Martin指出,根據鎖的用途還可以細分為以下兩類

(1)允許多個用戶端操作共享資源

這種情況下,對共享資源的操作一定是幂等性操作,無論你操作多少次都不會出現不同結果。在這裡使用鎖,無外乎就是為了避免重複操作共享資源進而提高效率。

(2)隻允許一個用戶端操作共享資源

這種情況下,對共享資源的操作一般是非幂等性操作。在這種情況下,如果出現多個用戶端操作共享資源,就可能意味着資料不一緻,資料丢失。

第一回合,單機情形比較

(1)Redis

先說加鎖,根據redis官網文檔的描述,使用下面的指令加鎖

SET resource_name my_random_value NX PX 30000
           
  • my_random_value是由用戶端生成的一個随機字元串,相當于是用戶端持有鎖的标志
  • NX表示隻有當resource_name對應的key值不存在的時候才能SET成功,相當于隻有第一個請求的用戶端才能獲得鎖
  • PX 30000表示這個鎖有一個30秒的自動過期時間。

至于解鎖,為了防止用戶端1獲得的鎖,被用戶端2給釋放,采用下面的Lua腳本來釋放鎖

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
           

在執行這段LUA腳本的時候,KEYS[1]的值為resource_name,ARGV[1]的值為my_random_value。原理就是先擷取鎖對應的value值,保證和用戶端穿進去的my_random_value值相等,這樣就能避免自己的鎖被其他人釋放。另外,采取Lua腳本操作保證了原子性.如果不是原子性操作,則有了下述情況出現

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

分析:這套redis加解鎖機制看起來很完美,然而有一個無法避免的硬傷,就是過期時間如何設定。如果用戶端在操作共享資源的過程中,因為長期阻塞的原因,導緻鎖過期,那麼接下來通路共享資源就不安全。

可是,有的人會說

那可以在用戶端操作完共享資源後,判斷鎖是否依然歸該用戶端所有,如果依然歸用戶端所有,則送出資源,釋放鎖。若不歸用戶端所有,則不送出資源啊.

OK,這麼做,隻能降低多個用戶端操作共享資源發生的機率,并不能解決問題。

為了友善讀者了解,部落客舉一個業務場景。

業務場景:我們有一個内容修改頁面,為了避免出現多個用戶端修改同一個頁面的請求,采用分布式鎖。隻有獲得鎖的用戶端,才能修改頁面。那麼正常修改一次頁面的流程如下圖所示

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

注意看,上面的步驟(3)-->步驟(4.1)并不是原子性操作。也就說,你可能出現在步驟(3)的時候傳回的是有效這個标志位,但是在傳輸過程中,因為延時等原因,在步驟(4.1)的時候,鎖已經逾時失效了。那麼,這個時候鎖就會被另一個用戶端鎖獲得。就出現了兩個用戶端共同操作共享資源的情況。

大家可以思考一下,無論你如何采用任何補償手段,你都隻能降低多個用戶端操作共享資源的機率,而無法避免。例如,你在步驟(4.1)的時候也可能發生長時間GC停頓,然後在停頓的時候,鎖逾時失效,進而鎖也有可能被其他用戶端獲得。這些大家可以自行思考推敲。

(2)zookeeper

先簡單說下原理,根據網上文檔描述,zookeeper的分布式鎖原理是利用了臨時節點(EPHEMERAL)的特性。

  • 當znode被聲明為EPHEMERAL的後,如果建立znode的那個用戶端崩潰了,那麼相應的znode會被自動删除。這樣就避免了設定過期時間的問題。
  • 用戶端嘗試建立一個znode節點,比如/lock。那麼第一個用戶端就建立成功了,相當于拿到了鎖;而其它的用戶端會建立失敗(znode已存在),擷取鎖失敗。

分析:這種情況下,雖然避免了設定了有效時間問題,然而還是有可能出現多個用戶端操作共享資源的。

大家應該知道,zookeeper如果長時間檢測不到用戶端的心跳的時候(Session時間),就會認為Session過期了,那麼這個Session所建立的所有的ephemeral類型的znode節點都會被自動删除。

這種時候會有如下情形出現

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

如上圖所示,用戶端1發生GC停頓的時候,zookeeper檢測不到心跳,也是有可能出現多個用戶端同時操作共享資源的情形。當然,你可以說,我們可以通過JVM調優,避免GC停頓出現。但是注意了,我們所做的一切,隻能盡可能避免多個用戶端操作共享資源,無法完全消除。

第二回合,叢集情形比較

我們在生産中,一般都是用叢集情形,是以第一回合讨論的單機情形。算是給大家熱熱身。

(1)redis

為了redis的高可用,一般都會給redis的節點挂一個slave,然後采用哨兵模式進行主備切換。但由于Redis的主從複制(replication)是異步的,這可能會出現在資料同步過程中,master當機,slave來不及同步資料就被選為master,進而資料丢失。具體流程如下所示:

  • (1)用戶端1從Master擷取了鎖。
  • (2)Master當機了,存儲鎖的key還沒有來得及同步到Slave上。
  • (3)Slave更新為Master。
  • (4)用戶端2從新的Master擷取到了對應同一個資源的鎖。

為了應對這個情形, redis的作者antirez提出了RedLock算法,步驟如下(該流程出自官方文檔),假設我們有N個master節點(官方文檔裡将N設定成5,其實大等于3就行)

  • (1)擷取目前時間(機關是毫秒)。
  • (2)輪流用相同的key和随機值在N個節點上請求鎖,在這一步裡,用戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的逾時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的逾時時間可能是5-50毫秒的範圍,這個可以防止一個用戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該盡快嘗試下一個master節點。
  • (3)用戶端計算第二步中擷取鎖所花的時間,隻有當用戶端在大多數master節點上成功擷取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是擷取成功了。
  • (4)如果鎖擷取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前擷取鎖所消耗的時間。
  • (5)如果鎖擷取失敗了,不管是因為擷取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,用戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有擷取成功的鎖。

分析:RedLock算法細想一下還存在下面的問題

節點崩潰重新開機,會出現多個用戶端持有鎖

假設一共有5個Redis節點:A, B, C, D, E。設想發生了如下的事件序列:

(1)用戶端1成功鎖住了A, B, C,擷取鎖成功(但D和E沒有鎖住)。

(2)節點C崩潰重新開機了,但用戶端1在C上加的鎖沒有持久化下來,丢失了。

(3)節點C重新開機後,用戶端2鎖住了C, D, E,擷取鎖成功。

這樣,用戶端1和用戶端2同時獲得了鎖(針對同一資源)。

為了應對節點重新開機引發的鎖失效問題,redis的作者antirez提出了延遲重新開機的概念,即一個節點崩潰後,先不立即重新開機它,而是等待一段時間再重新開機,等待的時間大于鎖的有效時間。采用這種方式,這個節點在重新開機前所參與的鎖都會過期,它在重新開機後就不會對現有的鎖造成影響。這其實也是通過人為補償措施,降低不一緻發生的機率。

時間跳躍問題

(1)假設一共有5個Redis節點:A, B, C, D, E。設想發生了如下的事件序列:

(2)用戶端1從Redis節點A, B, C成功擷取了鎖(多數節點)。由于網絡問題,與D和E通信失敗。

(3)節點C上的時鐘發生了向前跳躍,導緻它上面維護的鎖快速過期。

用戶端2從Redis節點C, D, E成功擷取了同一個資源的鎖(多數節點)。

用戶端1和用戶端2現在都認為自己持有了鎖。

為了應對始終跳躍引發的鎖失效問題,redis的作者antirez提出了應該禁止人為修改系統時間,使用一個不會進行“跳躍”式調整系統時鐘的ntpd程式。這也是通過人為補償措施,降低不一緻發生的機率。

逾時導緻鎖失效問題

RedLock算法并沒有解決,操作共享資源逾時,導緻鎖失效的問題。回憶一下RedLock算法的過程,如下圖所示

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

如圖所示,我們将其分為上下兩個部分。對于上半部分框圖裡的步驟來說,無論因為什麼原因發生了延遲,RedLock算法都能處理,用戶端不會拿到一個它認為有效,實際卻失效的鎖。然而,對于下半部分框圖裡的步驟來說,如果發生了延遲導緻鎖失效,都有可能使得用戶端2拿到鎖。是以,RedLock算法并沒有解決該問題。

zookeeper在叢集部署中,zookeeper節點數量一般是奇數,且一定大等于3。我們先回憶一下,zookeeper的寫資料的原理

如圖所示,這張圖懶得畫,直接搬其他文章的了。

為什麼分布式要有分布式鎖!為什麼寫這篇文章?文章結構正文為什麼使用分布式鎖?第一回合,單機情形比較第二回合,叢集情形比較第三回合,鎖的其他特性比較總結

那麼寫資料流程步驟如下

1.在Client向Follwer發出一個寫的請求

2.Follwer把請求發送給Leader

3.Leader接收到以後開始發起投票并通知Follwer進行投票

4.Follwer把投票結果發送給Leader,隻要半數以上傳回了ACK資訊,就認為通過

5.Leader将結果彙總後如果需要寫入,則開始寫入同時把寫入操作通知給Leader,然後commit;

6.Follwer把請求結果傳回給Client

還有一點,zookeeper采取的是全局串行化操作

OK,現在開始分析

叢集同步

client給Follwer寫資料,可是Follwer卻當機了,會出現資料不一緻問題麼?不可能,這種時候,client建立節點失敗,根本擷取不到鎖。

client給Follwer寫資料,Follwer将請求轉發給Leader,Leader當機了,會出現不一緻的問題麼?不可能,這種時候,zookeeper會選取新的leader,繼續上面的提到的寫流程。

總之,采用zookeeper作為分布式鎖,你要麼就擷取不到鎖,一旦擷取到了,必定節點的資料是一緻的,不會出現redis那種異步同步導緻資料丢失的問題。

不依賴全局時間,怎麼會存在這種問題

不依賴有效時間,怎麼會存在這種問題

第三回合,鎖的其他特性比較

(1)redis的讀寫性能比zookeeper強太多,如果在高并發場景中,使用zookeeper作為分布式鎖,那麼會出現擷取鎖失敗的情況,存在性能瓶頸。

(2)zookeeper可以實作讀寫鎖,redis不行。

(3)zookeeper的watch機制,用戶端試圖建立znode的時候,發現它已經存在了,這時候建立失敗,那麼進入一種等待狀态,當znode節點被删除的時候,zookeeper通過watch機制通知它,這樣它就可以繼續完成建立操作(擷取鎖)。這可以讓分布式鎖在用戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住,直到擷取到鎖為止。這套機制,redis無法實作

總結

OK,正文啰嗦了一大堆。其實隻是想表明兩個觀點,無論是redis還是zookeeper,其實可靠性都存在一點問題。但是,zookeeper的分布式鎖的可靠性比redis強太多!但是,zookeeper讀寫性能不如redis,存在着性能瓶頸。大家在生産上使用,可自行進行評估使用。

原文釋出時間為:2018-08-01

本文作者:孤獨煙

本文來自雲栖社群合作夥伴“

Java後端技術

”,了解相關資訊可以關注“