zookeeper由于其node path的唯一性,也具备实现分布式锁的天然条件。同时得益于其watch机制在实现起来更方便。
本文基于golang实现的zookeeper分布式锁,仅供参考。使用的第三方library是github.com/samuel/go-zookeeper/zk .
GetLock:
GetLock相对来说比较复杂。
Zookeeper在create key的时候可以选择不同模式:
currPath, err := conn.Create(zkKey, nil, 3, zk.WorldACL(zk.PermAll))
第三个参数3表示flag, 有4中取值:
- 0:永久,除非手动删除
- 1:短暂,session断开则该节点也被删除
- 2:会自动在节点后面添加序号
- 3:Ephemeral和Sequential,即,短暂且自动添加序号
这里我们选择EPHEMERAL_SEQUENTIAL,EPHEMERAL的好处是说如果当前session断连了,那么过段时间他锁创建的key就会自动删除,就避免了死锁的问题。
SEQUENTIAL的好处是说此时zookeeper创建的节点是默认自增的,可以很方便的找到prev node path。
由于zookeeper节点的唯一性,创建节点有两种结果:
- success:创建自增节点成功
- err: 创建失败
创建自增节点成功后,我们会拿到创建的节点path,类似 /lock/lock-000001 这种格式,然后我们通过判断当前创建的节点是否为/lock 父节点的第一个子节点,如果是,则说明当前client获得了锁。
children, _, err := conn.Children("/lock")
if err != nil {
fmt.Printf("err: %v", err)
}
if "/lock/"+children[len(children)-1] == currPath {
fmt.Println("lock got success.")
return true
}
如果不是,那么需要等待其他client释放锁。
为了避免节点删除时造成的大量网络请求,俗称羊群效应(可以参考公众号的另一篇文章),我们不会去watch /lock 这个节点,而是watch currPath-1 的节点:
这里还有一个tricky的地方,就是因为watch是两个原子操作,分别是先获取children,如果非首子节点,那么就进行watch操作,如果在watch的时候他的前一个兄弟节点已经delete了,怎么办?
exists, _, eventC, err := conn.ExistsW(prevPath) 可以看出在注册watch事件的时候,会返回一个exists的字段,如果exists是false,则说明要watch的节点已经被delete了,我们直接获得锁。
prevPath := getPrevPath(children, currPath)
exists, _, eventC, err := conn.ExistsW(prevPath)
if err != nil {
fmt.Printf("err: %v", err)
}
if !exists {
return true
}
gottenC := make(chan int)
go func() {
FOR:
for {
select {
case e := <-eventC:
fmt.Println("event type: " + e.Type.String())
if e.Type == zk.EventNodeDeleted {
gottenC <- 1
break FOR
}
}
}
}()
<-gottenC
return true
ReleaseLock
相对于GetLock,ReleaseLock就相对简单很多,
func ReleaseLock(lockId string) {
conn := GetConn()
conn.Delete(lockId, 0)
}
以上,就是我们基于Zookeeper实现的一个分布式锁。
欢迎关注公众号二蛋实验室,每天一篇技术分享,分享前端后端,架构,股票心得等。
本文由博客一文多发平台 OpenWrite 发布!



