etcd实现分布式锁
前言
分布式锁要解决两个问题:
1、锁竞争
2、死锁
以redis为例,redis提供了setnx来保证原子写入,只有一个客户端能写入成功,也就能成功获得锁。同时为了防止客户端异常导致锁没有及时释放,可以对这个锁设置过期s时间,命令如下:
SET lock_name my_random_value NX PX 30000
除了锁自动过期以外,还需要能手动释放锁,命令如下:
del lock_name
etcd的实现方式
实现流程如下:
-
写入key
客户端连接 etcd,以 /etcd/lock 为前缀创建全局唯一的 key,并设置租约。比如第一个客户端对应的 key="/etcd/lock/UUID1",第二个为 key="/etcd/lock/UUID2"。
-
客户端判断是否获得锁
(1). 通过WithFirstCreate()选项获取/etcd/lock 目录下最早创建的那个key,即CreateVersion最小的那个key;(2). 写入key时会返回一个Header.Revirsion,表示的是本次更新后的ModVersion,如果是第一次创建,则和它的CreateVersion是一致的,可以直接拿这个值和最小的那个CreateVersion做比较,如果相等则表示自己是最早创建的,因此就可以获取锁;
(3). 如果这个key之前已经创建了,则直接获取获取它的CreateVersion,再和最小的那个CreateVersion作比较,如果相等也可以获取锁。
-
执行业务
获得锁后,操作共享资源,执行业务代码。
-
解锁
删除这个key,会触发其他客户端的抢锁。
使用示例
官方包里(github.com/coreos/etcd/clientv3/concurrency)已经实现了上述流程,我们只需要做下简单的调用就可以实现分布式锁了。
package main
import (
"context"
"flag"
"github.com/coreos/etcd/clientv3"
"go.etcd.io/etcd/clientv3/concurrency"
"log"
"strings"
"time"
)
var (
addr = flag.String("addr", "http://127.0.0.1:2379", "etcd addresss")
lockName = flag.String("name", "mylock", "lock name")
)
func main() {
flag.Parse()
endpoints := strings.Split(*addr, ",")
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
userLock(cli) //测试锁
}
func userLock(cli1 *clientv3.Client) {
//为锁生成session
session, err := concurrency.NewSession(cli1)
if err != nil {
log.Fatal(err)
}
defer session.Close()
m := concurrency.NewMutex(session, *lockName)
ctx := context.TODO()
// 请求锁
log.Println("acquiring lock")
m.Lock(ctx)
log.Println("acquired lock")
time.Sleep(time.Duration(10 * time.Second))
m.Unlock(ctx) //释放锁
log.Println("released lock")
}
源码解析
加锁
func (m *Mutex) Lock(ctx context.Context) error {
s := m.s
client := m.s.Client()
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
// 写入key之前先判断这个key是否存在
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
// 写入空值,并加上租约
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
// 如果写入这个key之前这个key已经存在,则直接获取key的信息
get := v3.OpGet(m.myKey)
// 无论key是否存在,都要获取一次前缀里CreateVersion最小的那个key的信息
getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
// 把上述的几个步骤结合到一起做一次事务操作
resp, err := client.Txn(ctx).If(
cmp).Then(
put,
getOwner).Else(
get, getOwner).Commit()
if err != nil {
return err
}
if resp.Succeeded {
// 如果key不存在,则通过可以通过Header.Revision来获取它的ModVersion,因为是首次创建,因此ModVersion=CreateVersion
m.myRev = resp.Header.Revision
}else {
// 否则直接获取它的CreateVersion
m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
// 获取前缀目录中最小的那个CreateVersion
ownerKey := resp.Responses[1].GetResponseRange().Kvs
// 如果自己的CreateVersion等于这个这小的CreateVersion,则获取锁返回
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
m.hdr = resp.Header
return nil
}
// 走到这里说明没有获取锁,那么它必须要等待锁的释放,但这里它不是监听那个最小的CreateVersion的key的删除事件
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
if werr != nil {
m.Unlock(client.Ctx())
} else {
m.hdr = hdr
}
return werr
}
- 写入key时先判断这个key是否存在;
- 如果存在,则直接获取这个key的
CreateVersion
; - 如果不存在,则写入key,并通过PutResponse.Header.Version获取
ModVersion
,因为是首次创建,因此它的CreateVersion
等于ModVersion
; - 无论如何要获取一次前缀目录下
CreateVersion
最小的那个key的信息; - 将上述步骤组合到一起做一次事务操作;
- 判断是否获取到锁,如果自己的
CreateVersion
等于这个最小的CreateVersion
,则获取到锁,并退出; - 如果不相等则没有获取到锁,那它必须要去监听比它的
CreateVersion
小的key的删除事件,如果获监听到事件则可以获取到锁。
监听key的删除事件
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
// 先从前缀目录中找出比当前CreateVersion小的那些key,然后从中再获取CreateVersion最大(离当前key最近)的的那个key
getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
for {
resp, err := client.Get(ctx, pfx, getOpts...)
if err != nil {
return nil, err
}
// 如果没有key则返回
if len(resp.Kvs) == 0 {
return resp.Header, nil
}
lastKey := string(resp.Kvs[0].Key)
// 获取到这个key并监听它的删除事件
if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
return nil, err
}
}
}
- 先从前缀目录中找出比当前
CreateVersion
小的那些key,然后从中再获取CreateVersion
最大(离当前key最近)的的那个key; - 如果找不到则获取锁,直接返回;
- 如果找到,则监听它的删除事件;
解锁
func (m *Mutex) Unlock(ctx context.Context) error {
client := m.s.Client()
if _, err := client.Delete(ctx, m.myKey); err != nil {
return err
}
m.myKey = "\x00"
m.myRev = -1
return nil
}
直接删除这个key就可以了,至此整个流程介绍完毕。