Redis分佈式鎖的正確實現

開篇

  在負責的項目新實現的一個模塊中,要用到分佈式鎖,實現方案是Redis,結果發現網上大部門的博文都過於老舊或總有考慮不周的地方,這裏就和大家分享一個生產可用的Redis分佈式鎖是什麼樣的,又有那些考慮和問題。

分佈式鎖

使用環境

  分佈式鎖的概念網上都是,這裏就不再贅述。現在較廣泛的實現方案有三種:

  1. 數據庫實現
  2. zookeeper實現
  3. 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));

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