Golang使用Zookeeper實(shí)現(xiàn)分布式鎖
什么是分布式鎖
分布式鎖是一種在分布式系統(tǒng)中用于控制并發(fā)訪問的機(jī)制。在分布式系統(tǒng)中,多個客戶端可能會同時(shí)對同一個資源進(jìn)行訪問,這可能導(dǎo)致數(shù)據(jù)不一致的問題。分布式鎖的作用是確保同一時(shí)刻只有一個客戶端能夠?qū)δ硞€資源進(jìn)行訪問,從而避免數(shù)據(jù)不一致的問題。
分布式鎖的實(shí)現(xiàn)通常依賴于一些具有分布式特性的技術(shù),如 ZooKeeper、Redis、數(shù)據(jù)庫等。這些技術(shù)提供了在分布式環(huán)境中實(shí)現(xiàn)互斥訪問的機(jī)制,使得多個客戶端在競爭同一個資源時(shí)能夠有序地進(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分布式鎖主要依賴于其臨時(shí)節(jié)點(diǎn)和順序節(jié)點(diǎn)的特性??蛻舳嗽?nbsp;ZooKeeper中創(chuàng)建臨時(shí)順序節(jié)點(diǎn),并通過監(jiān)聽機(jī)制來實(shí)現(xiàn)鎖的獲取和釋放。Redis分布式鎖通常使用SETNX(set if not exists)命令來嘗試設(shè)置一個key,如果設(shè)置成功則獲取到鎖。也可以通過設(shè)置過期時(shí)間和輪詢機(jī)制來防止死鎖和提高鎖的可靠性。
特性
ZooKeeper分布式鎖具有嚴(yán)格的順序性和公平性,保證了鎖的獲取順序與請求順序一致,避免了饑餓問題。Redis分布式鎖的性能通常更高,因?yàn)樗且粋€內(nèi)存數(shù)據(jù)庫,讀寫速度非???。然而,它可能存在不公平性和死鎖的風(fēng)險(xiǎn),需要額外的機(jī)制來避免這些問題。
適用場景
ZooKeeper分布式鎖適用于對順序性和公平性要求較高的場景,如分布式調(diào)度系統(tǒng)、分布式事務(wù)等。Redis分布式鎖適用于對性能要求較高的場景,如緩存系統(tǒng)、高并發(fā)訪問的系統(tǒng)等。Redis的高性能使得它在處理大量并發(fā)請求時(shí)具有優(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)分布式鎖,主要得益于其以下幾個特性:
- 臨時(shí)節(jié)點(diǎn):
ZooKeeper支持創(chuàng)建臨時(shí)節(jié)點(diǎn),這些節(jié)點(diǎn)在創(chuàng)建它們的客戶端會話結(jié)束時(shí)會被自動刪除。這種特性使得ZooKeeper的節(jié)點(diǎn)具有生命周期,可以隨著客戶端的存活而存在,客戶端斷開連接后自動消失,非常適合作為鎖的標(biāo)識。 - 順序節(jié)點(diǎn):
ZooKeeper的另一個重要特性是支持創(chuàng)建順序節(jié)點(diǎn)。在創(chuàng)建節(jié)點(diǎn)時(shí),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ā)時(shí),ZooKeeper服務(wù)端會將事件通知到感興趣的客戶端,從而允許客戶端做出相應(yīng)的措施。這種機(jī)制使得ZooKeeper的分布式鎖可以實(shí)現(xiàn)阻塞鎖,即當(dāng)客戶端嘗試獲取已經(jīng)被其他客戶端持有的鎖時(shí),它可以等待鎖被釋放。
基于以上特性,ZooKeeper 可以實(shí)現(xiàn)分布式鎖。具體實(shí)現(xiàn)流程如下:
- 客戶端需要獲取鎖時(shí),在
ZooKeeper中創(chuàng)建一個臨時(shí)順序節(jié)點(diǎn)作為鎖標(biāo)識。 - 客戶端判斷自己創(chuàng)建的節(jié)點(diǎn)是否是所有臨時(shí)順序節(jié)點(diǎn)中序號最小的。如果是,則客戶端獲得鎖;如果不是,則客戶端監(jiān)聽序號比它小的那個節(jié)點(diǎn)。
- 當(dāng)被監(jiān)聽的節(jié)點(diǎn)被刪除時(shí)(即持有鎖的客戶端釋放鎖),監(jiān)聽者會收到通知,然后重新判斷自己是否獲得鎖。
- 當(dāng)客戶端釋放鎖時(shí),只需要將會話關(guān)閉,臨時(shí)節(jié)點(diǎn)就會被自動刪除,從而釋放了鎖。
因此,ZooKeeper 通過其臨時(shí)節(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 不存在的時(shí)候進(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)建一個臨時(shí)順序節(jié)點(diǎn),并判斷自己是否是所有臨時(shí)順序節(jié)點(diǎn)中序號最小的。
獲取鎖的關(guān)鍵是:
- 創(chuàng)建的需要是臨時(shí)節(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)建的是臨時(shí)節(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 的時(shí)候,如果前一個節(jié)點(diǎn)已經(jīng)被刪除,那么 ExistsW 會立即返回 false,否則我們可以通過 ExistsW 返回的第三個參數(shù) ev 來等待前一個節(jié)點(diǎn)被刪除。
在 <-ev 處,我們通過 <-ev 來等待前一個節(jié)點(diǎn)被刪除,一旦前一個節(jié)點(diǎn)被刪除,ev 會收到一個事件,這個時(shí)候我們就可以獲取到鎖了。
釋放鎖
如果調(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通過其臨時(shí)節(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-05
Go語言實(shí)現(xiàn)AzDG可逆加密算法實(shí)例
這篇文章主要介紹了Go語言實(shí)現(xiàn)AzDG可逆加密算法,實(shí)例分析了AzDG可逆加密算法的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02
golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例
這篇文章主要為大家介紹了golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐
MQTT是一個基于客戶端-服務(wù)器的消息發(fā)布/訂閱傳輸協(xié)議。本文主要介紹了go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
golang實(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í)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04
Go語言并發(fā)控制之sync.WaitGroup使用詳解
這篇文章主要為大家詳細(xì)介紹了Go語言并發(fā)控制中sync.Map的原理與使用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02

