每日一面 - 假設 Redis 基本可靠,如何用單進程 Redis 實現分佈式悲觀鎖

麻煩你們幫我投一票哈,謝謝redis

什麼是分佈式鎖

針對共享內存模型的程序(例如JAVA程序),鎖就是一個很是經常使用的機制。 通常簡單分爲悲觀鎖和樂觀鎖。悲觀鎖就是你獲取這塊數據的鎖以後,別人就沒法訪問或操做這塊數據,直到你釋放這個鎖。樂觀鎖通常就是CAS更新。 在單進程內內存的鎖,只控制進程內數據的,就是非分佈式鎖。相反的,跨進程,須要鎖住多個進程訪問數據的鎖就是分佈式鎖。緩存

悲觀鎖通常由Redis的SETNXEX實現,Key 爲資源名,EX 設置一個合理的超時時間, Value 設置爲一個客戶端生成的在超時時間內不會重複的隨機字符串。安全

lua腳本特性說明

因爲redis是單線程的,執行lua腳本的時候,不會同時執行其餘客戶端命令,這必定程度上保證了併發安全網絡

單進程Redis分佈式悲觀鎖實現思路

悲觀鎖通常由Redis的SETNX實現,經過SETNX獲取一個鎖,釋放鎖就把這個Key刪除便可。 例如,獲取一個名爲TestLock的鎖,經過SETNX TestLock TestLockValue,TestLockValue隨意設置一個值。SETNX是SET IF NOT EXISTS,若是不存在就會設置成功。 設置成功會返回1,沒設置成功會返回0。返回1表明獲取鎖成功,0表明沒獲取成功。 釋放鎖調用DEL命令,經過DEL TestLock釋放鎖。返回1表明釋放鎖成功,0表明沒有這個鎖。併發

可是若是這麼簡單實現,在以下場景時,會有問題:dom

  1. 客戶端1獲取鎖成功。
  2. 客戶端1死掉了,沒能釋放鎖。
  3. 其餘客戶端再也獲取不了這個鎖了。

因此,考慮這個,咱們通常會給鎖設置一個超時時間。在超過這個超時時間以後,redis會自動把這個key刪除掉。這樣,在客戶端1死掉了以後,不會致使這個鎖永遠不釋放。分佈式

這樣redis命令集合就變成了: 獲取鎖:lua

> SETNX TestLock TestLockValue
> EXPIRE TestLock 8

釋放鎖:url

> DEL TestLock

因爲SETNX還有EXPIRE是兩條命令,在以下場景還可能出問題:.net

  1. 客戶端1執行SETNX TestLock TestLockValue成功。
  2. 客戶端1死掉了。沒有設置過時時間。
  3. 其餘客戶端再也獲取不了這個鎖了。

Redis 2.8 版本中做者加入了 set 指令的擴展參數,使得 setnx 和 expire 指令能夠一塊兒執行

這樣redis命令集合就變成了: 獲取鎖:

> SET TestLock TestLockValue EX 5 NX

釋放鎖:

> DEL TestLock

也能夠經過Redis事務來執行這兩條命令,可是在Redis被kill掉時,可能只有部分命令被執行,因此仍是經過上面的命令更安全簡潔。

但在以下場景又會有新的問題:

  1. 客戶端1獲取鎖成功。
  2. 客戶端1在某個操做上阻塞了很長時間(GC等等)或者很長時間的網絡延遲。
  3. 過時時間到了,鎖自動釋放了。
  4. 客戶端2獲取到了對應同一個資源的鎖。
  5. 客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。

爲了不這種狀況發生,咱們通常會將鎖的KEY的VALUE設置爲一個隨機值,這個隨機值只要在一段連續時間內不會重複就行。在釋放鎖的時候,判斷當前鎖的VALUE與本身設置的是否一致,只有一致的時候纔會釋放

這樣redis命令集合就變成了: 獲取鎖:

> SET TestLock RandomValue EX 5 NX

釋放鎖:

> GET TestLock
//程序判斷是否一致
> DEL TestLock

這個在以下場景也會有問題:

  1. 客戶端1獲取鎖成功。
  2. 客戶端1訪問共享資源。
  3. 客戶端1爲了釋放鎖,先執行’GET’操做獲取隨機字符串的值。
  4. 客戶端1判斷隨機字符串的值,與預期的值相等。
  5. 客戶端1因爲某個緣由阻塞住了很長時間。
  6. 過時時間到了,鎖自動釋放了。
  7. 客戶端2獲取到了對應同一個資源的鎖。
  8. 客戶端1從阻塞中恢復過來,執行DEL操縱,釋放掉了客戶端2持有的鎖。

因此,釋放鎖必定要放在一個事務內,經過lua腳本。也就是:

傳入Lua腳本參數 TestLock,當前程序緩存的隨機值
判斷TestLock的值與傳入值是否一致,一致則刪除
返回是否解鎖成功

這樣在Redis單進程一直正常運行的狀況下,分佈式悲觀鎖就實現了。

每日一刷,輕鬆提高技術,斬獲各類offer:

image

相關文章
相關標籤/搜索