Redis分散式鎖的正確實現
開篇
在負責的專案新實現的一個模組中,要用到分散式鎖,實現方案是Redis,結果發現網上大部門的博文都過於老舊或總有考慮不周的地方,這裡就和大家分享一個生產可用的Redis分散式鎖是什麼樣的,又有那些考慮和問題。
分散式鎖
使用環境
分散式鎖的概念網上都是,這裡就不再贅述。現在較廣泛的實現方案有三種:
- 資料庫實現
- zookeeper實現
- Redis實現
在負責的模組中選用Redis實現方案的理由是因為,Redis實現的分散式鎖速度較快,實現簡單,並且使用的場景中不涉及資料的增刪改且不是核心業務,能夠接受分散式鎖被超時釋放和Redis資料髒讀現象。
最low實現
網上許多提供的程式碼都是使用Redis的setnx來實現的
> setnx redisLock true OK ...業務邏輯執行... > del redisLock
上述方案最大的問題是在業務邏輯執行時可能出現不可預料的異常,如機器故障,工程丟擲異常,網路波動等等。一旦出現問題,建立的鎖就無法及時釋放,間接導致死鎖,整個業務阻塞癱瘓甚至發生雪崩現象。
最常見實現
下面這種事最low方案的優化版為鎖添加了超時時間
> setnx redisLock true OK > expire redisLock 5 ... 業務邏輯執行 ... > del redisLock
可以看到上述方案在建立鎖後,為鎖添加了超時時間可以避免最low方案中死鎖的問題,但是真的是這樣麼?仔細想想可以發現這種方案可以大大降低死鎖的問題,但還是無法完全避免,因為expire 指令和 senx指令並不是原子操作,兩個指令並不是一次執行的。如果在執行setnx和expire中間專案出現最low方案中遇到的問題機器故障等等,還是會導致expire沒有執行,也會造成死鎖。
可喜可賀的是Redis中提供了setnx 和 expire 組合在一起的原子指令
> set redisLock true ex 10 nx OK ... 業務邏輯執行 ... > del redisLock
這個指令實現了setnx 和 expire的一次執行,解決了可能導致死鎖的最後一根稻草,但是這樣的實現就可以安全無憂了麼?
最終實現
想到了最常見方案中會出現什麼問題了麼?這個問題就是超時問題,也是Redis分散式鎖相較於zookeeper分散式鎖先天劣勢的一點,在zookeeper中一但伺服器程序down掉或者心跳超時,zk中的臨時序列會自動釋放。但是Redis中沒有這樣的機制,導致只能用超時機制來彌補,但是帶來的問題就是鎖的不安全性。
如果業務在加鎖和釋放鎖之間的邏輯執行的太長,超出了鎖的超時時間,鎖就會自動超時釋放,但是這時業務還沒有執行完,其它業務會因為鎖的釋放而獲取新的鎖進入業務執行,導致同時有兩個業務在持有鎖,出現數據混亂。甚至在第一個業務執行結束後,釋放了後進入業務的分散式鎖,打亂了整個鎖的持有和釋放。所以建議Redis分散式鎖不要用於較長時間的任務。
較為安全方案是為set指令的value引數設定一個隨機數,刪鎖時先確認隨機數是否一致,然後再刪除key。確認value和刪除key不是一個原子操作,這就需要使用Lua指令碼了,因為Lua指令碼可以實現多個連續指令的原子性執行。
//建立鎖 String random = Math.random() + ""; jedis.set("redisLock", random, "NX", "EX", 5); //刪除鎖 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(script,Collections.singletonList(lockKey), Collections.singletonList(requestId));