分散式鎖的實現及原理
概述
鎖是在執行多執行緒時用於強行限制資源訪問的同步機制,在分散式系統場景下,為了使多個程式(例項)對共享資源的讀寫同步,保證資料的最終一致性,而引入了分散式鎖。
分散式鎖應具備以下特點:
- 互斥性:任意時刻,同一個鎖,只有一個程式能持有
- 安全性:避免死鎖,當程式沒有主動釋放鎖(程式崩潰退出),保證其他程式能夠加鎖
- 可用性:當提供鎖的服務節點故障(當機)時,“熱備” 節點能夠接替故障的節點繼續提供服務,並保證自身持有的資料與故障節點一致。
- 對稱性:對同一個鎖,加鎖和解鎖必須是同一個程式,即不能把其他程式持有的鎖給釋放了
可以基於資料庫,快取,中介軟體實現分散式鎖,比較主流的是使用 Redis 或 Etcd (java 可能更多的是用 ZooKeeper) 來實現,當然也可以基於資料庫等支援事務的中介軟體實現,但相對不夠健壯,也不夠安全,一般不推薦,這裡就不展開說明了。結合以上的四個特點,下面將深入討論這兩種方案的實現方式與原理。
實現方案
基於 Etcd
Ectd 是一個高可用的鍵值儲存系統,具體以下特點:
- 簡單:使用 Go 語言編寫,部署簡單;
- 安全:可選 SSL 證照認證;
- 快速:在保證強一致性的同時,讀寫效能優秀;
- 可靠:採用 Raft 演算法實現分散式系統資料的高可用性和強一致性。
重要的是,etcd 支援以下功能,正是依賴這些功能來實現分散式鎖的:
- Lease 機制:即租約機制(TTL,Time To Live),Etcd 可以為儲存的 KV 對設定租約,當租約到期,KV 將失效刪除;同時也支援續約,即 KeepAlive。
- Revision 機制:每個 key 帶有一個 Revision 屬性值,etcd 每進行一次事務對應的全域性 Revision 值都會加一,因此每個 key 對應的 Revision 屬性值都是全域性唯一的。通過比較 Revision 的大小就可以知道進行寫操作的順序。 在實現分散式鎖時,多個程式同時搶鎖,根據 Revision 值大小依次獲得鎖,可以避免 “羊群效應” (也稱 “驚群效應”),實現公平鎖。
- Prefix 機制:即字首機制,也稱目錄機制。可以根據字首(目錄)獲取該目錄下所有的 key 及對應的屬性(包括 key, value 以及 revision 等)。
- Watch 機制:即監聽機制,Watch 機制支援 Watch 某個固定的 key,也支援 Watch 一個目錄(字首機制),當被 Watch 的 key 或目錄發生變化,客戶端將收到通知。
> 實現過程
就實現過程來說,跟 “買房搖號” 很相似。
1、定義一個 key 目錄(如:/xxx/lock/
)用於存放客戶端(程式)的操作 ID。類似申請買房的號碼牌; 2、客戶端先 put
key /xxx/lock/id
,id 是全域性唯一的,可以使用 UUID,並設定過期時間 TTL
,防止死鎖。記下返回的 Revision
值 R
。類似你拿到一個選房序號,並規定了進去選房時間,超時還沒有選中,就失效了; 3、get
目錄 /xxx/lock/
下所有的 key 及對應的 Revision
值,與上一步返回的 Revision
值進行比較:
- 如果當前返回的
Revision
值 R 小於或等於目錄下所有的 key 對應的Revision
,則當前客戶端獲取到了鎖。類似你是排在第一個選房的,不用等了,直接選房就是; - 否則,記下所有比 R 小的 Revision 對應的 key,Watch
/xxx/lock/
。盯緊大螢幕,等待排你前面的人選房;
4、當所有靠前的 key 都被刪除之後,則意味著的客戶端獲取到了鎖。類似前面的人都選好房或者棄權了,終於輪到你選房了!
但是,這裡有兩個問題,也是分散式鎖實現方案之間的重要區別:
- 客戶端拿到鎖後,在合法時間內(過期時間前)沒有釋放鎖(工作沒有做完),會導致不同客戶端同時拿到或釋放同一個鎖的情況;
- 當鎖依賴的中介軟體服務是多節點叢集部署時,怎麼保證新節點與故障節點的資料一致性?
Etcd 和 Redis 給出了不同的答案,後面將會對比闡述。
基於 Redis
Redis 可以使用 SET 命令:
SET KEY VALUE NX PX 100
這裡的 KEY 是同一個,VALUE 最好是全域性唯一的(原因後面會知道),如果執行成功,則意味著獲取到了鎖;如果失敗則迴圈嘗試,類似自旋鎖的獲取過程,但這裡不需要太頻繁,可以 Sleep
一段時間,還可以對續約次數進行限制。
看起來,這個實現方案比 Etcd 實現要簡單很多,區別就是,Etcd 實現的是公平鎖。但是,結合上面提的兩個問題,就會發現,這只是一個簡單的實現,並沒有給出問題的答案。
方案對比
對於那兩個問題,Etcd 與 Redis 給出了不同的答案。
1)問題一,租約(比工作完成時間)提前到期的問題。
> Etcd
本身支援 KeepAlive
機制,來進行租約續期,在 put
操作成功之後,對 KEY 設定 KeepAlive 即可。Etcd 的租約是與 KV 單獨分開的,有自己的租約 ID,所以實現起來並不複雜。
> Redis
Redis 本身沒有 KeepAlive
的機制,所以,只能客戶端自己模擬實現:
1、首先客戶端 SET 時,VALUE 要是全域性唯一的,也可以使用 UUID,並記下這個 VALUE 值; 2、使用單獨的線(協程)程 GET KEY,並對比 VALUE 值是否與前面的記錄的值相同,如果相同,說明當前客戶端仍然持有鎖,通過 EXPIRE
更新 KEY 失效時間; 3、當工作完程,釋放鎖(刪除 KEY)之前,先關閉這個續約執行緒,並且刪除 KEY 之前也要比較 VALUE 是否與本客戶端設定的一樣,防止釋放別的客戶端持有的鎖;
兩種續約方式,基本原理,效果都類似,Etcd 更優雅一些。
2)問題二,保證節點資料一致性的問題。
這是分散式架構中的基礎也是經典問題。現在為了保證服務穩定,中介軟體(儲存)服務一般都是多節點叢集化部署的。 Etcd 實現了 Raft 演算法,可以保證新節點與故障節點的資料一致性。Redis 由於歷史原因,很早之前都是單機部署的,後面才慢慢的支援叢集部署,由於分散式實現的方案選擇不同,並不保證節點間資料的強一致性。而且 Redis 叢集一般有多個 Master 節點,使用資料分片將資料負載到不同的 Master 節點上,一個 Master 節點有 N 個 Slave 節點,通過主從複製來保證服務可用性和資料一致性。這意味這在實際中叢集在特定的條件下可能會丟失寫操作,原因如下:
- 為了提高寫入的效能,主從複製的過程是非同步的;
- 出現網路隔離,如果某個 Master 節點 A 被隔離在叢集之外,那麼它的從節點 A' 可能被選舉為 Master 節點,此時,連線到 A 的客戶端寫資料可能會丟失;
為了解決這個問題,Redis 作者基於 Redis 設計實現了 Redlock 演算法,大致過程如下:
1、得到當前的時間,微妙單位
2、嘗試順序地在 5 個例項上申請鎖,當然需要使用相同的 key 和 random value,這裡一個 client 需要合理設定與 master 節點溝通的 timeout 大小,避免長時間和一個 fail 了的節點浪費時間
3、當 client 在大於等於 3 個 master 上成功申請到鎖的時候,且它會計算申請鎖消耗了多少時間,這部分消耗的時間採用獲得鎖的當下時間減去第一步獲得的時間戳得到,如果鎖的持續時長(lock validity time)比流逝的時間多的話,那麼鎖就真正獲取到了。
4、如果鎖申請到了,那麼鎖真正的 lock validity time 應該是 origin(lock validity time) - 申請鎖期間流逝的時間
5、如果 client 申請鎖失敗了,那麼它就會在少部分申請成功鎖的 master 節點上執行釋放鎖的操作,重置狀態
雖然,極端情況下,還是不能保證強一致性,但是,基本滿足絕大部分使用場景。這也是多 Master 節點的代價。
小結
通過深入分析分散式鎖的實現,可以發現,由於 Redis 主要是用於資料讀寫快取,需要優先保證大流量場景下讀寫效能,分割槽容錯性以及服務可用性是最重要的;而 Etcd 主要用於配置分發,必須要保證資料強一致性以及服務可用性。這也是 CAP 理論實踐,只能各有取捨。 因此,我們需要根據不同的場景,選擇更合適的方案。就分散式鎖的使用場景來說,使用 Etcd 來實現分散式鎖,要更加的簡潔,也更加安全。
雖然 Etcd (V3) 官方已經支援了分散式鎖的 API 實現,為了理解的更深刻,我自己也造了個輪子<https://github.com/jinhailang/rainforest/tree/master/ivy>STM)實現),所以,還可以使用事務來實現分散式鎖,具體參考 [NewSTM;。此外,因為支援事務(基於軟體事務記憶體機制( 使用例項](https://godoc.org/go.etcd.io/etcd/clientv3/concurrency#example-STM--Apply)。
PS: 事務也是非常常見而且非常重要的概念,我也在文章 <https://github.com/jinhailang/blog/issues/48> 較詳細的闡述了事務的應用及原理。
參考
更多技術文章分享
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- zookeeper 分散式鎖的原理及實現分散式
- Redisson實現分散式鎖---原理Redis分散式
- redisson實現分散式鎖原理Redis分散式
- Redis分散式鎖的原理和實現Redis分散式
- Redis分散式鎖的使用與實現原理Redis分散式
- 分散式鎖實現原理與最佳實踐分散式
- redisson之分散式鎖實現原理(三)Redis分散式
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- 技術分享| 基於 Etcd 的分散式鎖實現原理及方案分散式
- R2M分散式鎖原理及實踐分散式
- 分散式鎖的實現分散式
- 實現分散式鎖分散式
- 分散式鎖實現分散式
- 分散式鎖的實現方案分散式
- ZooKeeper分散式鎖的實現分散式
- redis分散式鎖的實現Redis分散式
- 分散式鎖初窺-分散式鎖的三種實現方式分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- 分散式鎖及其實現分散式
- 「分散式」實現分散式鎖的正確姿勢?!分散式
- 輕量級分散式鎖的設計原理分析與實現分散式
- 溫故知新-分散式鎖的實現原理和存在的問題分散式
- java中的鎖及實現原理Java
- Redis分散式實現原理Redis分散式
- memcached 分散式實現原理分散式
- Elasticsearch系列---實現分散式鎖Elasticsearch分散式
- Redis之分散式鎖實現Redis分散式
- 分散式鎖之Redis實現分散式Redis
- 分散式鎖之Zookeeper實現分散式
- 分散式鎖實現(二):Zookeeper分散式
- Redisson實現分散式鎖—RedissonLockRedis分散式
- redis分散式鎖-java實現Redis分散式Java
- Redis如何實現分散式鎖Redis分散式
- 分散式鎖實現(一):Redis分散式Redis
- 利用Redis實現分散式鎖Redis分散式
- etcd實現分散式鎖分散式
- 6 zookeeper實現分散式鎖分散式