
























ZooKeeper (ZK) 是分布式系统中实现强一致性分布式锁的首选工具之一。与 Redis 追求极致性能不同,ZK 基于 CP 模型(一致性优先),特别适合金融交易、核心配置管理等绝对不能出错的场景。
在 Go 语言中,使用 ZooKeeper 实现分布式锁主要依赖其临时顺序节点和Watcher 监听机制。
以下是 ZooKeeper 分布式锁的原理介绍及 Go 语言实战代码。
ZK 实现锁的机制就像是一个“银行排队取号”系统,它利用三个核心特性来保证锁的可靠性:
lock-001, lock-002)。这天然形成了一个 FIFO(先进先出)的等待队列。lock-003 只需要监听 lock-002。当 lock-002 被删除(锁释放)时,ZK 通知 lock-003 去尝试获取锁。这避免了所有客户端同时被唤醒争抢锁的“羊群效应”。在 Go 中,我们通常使用 go-zookeeper/zk 客户端库。为了让你更直观地理解,我将代码分为底层原理实现(展示逻辑)和生产级封装(推荐用法)。
这段代码展示了如何通过创建临时顺序节点和监听前驱节点来实现锁。
package main
import (
"fmt"
"sort"
"time"
"github.com/go-zookeeper/zk"
)
func main() {
// 1. 连接 ZooKeeper
conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, 5*time.Second)
if err != nil {
panic(err)
}
defer conn.Close()
lockPath := "/my-distributed-lock"
// 确保根节点存在
if _, err := conn.Create(lockPath, []byte{}, 0, zk.WorldACL(zk.PermAll)); err != nil && err != zk.ErrNodeExists {
panic(err)
}
// 2. 模拟客户端获取锁
node, err := acquireLock(conn, lockPath)
if err != nil {
fmt.Printf("获取锁失败: %v\n", err)
return
}
fmt.Printf("✅ 锁获取成功!节点: %s\n", node)
// 模拟业务执行时间
time.Sleep(5 * time.Second)
// 3. 释放锁(删除节点)
if err := conn.Delete(node, -1); err != nil {
fmt.Printf("释放锁失败: %v\n", err)
} else {
fmt.Println("🔓 锁已释放")
}
}
// acquireLock 核心逻辑
func acquireLock(conn *zk.Conn, path string) (string, error) {
// 创建临时顺序节点
// zk.FlagEphemeral | zk.FlagSequence 表示创建临时且带序号的节点
node, err := conn.Create(path+"/lock-", []byte{}, zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll))
if err != nil {
return "", err
}
for {
// 获取所有子节点
children, _, err := conn.Children(path)
if err != nil {
return "", err
}
// 排序节点(因为序号是字符串,需要正确排序)
sort.Strings(children)
// 判断自己是否是最小的节点(即队列的第一名)
if len(children) > 0 && children[0] == node[len(node)-12:] { // 截取节点名后12位进行比对
return node, nil // 获取锁成功
}
// 如果不是第一名,找到比自己小 1 的前驱节点
var prevNode string
for i, child := range children {
if child == node[len(node)-12:] {
if i > 0 {
prevNode = path + "/" + children[i-1]
}
break
}
}
// 监听前驱节点的删除事件
if prevNode != "" {
fmt.Printf("⏳ 等待锁... 监听节点: %s\n", prevNode)
_, _, ch, err := conn.GetW(prevNode)
if err != nil {
return "", err
}
// 阻塞等待事件
event := <-ch
if event.Err != nil {
return "", event.Err
}
// 收到通知,循环重新检查是否轮到我了
}
}
}
在实际生产环境中,不建议重复造轮子。go-zookeeper 库内部其实已经封装好了一个 Lock 结构体,使用它更简单、更安全。
package main
import (
"fmt"
"time"
"github.com/go-zookeeper/zk"
)
func main() {
conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, 5*time.Second)
if err != nil {
panic(err)
}
defer conn.Close()
// 使用 zk.NewLock 创建锁对象
// 注意:这里传入的是锁的路径前缀,不需要手动创建父节点
lock := zk.NewLock(conn, "/my-app/locks", zk.WorldACL(zk.PermAll))
// 尝试获取锁
if err := lock.Lock(); err != nil {
panic(err)
}
defer lock.Unlock() // 确保退出时释放
fmt.Println("✅ 使用封装锁成功!正在执行关键业务...")
time.Sleep(10 * time.Second)
fmt.Println("业务完成,锁将自动释放")
}
在 Go 微服务架构选型时,通常会在 Redis 和 ZK 之间纠结。以下是核心对比:
| 维度 | ZooKeeper 锁 | Redis 锁 (Redlock) |
|---|---|---|
| 一致性模型 | CP (强一致性) | AP (高可用性,最终一致性) |
| 可靠性 | 极高。客户端宕机自动释放,无死锁风险。 | 依赖持久化和主从同步,极端情况可能丢锁。 |
| 性能 | 中等。涉及磁盘 IO 和网络通信,延迟较高 (ms级)。 | 极高。纯内存操作,微秒级。 |
| 实现复杂度 | 中等。需处理节点监听和会话。 | 简单。但需处理续期(看门狗)和误删问题。 |
| 适用场景 | 金融交易、元数据管理、低频高可靠任务。 | 秒杀、高频缓存更新、对延迟极其敏感的场景。 |
zk.NewLock 即可,它已经帮你处理了顺序节点和 Watcher 的复杂逻辑。此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。