Golang使用Zookeeper實(shí)現(xiàn)分布式鎖
什么是分布式鎖
分布式鎖是一種在分布式系統(tǒng)中用于控制并發(fā)訪問的機(jī)制。在分布式系統(tǒng)中,多個客戶端可能會同時對同一個資源進(jìn)行訪問,這可能導(dǎo)致數(shù)據(jù)不一致的問題。分布式鎖的作用是確保同一時刻只有一個客戶端能夠?qū)δ硞€資源進(jìn)行訪問,從而避免數(shù)據(jù)不一致的問題。
分布式鎖的實(shí)現(xiàn)通常依賴于一些具有分布式特性的技術(shù),如 ZooKeeper
、Redis
、數(shù)據(jù)庫等。這些技術(shù)提供了在分布式環(huán)境中實(shí)現(xiàn)互斥訪問的機(jī)制,使得多個客戶端在競爭同一個資源時能夠有序地進(jìn)行訪問。
通過使用分布式鎖,可以確保分布式系統(tǒng)中的數(shù)據(jù)一致性和并發(fā)訪問的有序性,從而提高系統(tǒng)的可靠性和穩(wěn)定性。
Zookeeper 與 Redis 的分布式鎖對比
ZooKeeper
和 Redis
都是常用的實(shí)現(xiàn)分布式鎖的工具,但它們在實(shí)現(xiàn)方式、特性、適用場景等方面有一些區(qū)別。以下是 ZooKeeper
分布式鎖與 Redis
分布式鎖的比較:
實(shí)現(xiàn)方式
ZooKeeper
分布式鎖主要依賴于其臨時節(jié)點(diǎn)和順序節(jié)點(diǎn)的特性??蛻舳嗽?nbsp;ZooKeeper
中創(chuàng)建臨時順序節(jié)點(diǎn),并通過監(jiān)聽機(jī)制來實(shí)現(xiàn)鎖的獲取和釋放。Redis
分布式鎖通常使用SETNX(set if not exists)
命令來嘗試設(shè)置一個key
,如果設(shè)置成功則獲取到鎖。也可以通過設(shè)置過期時間和輪詢機(jī)制來防止死鎖和提高鎖的可靠性。
特性
ZooKeeper
分布式鎖具有嚴(yán)格的順序性和公平性,保證了鎖的獲取順序與請求順序一致,避免了饑餓問題。Redis
分布式鎖的性能通常更高,因?yàn)樗且粋€內(nèi)存數(shù)據(jù)庫,讀寫速度非???。然而,它可能存在不公平性和死鎖的風(fēng)險,需要額外的機(jī)制來避免這些問題。
適用場景
ZooKeeper
分布式鎖適用于對順序性和公平性要求較高的場景,如分布式調(diào)度系統(tǒng)、分布式事務(wù)等。Redis
分布式鎖適用于對性能要求較高的場景,如緩存系統(tǒng)、高并發(fā)訪問的系統(tǒng)等。Redis
的高性能使得它在處理大量并發(fā)請求時具有優(yōu)勢。
可靠性
ZooKeeper
分布式鎖具有較高的可靠性,因?yàn)樗蕾囉?nbsp;ZooKeeper
的高可用性和強(qiáng)一致性保證。即使部分節(jié)點(diǎn)宕機(jī),ZooKeeper
也能保證鎖的正確性和一致性。Redis
分布式鎖的可靠性取決于其實(shí)現(xiàn)方式和配置。在某些情況下,如Redis
節(jié)點(diǎn)宕機(jī)或網(wǎng)絡(luò)故障,可能會導(dǎo)致鎖失效或死鎖。因此,需要合理配置Redis
和采取額外的措施來提高鎖的可靠性。
綜上所述,ZooKeeper
分布式鎖和 Redis
分布式鎖各有優(yōu)缺點(diǎn),具體選擇哪種方式取決于實(shí)際業(yè)務(wù)場景和需求。在需要保證順序性和公平性的場景下,ZooKeeper
分布式鎖可能更適合;而在需要高性能和快速響應(yīng)的場景下,Redis
分布式鎖可能更合適。
為什么 Zookeeper 可以實(shí)現(xiàn)分布式鎖
ZooKeeper
可以實(shí)現(xiàn)分布式鎖,主要得益于其以下幾個特性:
- 臨時節(jié)點(diǎn):
ZooKeeper
支持創(chuàng)建臨時節(jié)點(diǎn),這些節(jié)點(diǎn)在創(chuàng)建它們的客戶端會話結(jié)束時會被自動刪除。這種特性使得ZooKeeper
的節(jié)點(diǎn)具有生命周期,可以隨著客戶端的存活而存在,客戶端斷開連接后自動消失,非常適合作為鎖的標(biāo)識。 - 順序節(jié)點(diǎn):
ZooKeeper
的另一個重要特性是支持創(chuàng)建順序節(jié)點(diǎn)。在創(chuàng)建節(jié)點(diǎn)時,ZooKeeper
會在節(jié)點(diǎn)名稱后自動添加一個自增的數(shù)字,確保節(jié)點(diǎn)在ZNode
中的順序性。這個特性使得ZooKeeper
可以實(shí)現(xiàn)分布式鎖中的公平鎖,按照請求的順序分配鎖。 Watcher
機(jī)制:ZooKeeper
還提供了Watcher
機(jī)制,允許客戶端在指定的節(jié)點(diǎn)上注冊監(jiān)聽事件。當(dāng)這些事件觸發(fā)時,ZooKeeper
服務(wù)端會將事件通知到感興趣的客戶端,從而允許客戶端做出相應(yīng)的措施。這種機(jī)制使得ZooKeeper
的分布式鎖可以實(shí)現(xiàn)阻塞鎖,即當(dāng)客戶端嘗試獲取已經(jīng)被其他客戶端持有的鎖時,它可以等待鎖被釋放。
基于以上特性,ZooKeeper
可以實(shí)現(xiàn)分布式鎖。具體實(shí)現(xiàn)流程如下:
- 客戶端需要獲取鎖時,在
ZooKeeper
中創(chuàng)建一個臨時順序節(jié)點(diǎn)作為鎖標(biāo)識。 - 客戶端判斷自己創(chuàng)建的節(jié)點(diǎn)是否是所有臨時順序節(jié)點(diǎn)中序號最小的。如果是,則客戶端獲得鎖;如果不是,則客戶端監(jiān)聽序號比它小的那個節(jié)點(diǎn)。
- 當(dāng)被監(jiān)聽的節(jié)點(diǎn)被刪除時(即持有鎖的客戶端釋放鎖),監(jiān)聽者會收到通知,然后重新判斷自己是否獲得鎖。
- 當(dāng)客戶端釋放鎖時,只需要將會話關(guān)閉,臨時節(jié)點(diǎn)就會被自動刪除,從而釋放了鎖。
因此,ZooKeeper
通過其臨時節(jié)點(diǎn)、順序節(jié)點(diǎn)和 Watcher
機(jī)制等特性,實(shí)現(xiàn)了分布式鎖的功能。
使用 Golang 實(shí)現(xiàn) Zookeeper 分布式鎖
下面我們通過一個簡單的例子來演示如何使用 Golang
實(shí)現(xiàn) ZooKeeper
分布式鎖。
創(chuàng)建 zookeeper 客戶端連接
import "github.com/go-zookeeper/zk" func client() *zk.Conn { // 默認(rèn)端口 2181 c, _, err := zk.Connect([]string{"192.168.2.168"}, time.Second) if err != nil { panic(err) } return c }
創(chuàng)建父節(jié)點(diǎn) - /lock
我們可以在獲取鎖之前,先創(chuàng)建一個父節(jié)點(diǎn),用于存放鎖節(jié)點(diǎn)。
type Lock struct { c *zk.Conn } // 父節(jié)點(diǎn) /lock 不存在的時候進(jìn)行創(chuàng)建 func NewLock() *Lock { c := client() e, _, err := c.Exists("/lock") if err != nil { panic(err) } if !e { _, err := c.Create("/lock", []byte(""), 0, zk.WorldACL(zk.PermAll)) if err != nil { panic(err) } } return &Lock{c: c} }
獲取鎖
在 Zookeeper 分布式鎖實(shí)現(xiàn)中,獲取鎖的過程實(shí)際上就是創(chuàng)建一個臨時順序節(jié)點(diǎn),并判斷自己是否是所有臨時順序節(jié)點(diǎn)中序號最小的。
獲取鎖的關(guān)鍵是:
- 創(chuàng)建的需要是臨時節(jié)點(diǎn)
- 創(chuàng)建的需要是順序節(jié)點(diǎn)
具體創(chuàng)建代碼如下:
p, err := l.c.Create("/lock/lock", []byte(""), zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll))
其中 zk.FlagEphemeral
表示創(chuàng)建的是臨時節(jié)點(diǎn),zk.FlagSequence
表示創(chuàng)建的是順序節(jié)點(diǎn)。
判斷當(dāng)前創(chuàng)建的節(jié)點(diǎn)是否是最小節(jié)點(diǎn)
具體步驟如下:
- 通過
l.c.Children("/lock")
獲取/lock
下的所有子節(jié)點(diǎn) - 對所有子節(jié)點(diǎn)進(jìn)行排序
- 判斷當(dāng)前創(chuàng)建的節(jié)點(diǎn)是否是最小節(jié)點(diǎn)
- 如果是最小節(jié)點(diǎn),則獲取到鎖,函數(shù)調(diào)用返回;如果不是,則監(jiān)聽前一個節(jié)點(diǎn)(這會導(dǎo)致函數(shù)調(diào)用阻塞)
childs, _, err := l.c.Children("/lock") if err != nil { return "", err } // childs 是無序的,所以需要排序,以便找到當(dāng)前節(jié)點(diǎn)的前一個節(jié)點(diǎn),然后監(jiān)聽前一個節(jié)點(diǎn) sort.Strings(childs) // 成功獲取到鎖 p1 := strings.Replace(p, "/lock/", "", 1) if childs[0] == p1 { return p, nil }
不是最小節(jié)點(diǎn),監(jiān)聽前一個節(jié)點(diǎn)
具體步驟如下:
- 通過
sort.SearchStrings
找到當(dāng)前節(jié)點(diǎn)在所有子節(jié)點(diǎn)中的位置 - 調(diào)用
l.c.ExistsW
判斷前一個節(jié)點(diǎn)是否依然存在(鎖有可能在調(diào)用ExistsW
之前已經(jīng)被釋放了),如果不存在則獲取到鎖 - 如果前一個節(jié)點(diǎn)依然存在,則阻塞等待前一個節(jié)點(diǎn)被刪除
// 監(jiān)聽鎖,等待鎖釋放 // 也就是說,如果當(dāng)前節(jié)點(diǎn)不是最小的節(jié)點(diǎn),那么就監(jiān)聽前一個節(jié)點(diǎn) // 一旦前一個節(jié)點(diǎn)被刪除,那么就可以獲取到鎖 index := sort.SearchStrings(childs, p1) b, _, ev, err := l.c.ExistsW("/lock/" + childs[index-1]) if err != nil { return "", err } // 在調(diào)用 ExistsW 之后,前一個節(jié)點(diǎn)已經(jīng)被刪除 if !b { return p, nil } // 等待前一個節(jié)點(diǎn)被刪除 <-ev return p, nil
在調(diào)用 ExistsW
的時候,如果前一個節(jié)點(diǎn)已經(jīng)被刪除,那么 ExistsW
會立即返回 false
,否則我們可以通過 ExistsW
返回的第三個參數(shù) ev
來等待前一個節(jié)點(diǎn)被刪除。
在 <-ev
處,我們通過 <-ev
來等待前一個節(jié)點(diǎn)被刪除,一旦前一個節(jié)點(diǎn)被刪除,ev
會收到一個事件,這個時候我們就可以獲取到鎖了。
釋放鎖
如果調(diào)用 Lock
可以成功獲取到鎖,我們會返回當(dāng)前創(chuàng)建的節(jié)點(diǎn)的路徑,我們可以通過這個路徑來釋放鎖。
func (l *Lock) Unlock(p string) error { return l.c.Delete(p, -1) }
完整代碼
package main import ( "github.com/go-zookeeper/zk" "sort" "strings" "time" ) func client() *zk.Conn { c, _, err := zk.Connect([]string{"192.168.2.168"}, time.Second) //*10) if err != nil { panic(err) } return c } type Lock struct { c *zk.Conn } func NewLock() *Lock { c := client() e, _, err := c.Exists("/lock") if err != nil { panic(err) } if !e { _, err := c.Create("/lock", []byte(""), 0, zk.WorldACL(zk.PermAll)) if err != nil { panic(err) } } return &Lock{c: c} } func (l *Lock) Lock() (string, error) { p, err := l.c.Create("/lock/lock", []byte(""), zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll)) if err != nil { return "", err } childs, _, err := l.c.Children("/lock") if err != nil { return "", err } // childs 是無序的,所以需要排序,以便找到當(dāng)前節(jié)點(diǎn)的前一個節(jié)點(diǎn),然后監(jiān)聽前一個節(jié)點(diǎn) sort.Strings(childs) // 成功獲取到鎖 p1 := strings.Replace(p, "/lock/", "", 1) if childs[0] == p1 { return p, nil } // 監(jiān)聽鎖,等待鎖釋放 // 也就是說,如果當(dāng)前節(jié)點(diǎn)不是最小的節(jié)點(diǎn),那么就監(jiān)聽前一個節(jié)點(diǎn) // 一旦前一個節(jié)點(diǎn)被刪除,那么就可以獲取到鎖 index := sort.SearchStrings(childs, p1) b, _, ev, err := l.c.ExistsW("/lock/" + childs[index-1]) if err != nil { return "", err } // 在調(diào)用 ExistsW 之后,前一個節(jié)點(diǎn)已經(jīng)被刪除 if !b { return p, nil } // 等待前一個節(jié)點(diǎn)被刪除 <-ev return p, nil } func (l *Lock) Unlock(p string) error { return l.c.Delete(p, -1) }
測試代碼
下面這個例子模擬了分布式的 counter
操作,我們通過 ZooKeeper
分布式鎖來保證 counter
的原子性。
當(dāng)然這個例子只是為了說明 ZooKeeper
分布式鎖的使用,實(shí)際上下面的功能通過 redis
自身提供的 incr
就可以實(shí)現(xiàn),不需要這么復(fù)雜。
package main import ( "context" "fmt" "github.com/redis/go-redis/v9" "sync" ) func main() { var count = 1000 var wg sync.WaitGroup wg.Add(count) l := NewLock() // 創(chuàng)建 redis 客戶端連接 redisClient = redis.NewClient(&redis.Options{ Addr: "192.168.2.168:6379", Password: "", // no password set DB: 0, // use default DB }) for i := 0; i < count; i++ { go func(i1 int) { defer wg.Done() // 獲取 Zookeeper 分布式鎖 p, err := l.Lock() if err != nil { return } // 成功獲取到了分布式鎖: // 1. 從 redis 獲取 zk_counter 的值 // 2. 然后對 zk_counter 進(jìn)行 +1 操作 // 3. 最后將 zk_counter 的值寫回 redis cmd := redisClient.Get(context.Background(), "zk_counter") i2, _ := cmd.Int() i2++ redisClient.Set(context.Background(), "zk_counter", i2, 0) // 釋放分布式鎖 err = l.Unlock(p) if err != nil { println(fmt.Errorf("unlock error: %v", err)) return } }(i) } wg.Wait() l.c.Close() }
我們需要將測試程序放到不同的機(jī)器上運(yùn)行,這樣才能模擬分布式環(huán)境。
總結(jié)
最后,再來回顧一下本文內(nèi)容:
sync.Mutex
這種鎖只能保證單進(jìn)程內(nèi)的并發(fā)安全,無法保證分布式環(huán)境下的并發(fā)安全。- 使用
Zookeeper
和Redis
都能實(shí)現(xiàn)分布式鎖,但是Zookeeper
可以保證順序性和公平性,而Redis
可以保證高性能。 Zookeeper
通過其臨時節(jié)點(diǎn)、順序節(jié)點(diǎn)和Watcher
機(jī)制等特性,實(shí)現(xiàn)了分布式鎖的功能。
以上就是Golang使用Zookeeper實(shí)現(xiàn)分布式鎖的詳細(xì)內(nèi)容,更多關(guān)于Go Zookeeper分布式鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Go語言RESTful JSON API創(chuàng)建
這篇文章主要介紹了詳解Go語言RESTful JSON API創(chuàng)建,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05Go語言實(shí)現(xiàn)AzDG可逆加密算法實(shí)例
這篇文章主要介紹了Go語言實(shí)現(xiàn)AzDG可逆加密算法,實(shí)例分析了AzDG可逆加密算法的實(shí)現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-02-02golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例
這篇文章主要為大家介紹了golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐
MQTT是一個基于客戶端-服務(wù)器的消息發(fā)布/訂閱傳輸協(xié)議。本文主要介紹了go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09golang實(shí)現(xiàn)簡易的分布式系統(tǒng)方法
這篇文章主要介紹了golang實(shí)現(xiàn)簡易的分布式系統(tǒng)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10一文弄懂用Go實(shí)現(xiàn)MCP服務(wù)的示例代碼
本文主要介紹了一文弄懂用Go實(shí)現(xiàn)MCP服務(wù)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04Go語言并發(fā)控制之sync.WaitGroup使用詳解
這篇文章主要為大家詳細(xì)介紹了Go語言并發(fā)控制中sync.Map的原理與使用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02