Zookeeper和Redis實現的分佈式鎖的區別,應用場景(字節一面,阿里中間件面試題)

前言

    將書籍與網上資料相結合加以摘抄並總結。

爲什麼需要分佈式鎖?

    爲了控制分佈式系統中的不同主機之間對共享資源的訪問,需要通過一些互斥手段來防止彼此之間的干擾,以保證數據一致性。

     在平時的實際項目開發中,常用基於數據庫的分佈式鎖。然而絕大多數大型分佈式系統的性能瓶預都集中在數據庫操作上。因此,如果上層業務再給數據庫添加一些額外的鎖,例如行鎖、表鎖甚至是繁重的事務處理,那麼是不是會讓數據庫更加不堪重負呢?下面我們來看看使用Reids、ZooKeeper如何實現分佈式鎖(如果面試官問到數據庫的分佈式鎖的實現,可以藉機引到這方面上來)。

分佈式鎖的不同實現

基於Zookeeper的分佈式鎖的實現

    這裏主要講解排他鎖和共享鎖兩類分佈式鎖。

    排他鎖(Exclusive Locks,簡稱X鎖),又稱爲寫鎖或獨佔鎖,是一種基本的鎖類型。如果事務T1對數據對象O1加上了排他鎖,那麼在整個加鎖期間,只允許事務T1對O1進行讀取和更新操作,其他任何事務都不能再對這個數據對象進行任何類型的操作,直到T1釋放了排他鎖。

    共享鎖(Shared Locks,簡稱S鎖),又稱爲讀鎖,同樣是一種基本的鎖類型。如果事務T1對數據對象O1加上了共享鎖,那麼當前事務只能對O1進行讀取操作,其他事務也只 能對這個數據對象加共享鎖一直到該數據對象上的所有共享鎖都被釋放。

    共享鎖和排他鎖最根本的區別在於,加上排他鎖後,數據對象只對一個事務可見,而加上共享鎖後,數據對所有事務都可見。下面我們就來看看如何藉助ZooKeeper來實現排他鎖和共享鎖。


排他鎖獲取鎖:

    每個客戶端嘗試在/exclusive_lock節點下創建臨時節點,最終只有一個客戶端創建成功,那麼就可以認爲該客戶端獲取了鎖。此外,其它沒有獲取到鎖(沒有創建節點成功)的客戶端,在exclusive_lock節點上註冊Watcher,監聽子節點的變更(即監聽鎖釋放)。監聽到子節點變更後,客戶端又重複嘗試在/exclusive_lock節點下創建臨時節點,如此循環往復。

共享鎖獲取鎖:

  1. 每個客戶端創建帶有請求序號的臨時節點,名稱例如:主機名-讀/寫請求-序號。
  2. 客戶端嘗試獲取共享鎖,如果沒有獲取成功那麼設置Watcher,監聽比自己的節點的序號小一號的節點。注意:“這裏小一號的節點”只是一個籠統的說法,具體對於寫請求和讀請求不一樣。
    讀請求:向比自己序號小一號的請求節點註冊Watcher監聽。
    寫請求:向比自己序號小一號的讀/寫請求節點註冊Watcher監聽。
    讀請求和寫請求之所以監聽節點不一樣,是因爲各個事務可以對同一數據資源進行讀請求,也就是說多個事務可以同時獲取共享鎖,而寫請求只能有一個事務進行,即此時只有一個事務能獲取共享鎖。
  3. 等待Watcher通知,繼續進入步驟2。

排他鎖和共享鎖的釋放鎖原理一樣,釋放鎖有以下兩種情況:

  • 持有當前鎖的客戶端宕機,服務器端檢測到客戶端的會話失效,那麼與該鎖對應的臨時節點就會被刪除。
  • 客戶端自己正常執行完邏輯,自己釋放鎖,刪除臨時節點。

基於Redis的分佈式鎖的實現

帶有超時限制特性的鎖:

    第一種實現,代碼實現上可用setnx()獲取鎖,然後用expire()設置超時時間,但是如果在執行這兩個方法中間的時候,客戶端突然宕機了(下邊的第二種實現提供了其它解決方法),那麼這時候想要鎖依然能釋放,客戶端會在嘗試獲取鎖失敗之後(客戶端會在一定時間內循環獲取鎖),檢查鎖的超時時間,併爲未設置超時時間的鎖設置超時時間。因此,鎖總會帶有超時時間。書中代碼截圖如下:
在這裏插入圖片描述
    對應的命令操作涉及到SETNX命令(獲取鎖)和SETEX命令(設置過期時間)。
    第二種實現,從Redis2.6.12開始可以直接使用SET命令新添加的可選選項,用戶可以獲得相當於SETNX命令和SETEX命令的效果以此來獲取鎖並設置超時時間,解決了上述第一種實現提到的客戶端突然宕機的問題。例如:

SET  value [EX seconds] [PX milliseconds] [NX|XX]
redis 127.0.0.1:6379> SET key-with-expire-and-NX "hello" EX 10086 NX
OK

其中key-with-expire-and-NX是鍵的名稱,"hello"是鍵的值,EX 10086表示過期時間爲10086,單位爲seconds。SET key value EX second 效果等同於 SETEX key second value 。NX表示只在鍵不存在時,纔對鍵進行設置操作。SET key value NX 效果等同於 SETNX key value 。
    對應的代碼實現則直接使用set()即可,Java代碼可以參考:Redis分佈式鎖的兩種實現方式的代碼
    釋放鎖,刪除該鎖對應的鍵即可。在高併發的情況下,第一個事務執行過久,導致鍵已經過期了,這時第二個事務獲取了鎖,第一個事務執行完了業務邏輯,那麼第一個事務就會釋放掉第二個事務的鎖,這樣就不對了,所以在釋放鎖之前需要一個隨機值來識別是否是自己的鎖。Java代碼同樣可以參考:Redis分佈式鎖的兩種實現方式的代碼。另外,如何解決上述剛剛所說的因爲過期時間過短,導致事務的業務邏輯沒有執行完就釋放了鎖呢?可以參考:史上關於分佈式鎖最全問題彙總,很好的文章

主從結構下的分佈式鎖

    有待繼續補充

集羣結構下的分佈式鎖

    有待繼續補充

應用場景

場景一:

  1. 假如有一個系統,能夠提供報表查看功能—>於是我們開發了一個單節點應用。
  2. 該系統訪量非常大,系統難以支撐—>我們部署了多個節點(增加至5個節點) ,分擔訪問請求,於是成了分佈式應用。
  3. 老闆要求增加一個每天晚上向指定郵件發送該日統計報表的功能---->應用中增加個定時線程, 每天晚上半夜跑報表,發郵件。

    這個時候,出問題了,老闆每天晚上會收到5封郵件!因爲同樣的程序我們部署了5個節點!怎麼解決呢?

    增加分佈式鎖。這樣,5個節點中,只有獲取到鎖的節點纔會發出郵件。

場景二:

    在凌晨3點對昨天購買了xx商品的用戶分發優惠券,但是線上有n臺機器,那麼會執行n次分發任務,爲了只執行1次,可以增加分佈式鎖,搶到鎖的機器就去執行。

場景三:秒殺系統的庫存管理,避免超賣。

總結

  • Zookeeper的分佈式鎖是客戶端基於創建臨時節點實現的,對於排他鎖,每個客戶端都嘗試創建臨時節點,但是隻有一個客戶端能成功創建,創建成功則相當於獲取了鎖。對於共享鎖,則會按照一定的順序隊列創建帶序號的臨時節點並嘗試獲取鎖(可以有多個客戶端獲取共享鎖)。Redis的分佈式鎖則是通過創建一個從未創建過的key並設置其過期時間實現的,創建成功則獲得了鎖,並且客戶端會在一定時間內循環獲取鎖,比較消耗服務器性能。
  • Zookeeper釋放鎖時,要麼正常執行完業務邏輯後,事務主動釋放,要麼是檢測到與客戶端的會話失效後釋放。Redis釋放鎖時,要麼正常執行完業務邏輯後,事務主動釋放,要麼是鍵超時後釋放鎖。
  • 對於Redis的主從結構中出現的主服務器宕機情況(單點故障),客戶端A已經獲取到鎖了,但是主服務器還沒來得及將鍵複製到從服務器,並且從服務器晉升爲了主服務器,這時客戶端B也可以獲取鎖,鎖互斥效果就失效了。可以使用RedLock解決,但是不建議,可以使用Zookeeper。
  • Redis性能更高。
        通過學習,我對分佈式鎖初步進行了瞭解,後面繼續深入學習,再加以完善。

參考文獻

《從Paxos到Zookeeper 分佈式一致性原理與實踐》的6.1.7節 分佈式鎖
《Redis實戰》的6.2.5節 帶有超時限制特性的鎖
Redis與Zookeeper實現分佈式鎖的區別,包含Java代碼
Redis的SET命令中文翻譯文檔
Redis分佈式鎖的兩種實現方式的代碼
Zookeeper場景一
Zookeeper場景二
史上關於分佈式鎖最全問題彙總,很好的文章
如何使用分佈式鎖?
2019 最新主從架構師Redis分佈式鎖 | 高併發場景庫存重複扣減問題分析 視頻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章