如何實現靠譜的分佈式鎖

 

分佈式鎖,是用來控制分佈式系統中互斥訪問共享資源的一種手段,從而避免並行導致的結果不可控。基本的實現原理和單進程鎖是一致的,通過一個共享標識來確定唯一性,對共享標識進行修改時能夠保證原子性和和對鎖服務調用方的可見性。由於分佈式環境需要考慮各種異常因素,爲實現一個靠譜的分佈式鎖服務引入了一定的複雜度。

分佈式鎖服務一般需要能夠保證:

  • 同一時刻只能有一個線程持有鎖;
  • 鎖能夠可重入;
  • 不會發生死鎖;
  • 具備阻塞鎖特性,且能夠及時從阻塞狀態被喚醒;
  • 鎖服務保證高性能和高可用。

當前使用較多的分佈式鎖方案主要基於 redis、zookeeper 提供的功能特性加以封裝來實現的,下面我
們會簡要分析下這兩種鎖方案的處理流程以及它們各自的問題。

1. 基於 Redis 實現的鎖服務

加鎖流程

SET resource_name my_random_value NX PX max-lock-time

注:資源不存在時才能夠成功執行 set 操作,用於保證鎖持有者的唯一性;同時設置過期時間用於防
止死鎖;記錄鎖的持有者,用於防止解鎖時解掉了不符合預期的鎖。

解鎖流程

if redis.get(“resource_name”) == “ my_random_value”
return redis.del(“resource_name”)
else
return 0

注:使用 lua 腳本保證獲取鎖的所有者、對比解鎖者是否所有者、解鎖是一個原子操作。

該方案的問題在於

  1. 通過過期時間來避免死鎖,過期時間設置多長對業務來說往往比較頭疼,時間短了可能會造成:持有鎖的線程 A 任務還未處理完成,鎖過期了,線程 B 獲得了鎖,導致同一個資源被 A、B 兩個線程併發訪問;時間長了會造成:持有鎖的進程宕機,造成其他等待獲取鎖的進程長時間的無效等待。
  2. Redis 的主從異步複製機制可能丟失數據,會出現如下場景:A 線程獲得了鎖,但鎖數據還未同步到 slave 上,master 掛了,slave 頂成主,線程 B 嘗試加鎖,仍然能夠成功,造成 A、B 兩個線程併發訪問同一個資源。

2. 基於 ZooKeeper 實現的鎖服務

加鎖流程

  1. 在 /resource_name 節點下創建臨時有序節點。
  2. 獲取當前線程創建的節點及 /resource_name 目錄下的所有子節點,確定當前節點序號是否最小,是則加鎖成功。否則監聽序號較小的前一個節點。
    注:zab 一致性協議保證了鎖數據的安全性,不會因爲數據丟失造成多個鎖持有者;心跳保活機制解決死鎖問題,防止由於進程掛掉或者僵死導致的鎖長時間被無效佔用。具備阻塞鎖特性,並通過watch 機制能夠及時從阻塞狀態被喚醒。

解鎖流程

刪除當前線程創建的臨時接點。

該方案的問題在於

通過心跳保活機制解決死鎖會造成鎖的不安全性,可能會出現如下場景:持有鎖的線程 A 僵死或網絡故障,導致服務端長時間收不到來自客戶端的保活心跳,服務端認爲客戶端進程不存活主動釋放鎖,線程 B 搶到鎖,線程 A 恢復,同時有兩個線程訪問共享資源。

基於上訴對現有鎖方案的討論,我們能看到,一個理想的鎖設計目標主要應該解決如下問題:

  1. 鎖數據本身的安全性;
  2. 不發生死鎖;
  3. 不會有多個線程同時持有相同的鎖。

而爲了實現不發生死鎖的目標,又需要引入一種機制,當持有鎖的進程因爲宕機、GC 活者網絡故障等各種原因無法主動過釋放鎖時,能夠有其他手段釋放掉鎖,主流的做法有兩種:

  1. 鎖設置過期時間,過期之後 Server 端自動釋放鎖;
  2. 對鎖的持有進程進行探活,發現持鎖進程不存活時 Server 端自動釋放。

實際上不管採用哪種方式,都可能造成鎖的安全性被破壞,導致多個線程同時持有同一把鎖的情況出現。因此我們認爲鎖設計方案應在預防死鎖和鎖的安全性上取得平衡,沒有一種方案能夠絕對意義上保證不發生死鎖並且是安全的。而鎖一般的用途又可以分爲兩種,實際應用場景下,需要根據具體需求出發,權衡各種因素,選擇合適的鎖服務實現模型。無論選擇哪一種模型,需要我們清楚地知道它在安全性上有哪些不足,以及它會帶來什麼後果。

  • 爲了效率,主要是避免一件事被重複的做多次,用於節省 IT 成本,即使鎖偶然失效,也不會造成數據錯誤,該種情況首要考慮的是如何防止死鎖。
  • 爲了正確性,在任何情況下都要保證共享資源的互斥訪問,一旦發生就意味着數據可能不一致,造成嚴重的後果,該種情況首要考慮的是如何保證鎖的安全。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章