如今面試都會聊聊分佈式系統,一般面試官都會從服務框架(Spring Cloud、Dubbo),一路聊到分佈式事務、分佈式鎖、ZooKeeper 等知識。今天就來聊聊分佈式鎖這塊的知識,先具體的來看看 Redis 分佈式鎖的實現原理。node
若是在公司裏落地生產環境用分佈式鎖的時候,必定是會用開源類庫的,好比 Redis 分佈式鎖,通常就是用 Redisson 框架就行了,很是的簡便易用。感興趣能夠去 Redisson 官網看看如何在項目中引入 Redisson 的依賴,而後基於 Redis 實現分佈式鎖的加鎖與釋放鎖。面試
一段簡單的使用代碼片斷,先直觀的感覺一下:redis
是否是感受簡單的不行!此外,還支持 Redis 單實例、Redis 哨兵、Redis Cluster、redis master-slave 等各類部署架構,均可以完美實現。算法
Redisson 實現 Redis 分佈式鎖的底層原理數據庫
如今經過一張手繪圖,說說 Redisson 這個開源框架對 Redis 分佈式鎖的實現原理。性能優化
加鎖機制數據結構
看上面那張圖,如今某個客戶端要加鎖。若是該客戶端面對的是一個 Redis Cluster 集羣,他首先會根據 Hash 節點選擇一臺機器。架構
注:僅僅只是選擇一臺機器!而後發送一段 Lua 腳本到 Redis 上,那段 Lua 腳本以下所示:併發
爲啥要用 Lua 腳本呢?由於一大坨複雜的業務邏輯,能夠經過封裝在 Lua 腳本中發送給 Redis,保證這段複雜業務邏輯執行的原子性。框架
那麼,這段 Lua 腳本是什麼意思呢?這裏 KEYS[1] 表明的是你加鎖的那個 Key,好比說:RLock lock = redisson.getLock("myLock");這裏你本身設置了加鎖的那個鎖 Key 就是「myLock」。
ARGV[1] 表明的就是鎖 Key 的默認生存時間,默認 30 秒。ARGV[2] 表明的是加鎖的客戶端的 ID,相似於下面這樣:8743c9c0-0795-4907-87fd-6c719a6b4586:1。
第一段 if 判斷語句,就是用「exists myLock」命令判斷一下,若是你要加鎖的那個鎖 Key 不存在的話,你就進行加鎖。如何加鎖呢?很簡單,用下面的命令:hset myLock。
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,經過這個命令設置一個 Hash 數據結構,這行命令執行後,會出現一個相似下面的數據結構:
上述就表明「8743c9c0-0795-4907-87fd-6c719a6b4586:1」這個客戶端對「myLock」這個鎖 Key 完成了加鎖。
接着會執行「pexpire myLock 30000」命令,設置 myLock 這個鎖 Key 的生存時間是 30 秒,加鎖完成。
鎖互斥機制
這個時候,若是客戶端 2 來嘗試加鎖,執行了一樣的一段 Lua 腳本,會怎樣?
第一個 if 判斷會執行「exists myLock」,發現 myLock 這個鎖 Key 已經存在了。
第二個 if 判斷,判斷myLock 鎖 Key 的 Hash 數據結構中,是否包含客戶端 2 的 ID,可是明顯不是的,由於那裏包含的是客戶端 1 的 ID。
因此,客戶端 2 會獲取到 pttl myLock 返回的一個數字,這個數字表明瞭 myLock 這個鎖 Key 的剩餘生存時間。好比還剩 15000 毫秒的生存時間。此時客戶端 2 會進入一個 while 循環,不停的嘗試加鎖。
watch dog 自動延期機制
客戶端 1 加鎖的鎖 Key 默認生存時間才 30 秒,若是超過了 30 秒,客戶端 1 還想一直持有這把鎖,怎麼辦呢?
只要客戶端 1 加鎖成功,就會啓動一個 watch dog 看門狗,這個後臺線程,會每隔 10 秒檢查一下,若是客戶端 1 還持有鎖 Key,就會不斷的延長鎖 Key 的生存時間。
可重入加鎖機制
那若是客戶端 1 都已經持有了這把鎖了,結果可重入的加鎖會怎麼樣呢?以下代碼:
分析一下上面那段 Lua 腳本。第一個if判斷確定不成立,「exists myLock」會顯示鎖 Key 已經存在了。
第二個 if 判斷會成立,由於 myLock 的 Hash 數據結構中包含的那個 ID,就是客戶端 1 的那個 ID,也就是「8743c9c0-0795-4907-87fd-6c719a6b4586:1」。
此時就會執行可重入加鎖的邏輯,incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1,經過這個命令,對客戶端 1 的加鎖次數,累加 1。
此時 myLock 數據結構變爲下面這樣:
myLock 的 Hash 數據結構中的那個客戶端 ID,就對應着加鎖的次數。
釋放鎖機制
若是執行 lock.unlock(),就能夠釋放分佈式鎖,此時的業務邏輯也是很是簡單的。就是每次都對 myLock 數據結構中的那個加鎖次數減 1。
若是發現加鎖次數是 0 了,說明這個客戶端已經再也不持有鎖了,此時就會用:「del myLock」命令,從 Redis 裏刪除這個 Key。
而另外的客戶端 2 就能夠嘗試完成加鎖了。這就是所謂的分佈式鎖的開源 Redisson 框架的實現機制。
通常咱們在生產系統中,能夠用 Redisson 框架提供的這個類庫來基於 Redis 進行分佈式鎖的加鎖與釋放鎖。
上述 Redis 分佈式鎖的缺點
上面那種方案最大的問題,就是若是你對某個 Redis Master 實例,寫入了 myLock 這種鎖 Key 的 Value,此時會異步複製給對應的 Master Slave 實例。
可是這個過程當中一旦發生 Redis Master 宕機,主備切換,Redis Slave 變爲了 Redis Master。
會致使客戶端 2 嘗試加鎖時,在新的 Redis Master 上完成加鎖,客戶端 1 也覺得本身成功加鎖。
此時就會致使多個客戶端對一個分佈式鎖完成了加鎖。這時系統在業務語義上必定會出現問題,致使各類髒數據的產生。
因此這個就是 Redis Cluster,或者是 redis master-slave 架構的主從異步複製致使的 Redis 分佈式鎖的最大缺陷:在 Redis Master 實例宕機的時候,可能致使多個客戶端同時完成加鎖。
七張圖完全講清楚 ZooKeeper 分佈式鎖的實現原理
下面再聊一下 ZooKeeper 實現分佈式鎖的原理。同理,我是直接基於比較經常使用的 Curator 這個開源框架,聊一下這個框架對 ZooKeeper(如下簡稱 ZK)分佈式鎖的實現。
通常除了大公司是自行封裝分佈式鎖框架以外,建議你們用這些開源框架封裝好的分佈式鎖實現,這是一個比較快捷省事的方式。
ZooKeeper 分佈式鎖機制
看看多客戶端獲取及釋放 ZK 分佈式鎖的整個流程及背後的原理。首先看看下圖,若是如今有兩個客戶端一塊兒要爭搶 ZK 上的一把分佈式鎖,會是個什麼場景?
若是你們對 ZK 還不太瞭解的話,建議先自行百度一下,簡單瞭解點基本概念,好比 ZK 有哪些節點類型等等。
參見上圖。ZK 裏有一把鎖,這個鎖就是 ZK 上的一個節點。兩個客戶端都要來獲取這個鎖,具體是怎麼來獲取呢?
假設客戶端 A 搶先一步,對 ZK 發起了加分佈式鎖的請求,這個加鎖請求是用到了 ZK 中的一個特殊的概念,叫作「臨時順序節點」。
簡單來講,就是直接在"my_lock"這個鎖節點下,建立一個順序節點,這個順序節點有 ZK 內部自行維護的一個節點序號。
好比第一個客戶端來搞一個順序節點,ZK 內部會給起個名字叫作:xxx-000001。
而後第二個客戶端來搞一個順序節點,ZK 可能會起個名字叫作:xxx-000002。
注意,最後一個數字都是依次遞增的,從 1 開始逐次遞增。ZK 會維護這個順序。
因此這個時候,假如說客戶端 A 先發起請求,就會搞出來一個順序節點,你們看下圖,Curator 框架大概會弄成以下的樣子:圖片描述
客戶端 A 發起一個加鎖請求,先在要加鎖的 node 下搞一個臨時順序節點,這列長名字都是 Curator 框架本身生成出來的。
而後,那個最後一個數字是"1"。由於客戶端 A 是第一個發起請求的,因此給他搞出來的順序節點的序號是"1"。
接着客戶端 A 建立完一個順序節點。還沒完,他會查一下"my_lock"這個鎖節點下的全部子節點,而且這些子節點是按照序號排序的,這個時候他大概會拿到這麼一個集合:
接着客戶端 A 會走一個關鍵性的判斷:這個集合裏建立的順序節點,是否排在首位?
若是是的話,就能夠加鎖,由於明明我就是第一個來建立順序節點的人,因此我就是第一個嘗試加分佈式鎖的人啊!
加鎖成功!看下圖,再來直觀的感覺一下整個過程:
圖片描述
接着假如說,客戶端 A 都加完鎖了,客戶端 B 過來想要加鎖了,這個時候他會幹同樣的事兒:先是在"my_lock"這個鎖節點下建立一個臨時順序節點,此時名字會變成相似於:
下圖:
圖片描述
客戶端 B 由於是第二個來建立順序節點的,因此 ZK 內部會維護序號爲"2"。
接着客戶端 B 會走加鎖判斷邏輯,查詢"my_lock"鎖節點下的全部子節點,按序號順序排列,此時他看到的相似於:
時檢查本身建立的順序節點,是否是集合中的第一個?明顯不是啊,此時第一個是客戶端 A 建立的那個順序節點,序號爲"01"的那個。因此加鎖失敗!
加鎖失敗了之後,客戶端 B 就會經過 ZK 的 API 對他的順序節點的上一個順序節點加一個監聽器。ZK 自然就能夠實現對某個節點的監聽。
若是你們還不知道 ZK 的基本用法,能夠百度查閱,很是的簡單。客戶端 B 的順序節點是:
他的上一個順序節點,不就是下面這個嗎?
即客戶端 A 建立的那個順序節點!因此,客戶端 B 會對:
這個節點加一個監聽器,監聽這個節點是否被刪除等變化!你們看下圖:
圖片描述
接着,客戶端 A 加鎖以後,可能處理了一些代碼邏輯,而後就會釋放鎖。那麼,釋放鎖是個什麼過程呢?
其實就是把本身在 ZK 裏建立的那個順序節點,也就是:
這個節點刪除。刪除了那個節點以後,ZK 會負責通知監聽這個節點的監聽器,也就是客戶端 B 以前加的那個監聽器,說:你監聽的那個節點被刪除了,有人釋放了鎖。
圖片描述
此時客戶端 B 的監聽器感知到了上一個順序節點被刪除,也就是排在他以前的某個客戶端釋放了鎖。
此時,就會通知客戶端 B 從新嘗試去獲取鎖,也就是獲取"my_lock"節點下的子節點集合,此時爲:
集合裏此時只有客戶端 B 建立的惟一的一個順序節點了!而後呢,客戶端 B 判斷本身竟然是集合中的第一個順序節點,Bingo!能夠加鎖了!直接完成加鎖,運行後續的業務代碼便可,運行完了以後再次釋放鎖。
圖片描述
其實若是有客戶端 C、客戶端 D 等 N 個客戶端爭搶一個 ZK 分佈式鎖,原理都是相似的:
並且用臨時順序節點的另一個用意就是,若是某個客戶端建立臨時順序節點以後,不當心本身宕機了也不要緊,ZK 感知到那個客戶端宕機,會自動刪除對應的臨時順序節點,至關於自動釋放鎖,或者是自動取消本身的排隊。
最後,我們來看下用 Curator 框架進行加鎖和釋放鎖的一個過程:
其實用開源框架就是方便。這個 Curator 框架的 ZK 分佈式鎖的加鎖和釋放鎖的實現原理,就是上面咱們說的那樣子。
可是若是你要手動實現一套那個代碼的話,要考慮到各類細節,異常處理等等。因此你們若是考慮用 ZK 分佈式鎖,能夠參考下本文的思路。
每秒上千訂單場景下的分佈式鎖高併發優化實踐
接着聊一個有意思的話題:每秒上千訂單場景下,如何對分佈式鎖的併發能力進行優化?
首先,咱們一塊兒來看看這個問題的背景。前段時間有個朋友在外面面試,而後有一天找我聊說:有一個國內不錯的電商公司,面試官給他出了一個場景題:
假以下單時,用分佈式鎖來防止庫存超賣,可是是每秒上千訂單的高併發場景,如何對分佈式鎖進行高併發優化來應對這個場景?
他說他當時沒答上來,由於沒作過沒什麼思路。其實我當時聽到這個面試題內心也以爲有點意思,由於若是是我來面試候選人的話,給的範圍會更大一些。好比,讓面試的同窗聊一聊電商高併發秒殺場景下的庫存超賣解決方案,各類方案的優缺點以及實踐,進而聊到分佈式鎖這個話題。
由於庫存超賣問題是有不少種技術解決方案的,好比悲觀鎖,分佈式鎖,樂觀鎖,隊列串行化,Redis 原子操做,等等吧。可是既然那個面試官兄弟限定死了用分佈式鎖來解決庫存超賣,我估計就是想問一個點:在高併發場景下如何優化分佈式鎖的併發性能。
面試官提問的角度仍是能夠接受的,由於在實際落地生產的時候,分佈式鎖這個東西保證了數據的準確性,可是他自然併發能力有點弱。
恰好我以前在本身項目的其餘場景下,確實是作太高併發場景下的分佈式鎖優化方案,所以正好是藉着這個朋友的面試題,把分佈式鎖的高併發優化思路,給你們來聊一聊。
庫存超賣現象是怎麼產生的?
先來看看若是不用分佈式鎖,所謂的電商庫存超賣是啥意思?你們看下圖:
這個圖其實很清晰了,假設訂單系統部署在兩臺機器上,不一樣的用戶都要同時買 10 臺 iPhone,分別發了一個請求給訂單系統。
接着每一個訂單系統實例都去數據庫裏查了一下,當前 iPhone 庫存是 12 臺,大於了要買的 10 臺數量。
因而每一個訂單系統實例都發送 SQL 到數據庫裏下單,而後扣減了 10 個庫存,其中一個將庫存從 12 臺扣減爲 2 臺,另一個將庫存從 2 臺扣減爲 -8 臺。
如今庫存出現了負數!沒有 20 臺 iPhone 發給兩個用戶啊!怎麼辦?
用分佈式鎖如何解決庫存超賣問題?
咱們用分佈式鎖如何解決庫存超賣問題呢?回憶一下上次咱們說的那個分佈式鎖的實現原理:
同一個鎖 Key,同一時間只能有一個客戶端拿到鎖,其餘客戶端會陷入無限的等待來嘗試獲取那個鎖,只有獲取到鎖的客戶端才能執行下面的業務邏輯。
代碼如上圖,分析一下爲何這樣能夠避免庫存超賣?
你們能夠順着上面的那個步驟序號看一遍,立刻就明白了。
從上圖能夠看到,只有一個訂單系統實例能夠成功加分佈式鎖,而後只有他一個實例能夠查庫存、判斷庫存是否充足、下單扣減庫存,接着釋放鎖。釋放鎖以後,另一個訂單系統實例才能加鎖,接着查庫存,一下發現庫存只有 2 臺了,庫存不足,沒法購買,下單失敗。不會將庫存扣減爲 -8 的。
有沒其餘方案解決庫存超賣問題?
固然有!好比悲觀鎖,分佈式鎖,樂觀鎖,隊列串行化,異步隊列分散,Redis 原子操做,等等,不少方案,咱們對庫存超賣有本身的一整套優化機制。可是前面說過,這篇文章就聊一個分佈式鎖的併發優化,不是聊庫存超賣的解決方案,因此庫存超賣只是一個業務場景而已。
分佈式鎖的方案在高併發場景下
如今咱們來看看,分佈式鎖的方案在高併發場景下有什麼問題?分佈式鎖一旦加了以後,對同一個商品的下單請求,會致使全部客戶端都必須對同一個商品的庫存鎖 Key 進行加鎖。
好比,對 iPhone 這個商品的下單,都必對「iphone_stock」這個鎖 Key 來加鎖。這樣會致使對同一個商品的下單請求,就必須串行化,一個接一個的處理。你們再回去對照上面的圖反覆看一下,應該能想明白這個問題。
假設加鎖以後,釋放鎖以前,查庫存→建立訂單→扣減庫存,這個過程性能很高吧,算他全過程 20 毫秒,這應該不錯了。那麼 1 秒是 1000 毫秒,只能容納 50 個對這個商品的請求依次串行完成處理。如一秒鐘50 個請求,都是對 iPhone 下單的,那麼每一個請求處理 20 毫秒,逐個來,最後 1000 毫秒正好處理完 50 個請求。
你們看下圖,加深印象。
因此看到這裏,你們起碼也明白了,簡單的使用分佈式鎖來處理庫存超賣問題,存在什麼缺陷。
同一商品多用戶同時下單時,會基於分佈式鎖串行化處理,致使無法同時處理同一個商品的大量下單的請求。這種方案應對那種低併發、無秒殺場景的普通小電商系統,可能還能夠接受。
由於若是併發量很低,每秒就不到 10 個請求,沒有瞬時高併發秒殺單個商品的場景的話,其實也不多會對同一個商品在 1 秒內瞬間下 1000 個訂單,由於小電商系統沒那場景。
如何對分佈式鎖進行高併發優化?
那麼如今怎麼辦呢?面試官說,我如今就卡死,庫存超賣就是用分佈式鎖來解決,並且一秒對一個 iPhone 下上千訂單,怎麼優化?
如今按照剛纔的計算,你 1 秒鐘只能處理針對 iPhone 的 50 個訂單。其實說出來也很簡單,相信不少人看過 Java 裏的 ConcurrentHashMap 的源碼和底層原理,應該知道里面的核心思路,就是分段加鎖!
把數據分紅不少個段,每一個段是一個單獨的鎖,因此多個線程過來併發修改數據的時候,能夠併發的修改不一樣段的數據。不至於說,同一時間只能有一個線程獨佔修改 ConcurrentHashMap 中的數據。
另外,Java 8 中新增了一個 LongAdder 類,也是針對 Java 7 之前的 AtomicLong 進行的優化,解決的是 CAS 類操做在高併發場景下,使用樂觀鎖思路,會致使大量線程長時間重複循環。LongAdder 中也採用了相似的分段 CAS 操做,失敗則自動遷移到下一個分段進行 CAS 的思路。
其實分佈式鎖的優化思路也是相似的,以前咱們是在另一個業務場景下落地了這個方案到生產中,不是在庫存超賣問題裏用的。可是庫存超賣這個業務場景不錯,很容易理解,因此咱們就用這個場景來講一下。
你們看下圖:
這就是分段加鎖。假如如今 iPhone 有 1000 個庫存,徹底能夠給拆成 20 個庫存段。
要是你願意,能夠在數據庫的表裏建 20 個庫存字段,好比 stock_01,stock_02,相似這樣的,也能夠在 Redis 之類的地方放 20 個庫存 Key。
總之,就是把你的 1000 件庫存給他拆開,每一個庫存段是 50 件庫存,好比 stock_01 對應 50 件庫存,stock_02 對應 50 件庫存。
接着,每秒 1000 個請求過來了!此時能夠本身寫一個簡單的隨機算法,每一個請求都是隨機在 20 個分段庫存裏,選擇一個進行加鎖。
這樣同時能夠有最多 20 個下單請求一塊兒執行,每一個下單請求鎖了一個庫存分段,而後在業務邏輯裏面,就對數據庫或者是 Redis 中的那個分段庫存進行操做便可,包括查庫存→判斷庫存是否充足→扣減庫存。
這至關於一個 20 毫秒,能夠併發處理掉 20 個下單請求,那麼 1 秒,也就能夠依次處理掉 20 * 50 = 1000 個對 iPhone 的下單請求了。
一旦對某個數據作了分段處理以後,有一個坑你們必定要注意:就是若是某個下單請求,咔嚓加鎖,而後發現這個分段庫存裏的庫存不足了。這時你得自動釋放鎖,而後立馬換下一個分段庫存,再次嘗試加鎖後嘗試處理。這個過程必定要實現。
分佈式鎖併發優化方案有什麼不足?
最大的不足是很不方便,實現太複雜:
這個過程都是要手動寫代碼實現的,仍是有點工做量。不過咱們確實在一些業務場景裏,由於用到了分佈式鎖,而後又必需要進行鎖併發的優化,又進一步用到了分段加鎖的技術方案,效果固然是很好的了,一會兒併發性能能夠增加幾十倍。
該優化方案的後續改進:以咱們本文所說的庫存超賣場景爲例,你要是這麼玩,會把本身搞的很痛苦!再次強調,咱們這裏的庫存超賣場景,僅僅只是做爲演示場景而已。
順便在此給你們推薦一個Java方面的交流學習羣:4112676,裏面會分享一些高級面試題,還有資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,主要針對Java開發人員提高本身,突破瓶頸,相信你來學習,會有提高和收穫。在這個羣裏會有你須要的內容 朋友們請抓緊時間加入進吧