分布式系统概念 | 分布式锁:数据库、Redis、Zookeeper解决方案
文章目錄
- 分布式鎖
- 數據庫
- 唯一索引
- Redis
- SETNX、EXPIRE
- RedLock算法
- Zookeeper
- 實現原理
- 羊群效應
- 改進方法
- 總結
分布式鎖
隨著互聯網技術的不斷發展、數據量的大幅增加、業務邏輯的復雜化導致傳統的集中式系統已經無法應用于當前的業務場景,因此分布式系統被應用在越來越多的地方,但是在分布式系統中,由于網絡、機器(如網絡延遲、分區,機器宕機)等情況導致場景更加復雜,充滿了不可靠的情況。為了保證一致性,在這種情況下我們就需要用到分布式鎖。
那么分布式鎖需要具備哪些條件呢?
- 獲取、釋放鎖的性能要高
- 判斷鎖的獲取操作必須要是原子的(防止同一個鎖被多個節點獲取)
- 網絡或者機器出現問題導致無法繼續工作時,必須要釋放鎖(防止死鎖)
- 可重入的,一個線程可以多次獲取同一把鎖(防止死鎖)
- 阻塞鎖(依據業務需求)
但是目前并沒有能夠滿足上面所有要求的完美結局方案,對于分布式鎖,我們通常使用以下三種方法來實現
數據庫
唯一索引
我們可以利用數據庫中的唯一索引來實現。由于唯一索引能夠保證記錄只被插入一次,因此我們可以利用其判斷當前是否處于鎖定狀態。所以當想要獲取鎖的時候,就向數據庫中插入一條記錄,而釋放鎖的時候就刪除這條記錄即可。
但是該方法存在以下問題
- 鎖沒有失效時間,如果解鎖失敗的話其他進程無法再獲得該鎖(死鎖)
- 非阻塞鎖,插入失敗就直接報錯,沒有辦法進入隊列重試
- 不可重入,同一線程在沒有釋放鎖之前無法重復獲得該鎖
對于數據庫來說我們還可以選擇使用排他鎖、樂觀鎖等方法來實現分布式鎖,但是由于這些方法對原表有侵入、占用數據庫連接等情況,一般情況下都不做考慮,因此這里也就不詳細描述。
Redis
SETNX、EXPIRE
我們可以利用setnx(set if not exist)命令來實現鎖。只有在緩存中不存在Key的時候才會set并返回true,而Key已存在的時候就直接返回false。同時為了防止獲取鎖失敗而導致的死鎖情況,我們可以利用expire命令對這個key設置一個超時時間。
為了防止我們setnx成功之后線程發生異常中斷導致我們來不及設置expire而導致死鎖,我們通常會使用以下命令來設置
SET key random_value NX EX 30000 /*EX seconds – 設置鍵key的過期時間,單位時秒PX milliseconds – 設置鍵key的過期時間,單位時毫秒NX – 只有鍵key不存在的時候才會設置key的值XX – 只有鍵key存在的時候才會設置key的值 */該命令僅在Key不存在(NX選項)時才插入,并且設置到期時間為30000毫秒(PX選項),value設置為隨機值,該值在所有客戶端和所有鎖定請求中必須唯一(防止被他人誤刪)。
當我們想要釋放鎖時,為了保證安全(防止誤刪除另一個客戶端創建的鎖),僅當密鑰存在且存儲在密鑰上的值恰好是期望的值時,才刪除該密鑰,下面是以lua腳本完成的刪除邏輯
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end這種方法雖然實現起來非常簡單,但是其存在著單點問題,它加鎖時只作用于一個Redis節點上,如果該節點出現故障故障,即使使用哨兵來保證高可用,也會出現鎖丟失的情況,如下場景
考慮到這種情況,Redis作者antirez基于分布式環境下提出了一種更高級的分布式鎖的實現方式:Redlock。
RedLock算法
Redlock 是 Redis 的作者 antirez 給出的集群模式的 Redis 分布式鎖,它基于 N 個完全獨立(不存在主從復制或者其他集群協調機制)的 Redis節點(通常情況下 N 設置成 5,為了資源的合理利用通常為奇數)。
算法的流程如下
雖然RedLock算法比上面的單點Redis鎖更可靠,但是由于分布式的復雜性,實現起來的條件也更加的苛刻。
介于以上情況,我們還可以選擇更加可靠的方法,即Zookeeper實現的分布式鎖。
Zookeeper
Zookeeper是一個為分布式應用提供一致性服務的軟件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。
由于Zookeeper同樣沒有實現鎖API,所以我們利用其數據節點來表示鎖,數據節點分為以下三種類型
- 永久節點:節點創建后永久存在,不會因為會話的消失而消失
- 臨時節點:與永久節點相反,當客戶端結束會話后立即刪除
- 順序節點:會在節點名的后面加一個數字后綴,并且是有序的,例如生成的有序節點為 /lock/node-0000000000,它的下一個有序節點則為 /lock/node-0000000001,以此類推。
實現原理
除了上面介紹的節點外,我們還需要用到Watcher(監視器)來注冊對節點狀態的監聽
- Watcher:注冊一個該節點的監視器,當節點狀態發生變化時,Watcher就會觸發,此時Zookeeper將會向客戶端發送一條通知(Watcher只能被觸發一次)
根據上述特性,我們就可以使用臨時順序節點與Watcher來實現分布式鎖
那如果出現網絡中斷或者機器宕機,鎖還能釋放嗎?
這里需要注意的是,我們使用的是臨時節點,所以當客戶端因為某種原因無法繼續工作時,就會導致會話的中斷,臨時節點就會被Zookeeprer自動刪除。這也就是Zookeeper相較于Redis更加可靠的原因。
羊群效應
上面這個實現方法,大體上能夠滿足一般的分布式集群競爭鎖的需求,并且能夠保證一定的性能。但是隨著機器規模的擴大,其效率會越來越低。
為什么呢?我們思考一下鎖的釋放流程
在我們獲取鎖失敗后,會注冊一個對lock目錄的Watcher監控,當有節點變更消息時,就會通知給所有注冊了的機器。然而這個通知除了使序號最小的節點獲取鎖外,對其他的節點沒有產生任何實際作用。
性能瓶頸的原因就是上面這個問題,大量的Watcher通知和子節點列表獲取兩個操作重復運行,并且絕大多數運行結果都是判斷出自己并非是序號最小的節點,從而繼續等待下一次的通知,浪費了大量的資源。
如果集群規模較大,不僅會對Zookeeper服務器造成巨大的性能影響和網絡沖擊,更嚴重的時候甚至會因為多個節點對應的客戶端同時釋放鎖導致大量的節點消失,從而短時間內向剩余客戶端發送大量的事件通知——這就是所謂的羊群效應。
改進方法
羊群效應出現根源在于其沒有找到事件的真正關注點,對于分布式鎖的競爭過程來說,它的核心邏輯就是判斷自己是否是所有子節點中序號最小的。那么問題就簡單了,我們只需要關注比自己序號小的那一個相關節點的變更情況就可以了,而不再需要關注全局的子列表變更情況。
于是,改進后的獲取流程如下
總結
| 數據庫 | 直接使用數據庫,操作簡單 | 分布式系統的性能瓶頸大部分都在數據庫,而使用數據庫鎖加大了負擔 | 業務邏輯簡單,對性能要求不高 |
| Redis | 性能較高,且實現起來方便 | 鎖超時機制不可靠,當線程獲取鎖時,可能因為處理時間過長導致鎖超時失效 | 追求高性能,允許偶發的鎖失效問題 |
| ZooKeeper | 不依賴超時時間釋放鎖,可靠性高 | 由于頻繁的創建和刪除節點,性能比不上Redis鎖 | 系統要求高可靠性 |
總結
以上是生活随笔為你收集整理的分布式系统概念 | 分布式锁:数据库、Redis、Zookeeper解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flink 容错机制:Checkpoin
- 下一篇: Flink 状态一致性:端到端状态一致性