淺析 Redis 分佈式鎖解決方案

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1 背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們日常在電商網站購物時經常會遇到一些高併發的場景,例如電商 App 上經常出現的秒殺活動、限量優惠券搶購,還有我們去哪兒網的火車票搶票系統等,這些場景有一個共同特點就是訪問量激增,雖然在系統設計時會通過限流、異步、排隊等方式優化,但整體的併發還是平時的數倍以上,爲了避免併發問題,防止庫存超賣,給用戶提供一個良好的購物體驗,這些系統中都會用到鎖的機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於單進程的併發場景,可以使用編程語言及相應的類庫提供的鎖,如 Java 中的 synchronized 語法以及 ReentrantLock 類等,避免併發問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/e8\/52\/e8cab0d04fyya53f65127de9fa178e52.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果在分佈式場景中,實現不同客戶端的線程對代碼和資源的同步訪問,保證在多線程下處理共享數據的安全性,就需要用到分佈式鎖技術。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/f1\/40\/f161d9e44f7eea10bdeb512c1b127640.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼何爲分佈式鎖呢?分佈式鎖是控制分佈式系統或不同系統之間共同訪問共享資源的一種鎖實現,如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往需要互斥來防止彼此干擾保證一致性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個相對安全的分佈式鎖,一般需要具備以下特徵:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互斥性。互斥是鎖的基本特徵,同一時刻鎖只能被一個線程持有,執行臨界區操作。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"超時釋放。通過超時釋放,可以避免死鎖,防止不必要的線程等待和資源浪費,類似於MySQL的InnoDB引擎中的innodblockwait_timeout參數配置。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可重入性。一個線程在持有鎖的情況可以對其再次請求加鎖,防止鎖在線程執行完臨界區操作之前釋放。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高性能和高可用。加鎖和釋放鎖的過程性能開銷要儘可能的低,同時也要保證高可用,防止分佈式鎖意外失效。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出實現分佈式鎖,並不是鎖住資源就可以了,還需要滿足一些額外的特徵,避免出現死鎖、鎖失效等問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2 分佈式鎖的實現方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前實現分佈式鎖的方式有很多,常見的主要有:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Memcached 分佈式鎖"}]}]},{"type":"listitem","attrs":{"listStyle":"none"},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情況下,才能 add 成功,也就意味着線程得到了鎖。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Zookeeper 分佈式鎖"}]}]},{"type":"listitem","attrs":{"listStyle":"none"},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"利用 Zookeeper 的順序臨時節點,來實現分佈式鎖和等待隊列。ZooKeeper 作爲一個專門爲分佈式應用提供方案的框架,它提供了一些非常好的特性,如 ephemeral 類型的 znode 自動刪除的功能,同時 ZooKeeper 還提供 watch 機制,可以讓分佈式鎖在客戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住,直到獲取到鎖爲止。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Chubby"}]}]},{"type":"listitem","attrs":{"listStyle":"none"},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Google 公司實現的粗粒度分佈式鎖服務,有點類似於 ZooKeeper,但也存在很多差異。Chubby 通過 sequencer 機制解決了請求延遲造成的鎖失效的問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Redis 分佈式鎖"}]}]},{"type":"listitem","attrs":{"listStyle":"none"},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於 Redis 單機實現的分佈式鎖,其方式和 Memcached 的實現方式類似,利用 Redis 的 SETNX 命令,此命令同樣是原子性操作,只有在 key 不存在的情況下,才能 set 成功。而基於 Redis 多機實現的分佈式鎖Redlock,是Redis 的作者 antirez 爲了規範 Redis 分佈式鎖的實現,提出的一個更安全有效的實現機制。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文主要討論分析基於Redis的分佈式鎖的幾種實現方式以及存在的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3 Redis分佈式鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Redis 作爲分佈式鎖,本質上要實現的目標就是一個進程在 Redis 裏面佔據了僅有的一個“茅坑”,當別的進程也想來佔坑時,發現已經有人蹲在那裏了,就只好放棄或者等待稍後再試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前基於 Redis 實現分佈式鎖主要有兩大類,一類是基於單機,另一類是基於 Redis 多機,不管是哪種實現方式,均需要實現加鎖、解鎖、鎖超時這三個分佈式鎖的核心要素。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 基於 Redis 單機實現的分佈式鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3.1.1 使用 SETNX 指令"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最簡單的加鎖方式就是直接使用 Redis 的 SETNX 指令,該指令只在 key 不存在的情況下,將 key 的值設置爲 value,若 key 已經存在,則 SETNX 命令不做任何動作。key 是鎖的唯一標識,可以按照業務需要鎖定的資源來命名。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如在某商城的秒殺活動中對某一商品加鎖,那麼 key 可以設置爲  lock_resource_id ,value 可以設置爲任意值,在資源使用完成後,使用 DEL 刪除該 key 對鎖進行釋放,整個過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/e6\/6c\/e64ce358de2f0bdab6e1dee4d2a5b16c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很顯然,這種獲取鎖的方式很簡單,但也存在一個問題,就是我們上面提到的分佈式鎖三個核心要素之一的鎖超時問題,即如果獲得鎖的進程在業務邏輯處理過程中出現了異常,可能會導致 DEL 指令一直無法執行,導致鎖無法釋放,該資源將會永遠被鎖住。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/33\/1e\/33d85bcdcf1c3858ff918134ae07e61e.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,在使用 SETNX 拿到鎖以後,必須給 key 設置一個過期時間,以保證即使沒有被顯式釋放,在獲取鎖達到一定時間後也要自動釋放,防止資源被長時間獨佔。由於 SETNX 不支持設置過期時間,所以需要額外的 EXPIRE 指令,整個過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/31\/6b\/3183a3c30af87a67999097795256aa6b.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣實現的分佈式鎖仍然存在一個嚴重的問題,由於 SETNX 和 EXPIRE 這兩個操作是非原子性的, 如果進程在執行 SETNX 和 EXPIRE 之間發生異常,SETNX 執行成功,但 EXPIRE 沒有執行,導致這把鎖變得“長生不老”,這種情況就可能出現前文提到的鎖超時問題,其他進程無法正常獲取鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/8c\/c3\/8c5cda6ae509c4a2d77bfcb9077a74c3.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3.1.2 使用 SET 擴展指令"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決 SETNX 和 EXPIRE 兩個操作非原子性的問題,可以使用 Redis 的 SET 指令的擴展參數,使得 SETNX 和 EXPIRE 這兩個操作可以原子執行,整個過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9d\/f7\/9dd36143ac259429f9e6ef0ebebaf9f7.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個 SET 指令中:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NX 表示只有當 lock_resource_id 對應的 key 值不存在的時候才能 SET 成功。保證了只有第一個請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"EX 10 表示這個鎖10秒鐘後會自動過期,業務可以根據實際情況設置這個時間的大小。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是這種方式仍然不能徹底解決分佈式鎖超時問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鎖被提前釋放。假如線程 A 在加鎖和釋放鎖之間的邏輯執行的時間過長(或者線程 A 執行過程中被堵塞),以至於超出了鎖的過期時間後進行了釋放,但線程 A 在臨界區的邏輯還沒有執行完,那麼這時候線程 B 就可以提前重新獲取這把鎖,導致臨界區代碼不能嚴格的串行執行。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鎖被誤刪。假如以上情形中的線程A執行完後,它並不知道此時的鎖持有者是線程 B,線程A會繼續執行 DEL 指令來釋放鎖,如果線程 B 在臨界區的邏輯還沒有執行完,線程 A 實際上釋放了線程 B 的鎖。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了避免以上情況,建議不要在執行時間過長的場景中使用 Redis 分佈式鎖,同時一個比較安全的做法是在執行 DEL 釋放鎖之前對鎖進行判斷,驗證當前鎖的持有者是否是自己。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體實現就是在加鎖時將 value 設置爲一個唯一的隨機數(或者線程 ID ),釋放鎖時先判斷隨機數是否一致,然後再執行釋放操作,確保不會錯誤地釋放其它線程持有的鎖,除非是鎖過期了被服務器自動釋放,整個過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/2d\/88\/2dfe6d82fefe6a5d0c2e5d98492b7888.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但判斷 value 和刪除 key 是兩個獨立的操作,並不是原子性的,所以這個地方需要使用 Lua 腳本進行處理,因爲 Lua 腳本可以保證連續多個指令的原子性執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/ec\/5e\/ecd616659beb01457a033c61a0f5b65e.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/66\/d6\/660c1c540c835f6c8c6e77601db894d6.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於 Redis 單節點的分佈式鎖基本完成了,但是這並不是一個完美的方案,只是相對完全一點,因爲它並沒有完全解決當前線程執行超時鎖被提前釋放後,其它線程乘虛而入的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3.1.3 使用 Redisson 的分佈式鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼能解決鎖被提前釋放這個問題呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以利用鎖的可重入特性,讓獲得鎖的線程開啓一個定時器的守護線程,每 expireTime\/3 執行一次,去檢查該線程的鎖是否存在,如果存在則對鎖的過期時間重新設置爲 expireTime,即利用守護線程對鎖進行“續命”,防止鎖由於過期提前釋放。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然業務要實現這個守護進程的邏輯還是比較複雜的,可能還會出現一些未知的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前互聯網公司在生產環境用的比較廣泛的開源框架 Redisson 很好地解決了這個問題,非常的簡便易用,且支持 Redis 單實例、Redis M-S、Redis Sentinel、Redis Cluster 等多種部署架構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"感興趣的朋友可以查閱下官方文檔或者源碼:"},{"type":"link","attrs":{"href":"https:\/\/github.com\/redisson\/redisson\/wiki","title":"","type":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"https:\/\/github.com\/redisson\/redisson\/wiki"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實現原理如圖所示(圖中以 Redis 集羣爲例):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/15\/87\/1522bfd21c92cddc8072934f336e0787.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 基於 Redis 多機實現的分佈式鎖 Redlock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上幾種基於 Redis 單機實現的分佈式鎖其實都存在一個問題,就是加鎖時只作用在一個 Redis 節點上,即使 Redis 通過 Sentinel 保證了高可用,但由於 Redis 的複製是異步的,Master 節點獲取到鎖後在未完成數據同步的情況下發生故障轉移,此時其他客戶端上的線程依然可以獲取到鎖,因此會喪失鎖的安全性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整個過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端 A 從 Master 節點獲取鎖。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Master 節點出現故障,主從複製過程中,鎖對應的 key 沒有同步到 Slave 節點。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Slave升 級爲 Master 節點,但此時的 Master 中沒有鎖數據。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端 B 請求新的 Master 節點,並獲取到了對應同一個資源的鎖。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出現多個客戶端同時持有同一個資源的鎖,不滿足鎖的互斥性。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正因爲如此,在 Redis 的分佈式環境中,Redis 的作者 antirez 提供了 RedLock 的算法來實現一個分佈式鎖,該算法大概是這樣的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設有 N(N>=5)個 Redis 節點,這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制,確保在這N個節點上使用與在 Redis 單實例下相同的方法獲取和釋放鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取鎖的過程,客戶端應執行如下操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取當前 Unix 時間,以毫秒爲單位。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按順序依次嘗試從5個實例使用相同的 key 和具有唯一性的 value(例如 UUID)獲取鎖。當向 Redis 請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端 Redis 已經掛掉的情況下,客戶端還在一直等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個 Redis 實例請求獲取鎖。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N\/2+1,這裏是3個節點)的 Redis 節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果取到了鎖,key 的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果因爲某些原因,獲取鎖失敗(沒有在至少N\/2+1個 Redis 實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的 Redis 實例上進行解鎖(使用 Redis Lua 腳本)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"釋放鎖的過程相對比較簡單:客戶端向所有 Redis 節點發起釋放鎖的操作,包括加鎖失敗的節點,也需要執行釋放鎖的操作,antirez 在算法描述中特別強調這一點,這是爲什麼呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原因是可能存在某個節點加鎖成功後返回客戶端的響應包丟失了,這種情況在異步通信模型中是有可能發生的:客戶端向服務器通信是正常的,但反方向卻是有問題的。雖然對客戶端而言,由於響應超時導致加鎖失敗,但是對 Redis節點而言,SET 指令執行成功,意味着加鎖成功。因此,釋放鎖的時候,客戶端也應該對當時獲取鎖失敗的那些 Redis 節點同樣發起請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,爲了避免 Redis 節點發生崩潰重啓後造成鎖丟失,從而影響鎖的安全性,antirez 還提出了延時重啓的概念,即一個節點崩潰後不要立即重啓,而是等待一段時間後再進行重啓,這段時間應該大於鎖的有效時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於 Redlock 的更深層次的學習,感興趣的朋友可以查閱下官方文檔,"},{"type":"link","attrs":{"href":"https:\/\/redis.io\/topics\/distlock","title":"","type":null},"content":[{"type":"text","text":"https:\/\/redis.io\/topics\/distlock"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4 總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分佈式系統設計是實現複雜性和收益的平衡,既要儘可能地安全可靠,也要避免過度設計。Redlock 確實能夠提供更安全的分佈式鎖,但也是有代價的,需要更多的 Redis 節點。在實際業務中,一般使用基於單點的 Redis 實現分佈式鎖就可以滿足絕大部分的需求,偶爾出現數據不一致的情況,可通過人工介入回補數據進行解決,正所謂“技術不夠,人工來湊”!。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/d4\/4e\/d451355e47a750b127f43ca99506854e.jpg","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"頭圖"},{"type":"text","text":":Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者"},{"type":"text","text":":冷正磊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/TPJwulKmZmJjGSxLDwiPhQ","title":"","type":null},"content":[{"type":"text","text":"淺析 Redis 分佈式鎖解決方案"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"來源"},{"type":"text","text":":Qunar技術沙龍 - 微信公衆號 [ID:QunarTL]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"轉載"},{"type":"text","text":":著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章