聊聊 Redis 分佈式鎖的正確實現

最近在參加學校安排的實訓任務,咱們小組需完成一套分佈式&微服務跨境電商,雖然這題目看起來有點老套,而且隊友可能是 Java 技術棧,因此我光榮(被迫)
的成爲了一名前端,並順路使用 PHP 的 Swoole 幫助負責服務器端的同窗編寫了幾個微服務模塊。在小組成員之間的協做中,仍是出現了很多有趣的火花。前端

在昨天 review 隊友代碼的過程當中,發現了咱們組分佈式鎖的寫法彷佛有點問題,實現代碼以下:redis

加鎖部分
image.png安全

解鎖部分
image-1.png服務器

主要原理是使用了 redis 的 setnx 去插入一組 key-value,其中 key 要上鎖的標識(在項目中是鎖死用戶 userId),若是上鎖失敗則返回 false。可是根據二段鎖的思路,仔細思考會存在這麼一個有趣的現象:網絡

假設微服務 A 的某個請求對 userId = 7 的用戶上鎖,則微服務 A 的這個請求能夠讀取這個用戶的信息,且能夠修改其內容 ;其餘模塊只能讀取這個用戶的信息,沒法修改其內容。
假設微服務 A 的當前請求對 userId = 7 的用戶解鎖,則全部模塊能夠讀取這個用戶的信息,且能夠修改其內容
如此一來:架構

  • 若微服務模塊 A 接收到另外一個須要修改 userId = 7 的用戶 的請求時,假設這個用戶還在被鎖狀態下,此次請求能夠修改它嗎?(能夠,解個鎖就行)
  • 若微服務模塊 B 接收到另外一個須要修改 userId = 7 的用戶 的請求時,假設這個用戶還在被鎖狀態下,此次請求能夠修改它嗎?(能夠,解個鎖就行)
  • 若微服務模塊 A 執行上鎖的請求中途意外崩掉,其餘用戶還能修改信息嗎? (能夠,解個鎖就行)

很明顯,這三點並非咱們所但願的。那麼如何實現分佈式鎖纔是最佳實踐吶?異步

一個好的分佈式鎖須要實現什麼

  • 由某個模塊的某次請求上鎖,而且只有由這個模塊的此次請求解鎖(互斥,只能有一個微服務的某次請求持有鎖)
  • 若上鎖模塊的上鎖請求超時執行,則應自動解鎖,並還原其所作修改(容錯,就算 一個持有鎖的微服務宕機也不影響最終其餘模塊的上鎖 )

咱們應該怎麼作
綜上所述,咱們小組的分佈式鎖在實現模塊互斥的狀況下,忽略的一個重要問題即是「請求互斥」。咱們只須要在加鎖時,key-value 的值保存爲當前請求的 requestId ,解鎖時加多一次判斷,是否爲同一請求便可。socket

那麼這麼修改以後,咱們能夠高枕無憂了嗎?分佈式

是的,夠用了。由於咱們開發環境 Redis 是統一用一臺服務器上的單例,採用上述方式實現的分佈式鎖並無什麼問題,但在準備部署到生產環境下時,忽然意識到一個問題:若是實現主從讀寫分離,redis 多機主從同步數據時,採用的是異步複製,也即是一個「寫」操做到咱們的 reids 主庫以後,便立刻返回成功(並不會等到同步到從庫後再返回,若是這種是同步完成後再返回即是同步複製),這將會形成一個問題:微服務

假設咱們的模塊 A中 id=1 的請求上鎖成功後,沒同步到從庫前主庫被咱們玩壞了(宕機),則 redis 哨兵將會從從庫中選擇出一臺新的主庫,此時若模塊 A 中 id=2 的請求從新請求加鎖,將會是成功的。

技不如人,咱們只能藉助搜索引擎划水了(大霧),發現這種狀況還真的有通用的解決方案:redlock。

怎麼實現 Redlock 分佈式安全鎖

首先 redlock 是 redis 官方文檔推薦的實現方式,自己並無用到主從層面的架構,採用的是多態主庫,依次去取鎖的方式。假設這裏有 5 臺主庫,總體流程大體以下:

加鎖

  1. 應用層請求加鎖
  2. 依次向 5 臺 redis 服務器發送請求
  3. 如有超過半數的服務器返回加鎖成功,則完成加鎖,若是沒有則自動執行解鎖,並等待一段隨機時間後重試。(客觀緣由加鎖失敗:網絡狀況很差、服務器未響應等問題, 等待一段隨機時間後重試能夠避開「蜂擁而進」的狀況形成服務器資源佔用瞬時猛增 )
  4. 若有其中任意一臺服務器已經持有該鎖,則加鎖失敗, 等待一段隨機時間後重試。 (主觀緣由加鎖失敗:已經被被別人鎖上了)

解鎖

直接向 5 臺服務器發起請求便可,不管這臺服務器上是否是已經有鎖。
總體思路很簡單,可是實現起來仍有許多值得注意的地方。在向這 5 臺服務器發送加鎖請求時,因爲會帶上一個過時時間以保證上文所提到的「自動解鎖(容錯性) 」,考慮到延時等緣由,這 5 臺機自動解鎖的時間不徹底相同,所以存在一個加鎖時間差的問題,通常而言是這麼解決的:

  1. 在加鎖以前,必須在應用層(或者把分佈式鎖單獨封裝成一個全局通用的微服務亦可)2. 記錄請求加鎖的時間戳 T1
  2. 完成最後一臺 redis 主庫加鎖後,記錄時間戳 T2
  3. 則加鎖所需時間爲 T1 – T2
  4. 假設資源自動解鎖的時間爲 10 秒後,則資源真正可利用的時間爲 10 – T1 + T2。若

可利用時間不符合預期,或者爲負數,你懂的,從新來一遍吧。
若是你對鎖的過時時間有着更加嚴格的把控,能夠把 T1 到第一臺服務器加鎖成功的時間單獨記錄,再在最後的可用時間上加上這段時間便可獲得一個更加準確的值
如今考慮另外一個問題,若是剛好某次請求的鎖保存在了三臺服務器上,其中這三臺都宕機了(怎麼這麼倒黴.. TAT),那此時另外一個請求又來請求加鎖,豈不又回到最初咱們小組所面臨的問題了?很遺憾的說,是的,在這種問題上官方文檔給出的答案是:啓用AOF持久化功能狀況會獲得好轉 🙂

關於性能方面的處理, 通常而言不止要求低延時,同時要求高吞吐量,咱們能夠按照官方文檔的說法, 採用多路傳輸同時對 5 臺 redis 主庫進行通訊以下降總體耗時,或者把 socket 設置成非阻塞模式 (這樣的好處是發送命令時並不等待返回,所以能夠一次性發送所有命令再進行等待總體運行結果,雖然本人認爲一般狀況下若是自己網絡延遲極低的狀況下做用不大,等待服務器處理的時間佔比會更加大)

如有任何疑問,能夠移步個人博客:http://www.zzfly.net/redis-re... 留言討論

相關文章
相關標籤/搜索