Redis + Lua實現分佈式鎖(SpringBoot版)

設計思路

既然是實現分佈式鎖,那確定得保證多個鏈接集中請求一個資源的排他性,而redis的單線程特性則很好的知足了這一需求。redis提供的set方法則是知足這一需求的關鍵,下圖是實現redis分佈式鎖的簡單流程,先有個初步的料及。
redisredis

場景分析

下面是set命令的相關用法:分佈式

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

可選參數:lua

  • EX second :設置鍵的過時時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。
  • PX millisecond :設置鍵的過時時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value 。
  • NX :只在鍵不存在時,纔對鍵進行設置操做。 SET key value NX 效果等同於 SETNX key value 。
  • XX :只在鍵已經存在時,纔對鍵進行設置操做。

看似簡單的實現,實則有不少隱藏的坑,下面我將以幾個案例做爲分析.spa

Case1 :

熟悉redis命令的同窗可能注意到 setnx這個命令,而後再配合 expire命令是否能夠實現同樣的效果了?
SETEX user_id 10086
// 此處服務掛掉了,那麼user_id將永生
expire user_id

注意,這是2條命令也就意味着這是非原子性操做,當執行到第一條命令SETEX user_id 10086,再準備給這個key設置失效時間時服務忽然掛掉了,那麼這個key將會永生,其餘請求將永遠沒法獲取到這把鎖。
因此,咱們須要用SET保證其原子性. SET user_id 10086 EX 30 NX,即設置key爲user_id,value爲10086,而且設置失效時間爲30s,若是該key存在則放棄更改。線程

Case2 :

在咱們設置key值的時候通常會盡可能保證他的惟一性,好比訂單ID,庫存ID等。而根據 SET命令的返回結果來判斷是否有其餘請求強佔,貌似value值的設置無關緊要,事實真的如此嗎?

僞代碼:設計

SET user_id 10086 EX 30 NX
// 處理業務中

//業務處理完畢
DEL user_id

流程圖分析:code

錯刪

經過上圖咱們將請求的過程肢解,即分爲如下幾步:ip

  1. 三個請求A,B,C同時競爭鎖,被請求A搶先得到,其餘請求只能不斷嘗試獲取鎖(tryLock)
  2. 請求A因爲業務比較複雜處理時間已經超時,因此請求B可以獲取到鎖
  3. 請求A終於完成了本身的業務,這個時候執行了DEL user_id,可是他本身的鎖已經失效了,刪除的是請求B鎖。而請求B的業務此時並未處理完,因此此處就出現了問題!

改進:
爲了不誤刪除別人的鎖,因此咱們須要在刪除鎖的時候須要判斷一下這個鎖是不是本身的。這個時候咱們設置的value就生效了,能夠經過value來判斷這把鎖是否屬於本身。 這個value值設置比較隨意,只要能作區分就能夠了。資源

僞代碼:get

SET user_id 10086 EX 30 NX
// 處理業務中

//業務處理完畢
if( (GET user_id) == "XXX" ){
  DEL user_id
}

Case3 :

好吧,終於解決了這一系列坑本覺得就要完工 。正在得意回味本身改進的代碼時總以爲哪裏有點怪怪的,猛地發現這個 GET取值判斷和DEL刪除並不是原子操做。那麼接着上面的分析,會出現什麼問題呢?
if( (GET user_id) == "XXX" ){ //獲取到本身鎖後,進行取值判斷且判斷爲真。此時,這把鎖剛好失效。
  DEL user_id
}

當程序判經過該鎖的值判斷髮現這把鎖是本身加上的,準備DEL。此時該鎖剛好失效,而另一個請求剛好得到key值爲user_id的鎖。
此時程序執行了了DEL user_id,刪除了別人加的鎖,尷尬!
因此這段代碼並不完美,爲了保證查詢和刪除的原子性操做,須要引入lua腳本支持。

改進:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

補充

回到Case2中,請求A中的業務處理時長超過了鎖的失效時間。對於此類問題,看到不少網上大佬給出的答案是起一個守護線程進行監聽key的失效時間,而後在快要失效的時候爲期續命。

其實redis對於分佈式鎖,Redisson有着更好的實現方式。

代碼(稍後上傳....)

相關文章
相關標籤/搜索