最近在參加學校安排的實訓任務,咱們小組需完成一套分佈式&微服務跨境電商,雖然這題目看起來有點老套,而且隊友可能是 Java 技術棧,因此我光榮(被迫)
的成爲了一名前端,並順路使用 PHP 的 Swoole 幫助負責服務器端的同窗編寫了幾個微服務模塊。在小組成員之間的協做中,仍是出現了很多有趣的火花。前端
在昨天 review 隊友代碼的過程當中,發現了咱們組分佈式鎖的寫法彷佛有點問題,實現代碼以下:redis
加鎖部分安全
解鎖部分服務器
主要原理是使用了 redis 的 setnx 去插入一組 key-value,其中 key 要上鎖的標識(在項目中是鎖死用戶 userId),若是上鎖失敗則返回 false。可是根據二段鎖的思路,仔細思考會存在這麼一個有趣的現象:網絡
假設微服務 A 的某個請求對 userId = 7 的用戶上鎖,則微服務 A 的這個請求能夠讀取這個用戶的信息,且能夠修改其內容 ;其餘模塊只能讀取這個用戶的信息,沒法修改其內容。
假設微服務 A 的當前請求對 userId = 7 的用戶解鎖,則全部模塊能夠讀取這個用戶的信息,且能夠修改其內容
如此一來:架構
很明顯,這三點並非咱們所但願的。那麼如何實現分佈式鎖纔是最佳實踐吶?異步
咱們應該怎麼作
綜上所述,咱們小組的分佈式鎖在實現模塊互斥的狀況下,忽略的一個重要問題即是「請求互斥」。咱們只須要在加鎖時,key-value 的值保存爲當前請求的 requestId ,解鎖時加多一次判斷,是否爲同一請求便可。socket
那麼這麼修改以後,咱們能夠高枕無憂了嗎?分佈式
是的,夠用了。由於咱們開發環境 Redis 是統一用一臺服務器上的單例,採用上述方式實現的分佈式鎖並無什麼問題,但在準備部署到生產環境下時,忽然意識到一個問題:若是實現主從讀寫分離,redis 多機主從同步數據時,採用的是異步複製,也即是一個「寫」操做到咱們的 reids 主庫以後,便立刻返回成功(並不會等到同步到從庫後再返回,若是這種是同步完成後再返回即是同步複製),這將會形成一個問題:微服務
假設咱們的模塊 A中 id=1 的請求上鎖成功後,沒同步到從庫前主庫被咱們玩壞了(宕機),則 redis 哨兵將會從從庫中選擇出一臺新的主庫,此時若模塊 A 中 id=2 的請求從新請求加鎖,將會是成功的。
技不如人,咱們只能藉助搜索引擎划水了(大霧),發現這種狀況還真的有通用的解決方案:redlock。
首先 redlock 是 redis 官方文檔推薦的實現方式,自己並無用到主從層面的架構,採用的是多態主庫,依次去取鎖的方式。假設這裏有 5 臺主庫,總體流程大體以下:
加鎖
解鎖
直接向 5 臺服務器發起請求便可,不管這臺服務器上是否是已經有鎖。
總體思路很簡單,可是實現起來仍有許多值得注意的地方。在向這 5 臺服務器發送加鎖請求時,因爲會帶上一個過時時間以保證上文所提到的「自動解鎖(容錯性) 」,考慮到延時等緣由,這 5 臺機自動解鎖的時間不徹底相同,所以存在一個加鎖時間差的問題,通常而言是這麼解決的:
可利用時間不符合預期,或者爲負數,你懂的,從新來一遍吧。
若是你對鎖的過時時間有着更加嚴格的把控,能夠把 T1 到第一臺服務器加鎖成功的時間單獨記錄,再在最後的可用時間上加上這段時間便可獲得一個更加準確的值
如今考慮另外一個問題,若是剛好某次請求的鎖保存在了三臺服務器上,其中這三臺都宕機了(怎麼這麼倒黴.. TAT),那此時另外一個請求又來請求加鎖,豈不又回到最初咱們小組所面臨的問題了?很遺憾的說,是的,在這種問題上官方文檔給出的答案是:啓用AOF持久化功能狀況會獲得好轉 🙂
關於性能方面的處理, 通常而言不止要求低延時,同時要求高吞吐量,咱們能夠按照官方文檔的說法, 採用多路傳輸同時對 5 臺 redis 主庫進行通訊以下降總體耗時,或者把 socket 設置成非阻塞模式 (這樣的好處是發送命令時並不等待返回,所以能夠一次性發送所有命令再進行等待總體運行結果,雖然本人認爲一般狀況下若是自己網絡延遲極低的狀況下做用不大,等待服務器處理的時間佔比會更加大)
如有任何疑問,能夠移步個人博客:http://www.zzfly.net/redis-re... 留言討論