etcd分布式锁实现
etcd v3的 concurrency 包封装了分布式锁的实现,我们可以在需要分布式锁的场景中直接使用。
先看一个简单的demo:
package main
import (
"context"
"fmt"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/concurrency"
)
var cli *clientv3.Client
var serverList = []string{
"192.168.3.102:2379",
"192.168.3.105:2379",
"192.168.3.103:2379",
}
var userName = "root"
var password = "shiajun666"
var dialTimeout = 5
var opTimeout = 5
func main() {
var err error
cli, err := clientv3.New(clientv3.Config{
Endpoints: serverList,
DialTimeout: time.Duration(dialTimeout) * time.Second,
Username: userName,
Password: password,
})
if err != nil {
fmt.Println("Connect etcd server failed: ", err)
return
}
s1, err := concurrency.NewSession(cli)
if err != nil {
fmt.Println("new s1 err: ", err)
return
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "dist-lock")
s2, err := concurrency.NewSession(cli)
if err != nil {
fmt.Println("new s2 err: ", err)
return
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "dist-lock")
fmt.Println("s1 is trying to get the lock...")
err = m1.Lock(context.Background())
if err != nil {
fmt.Println("s1 lock err: ", err)
return
}
fmt.Println("s1 is keeping the lock! lock key is: ", m1.Key())
waitCh := make(chan struct{})
go func() {
defer close(waitCh)
fmt.Println("s2 is trying to get the lock...")
err = m2.Lock(context.Background())
if err != nil {
fmt.Println("s2 lock err: ", err)
return
}
fmt.Println("s2 is keeping the lock! lock key is: ", m2.Key())
time.Sleep(10 * time.Second)
fmt.Println("s2 is going to unlock...")
err = m2.Unlock(context.Background())
if err != nil {
fmt.Println("s2 unlock err: ", err)
return
}
fmt.Println("s2 unlock!")
}()
time.Sleep(10 * time.Second)
fmt.Println("s1 is going to unlock...")
err = m1.Unlock(context.Background())
if err != nil {
fmt.Println("s1 unlock err: ", err)
return
}
fmt.Println("s1 unlock!")
<-waitCh
}
该demo开启两个session,模拟两个客户端竞争一个分布式锁的场景。客户端s1首先得到分布式锁,当客户端s2尝试加锁的时候会进入等待,直到s1释放锁,s2才能持有分布式锁。
运行,输出结果如下:

etcd查询结果如下:

截图中共有5次查询,第1次是在程序运行之前,第2次和第3次是在s1获得分布式锁之后,第4次是在s2得到锁之后,第5次是在程序结束以后。
可以看到,竞争同一个分布式锁的每一个客户端都会在etcd中创建一个有着相同前缀的key,且每个key都绑定了带有过期时间的租约。客户端释放锁之后,对应的key即删除。
源码分析:
1. NewSession
源码如下:

第一步其实就是调用etcd clientv3的 Grant() 方法申请一个租约,然后通过 KeepAlive() 方法不断进行续租,确保etcd上记录的分布式锁的所有竞争者都是活跃的。
这里lease的TTL默认是60秒,也可以通过为 NewSession() 函数赋值第二个参数 concurrency.WithTTL() 来指定lease的TTL,这里的TTL决定了前一个客户端异常退出以后,后一个客户端多久才能开启工作。
2. NewMutex
源码如下:

这一步很简单,就是初始化一个 Mutex 结构体。
3. Lock
这一步是加锁,这是最重要也是最复杂的一步。
以key为 dist-lock/2de07b11262a2134 的客户端为例,分步解析:
(1)生成key名:分布式锁前缀/第1步中申请的租约ID的十六进制表示,判断key dist-lock/2de07b11262a2134 是否已存在,方法:查询该key的create_revision,若其为0,则不存在,否则已存在;
(2)若key不存在,Put key,同时绑定第1步中申请的租约,若key已存在,Get key;
(3)查询前缀为 dist-lock/ 的create_revision最小的key,即最早创建的key,取其create_revision与当前key dist-lock/2de07b11262a2134 的create_revision进行比较,若相等,则当前key对应的竞争者可获得分布式锁;否则,进行下一步;
(4)查询前缀为 dist-lock/ 的create_revision最大,但小于当前key create_revision的key(即排在当前key前一位的key),调用 Watch() 方法监控该key,直到该key被删除,当前key才能获得分布式锁。(实际上,当watch到前一位的key被删除以后,还会检查一下前面是否还存在create_revision比自己小的key,防止出现个别客户端异常退出的情况,下面源码中 waitDeletes() 方法中for循环则体现了这个逻辑)
相关源码如下:



4. Unlock
最后是释放锁,很简单,直接将对应的key删除即可。
源码如下:

原理总结:
通过对源码的理解,etcd分布式锁的实现原理可总结如下图:

假设有四个客户端需要竞争分布式锁,它们调用 Lock() 进行加锁的顺序(也就是到etcd上创建key的顺序)为:client1、client2、client3、client4,创建的key分别为dist-lock/1、dist-lock/2、dist-lock/3、dist-lock/4。每一个客户端需要调用 KeepAlive() 方法为自己的key进行续租,还需要调用 Watch() 方法监控它的前一个key。如无意外,这四个客户端获得分布式锁的顺序为:client1 -> client2 -> client3 -> client4。整个过程相当于对并发获取分布式锁的多个客户端根据先来后到的顺序进行排队等待,而排队的序号就是各个客户端持有key的create_revision。

浙公网安备 33010602011771号