【etcd】go etcd实战一:etcd基本使用
【etcd】go etcd实战二:分布式锁
一、分布式锁介绍
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁 ——百度百科。
二、使用方法
- 创建
Session
session, err := concurrency.NewSession(client, concurrency.WithTTL(10))
if err != nil {
fmt.Println(err)
return
}
- 创建
Mutex
mutex := concurrency.NewMutex(session, fmt.Sprintf("/dLock/%s", key))
- 上锁
err = mutex.Lock(context.Background())
if err != nil {
panic(err)
}
err = mutex.TryLock(context.Background())
if err != nil {
panic(err)
}
- 解锁
err = mutex.Unlock(context.Background())
if err != nil {
panic(err)
}
示例代码:
package main
import (
"context"
"fmt"
v3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"time"
)
func main() {
client, err := v3.New(v3.Config{
Endpoints: []string{"10.1.30.79:12379"},
DialTimeout: time.Second * 10,
})
if err != nil {
fmt.Println(err)
return
}
go testLock(client, "l1", "test1", time.Second*3)
testLock(client, "l1", "test2", time.Second*3)
time.Sleep(time.Minute)
}
func testLock(client *v3.Client, key, name string, duration time.Duration) {
session, err := concurrency.NewSession(client, concurrency.WithTTL(30))
if err != nil {
fmt.Println(err)
return
}
mutex := concurrency.NewMutex(session, fmt.Sprintf("/dLock/%s", key))
start := time.Now()
err = mutex.Lock(context.Background())
if err != nil {
panic(err)
}
fmt.Println(name, "cost", time.Now().Sub(start))
time.Sleep(duration)
err = mutex.Unlock(context.Background())
if err != nil {
panic(err)
}
}
执行结果:
三、源码分析
使用etcd
分布式锁,首先要创建Session
实例。
type Session struct {
// 记录etcd连接
client *v3.Client
// 一些配置
opts *sessionOptions
// 租约id
id v3.LeaseID
cancel context.CancelFunc
donec <-chan struct{}
}
// 新建会话实例
func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
for _, opt := range opts {
opt(ops)
}
// 判断租约,没有传入则自动生成
id := ops.leaseID
if id == v3.NoLease {
resp, err := client.Grant(ops.ctx, int64(ops.ttl))
if err != nil {
return nil, err
}
id = resp.ID
}
ctx, cancel := context.WithCancel(ops.ctx)
// 锁key保活
keepAlive, err := client.KeepAlive(ctx, id)
if err != nil || keepAlive == nil {
cancel()
return nil, err
}
// ... ...
}
这里可以看到,创建session
时,会创建etcd
租约,并调用KeepAlive
对租约保活。这保证了上锁进程在出现宕机时,保活会失效,同时该进程的锁也会被解除。
Mutex
包含上锁、解锁等方法实现。
type Mutex struct {
// 会话,包含租约信息
s *Session
pfx string
myKey string
myRev int64
hdr *pb.ResponseHeader
}
func NewMutex(s *Session, pfx string) *Mutex {
return &Mutex{s, pfx + "/", "", -1, nil}
}
// 尝试上锁,不管简介立即返回
func (m *Mutex) TryLock(ctx context.Context) error {
resp, err := m.tryAcquire(ctx)
if err != nil {
return err
}
// key不存在或者是自己创建的key,视为已拥有锁
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
m.hdr = resp.Header
return nil
}
client := m.s.Client()
// 上锁失败,删除key
if _, err := client.Delete(ctx, m.myKey); err != nil {
return err
}
//... ...
}
// 上锁,如果已被锁定,则阻塞等待
func (m *Mutex) Lock(ctx context.Context) error {
resp, err := m.tryAcquire(ctx)
if err != nil {
return err
}
// key不存在或者是自己创建的key,视为已拥有锁
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
m.hdr = resp.Header
return nil
}
client := m.s.Client()
// 等待key被删除
_, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
// ... ...
}
func (m *Mutex) tryAcquire(ctx context.Context) (*v3.TxnResponse, error) {
s := m.s
client := m.s.Client()
// 锁对应key, prefix+leaseId
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
// 可以是否不存在
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
// 设置key
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
// 获取key
get := v3.OpGet(m.myKey)
// 这里是根据prefix获取对应key的拥有者(要注意,入参prefix不同的锁要不同)
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 nil, err
}
m.myRev = resp.Header.Revision
if !resp.Succeeded {
// 如果对应key已存在,则获取本身key对应的revision
m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
return resp, nil
}
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
// 获取revision不超过maxCreateRev最后创建的key
getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
for {
resp, err := client.Get(ctx, pfx, getOpts...)
if err != nil {
return nil, err
}
// 直到被删除后返回
if len(resp.Kvs) == 0 {
return resp.Header, nil
}
lastKey := string(resp.Kvs[0].Key)
if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
return nil, err
}
}
}
func waitDelete(ctx context.Context, client *v3.Client, key string, rev int64) error {
cctx, cancel := context.WithCancel(ctx)
defer cancel()
var wr v3.WatchResponse
// 监听指定key的删除事件
wch := client.Watch(cctx, key, v3.WithRev(rev))
for wr = range wch {
for _, ev := range wr.Events {
if ev.Type == mvccpb.DELETE {
return nil
}
}
}
if err := wr.Err(); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
return fmt.Errorf("lost watcher waiting for delete")
}
// 解锁
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
}
在使用Lock
的时候,首先会检查对应key
是不是已存在,如果已存在并且当前Mutex
实例不是拥有者,就等待锁对应的key
被删除,否则视为上锁成功。在等待key
被删除时,会循环根据revision
获取key
对应的信息,并监听删除事件,当获取到key
的信息为空时代表该锁被解锁,可以进行上锁。
使用TryLock
的时候,前面逻辑和Lock
比较类似,不同的是如果上锁失败不会阻塞等待。
四、封装
为了方便使用etcd实现的分布式锁,我对使用方法进行了封装。源码仓库地址:戳戳戳。封装包含两部分,一部分是分布式锁接口,另一部分是基于etcd的分布式锁实现。
// 解锁函数
type Unlocker func(ctx context.Context) error
// 锁实现类型
type LockType int
const (
// etcd分布式锁
EtcdLock LockType = 0
// redis分布式锁
RedisLock LockType = 1
defaultWaitSeconds = -1
)
type Op struct {
ttl int
}
type OpOption func(*Op)
// 上锁时,可以控制阻塞等待时间
func WithTTL(ttl int) OpOption {
return func(op *Op) {
if ttl > 0 {
op.ttl = ttl
}
}
}
// 锁接口
type Locker interface {
TryLock(ctx context.Context, key string) (Unlocker, error)
Lock(ctx context.Context, key string, ops ...OpOption) (Unlocker, error)
}
// 创建锁实例
// TODO:实现redis锁
func NewLocker(serverAddr string, ttl int, lockType LockType) Locker {
switch lockType {
case EtcdLock:
return newEtcdLock(serverAddr, ttl)
}
return nil
}
可以看到接口比较简单,只有两个上锁方法,解锁以闭包形式返回。
import (
"context"
"errors"
"fmt"
v3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"sync/atomic"
"time"
)
const (
lockType = 1
tryLockType = 2
)
var notInitErr = errors.New("etcd locker: The locker was not initialized")
type etcdLock struct {
addr string
initFlag atomic.Bool
client *v3.Client
ttl int
}
func (e *etcdLock) Lock(ctx context.Context, key string, ops ...OpOption) (Unlocker, error) {
op := &Op{ttl: defaultWaitSeconds}
for _, opt := range ops {
opt(op)
}
// 需要控制阻塞时间时,用上下文控制
var cancel context.CancelFunc
if op.ttl > 0 {
ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(op.ttl))
}
defer func() {
if cancel != nil {
cancel()
}
}()
return e.doLock(ctx, key, lockType)
}
func (e *etcdLock) TryLock(ctx context.Context, key string) (Unlocker, error) {
return e.doLock(ctx, key, tryLockType)
}
func (e *etcdLock) doLock(ctx context.Context, key string, t int) (Unlocker, error) {
if !e.isInit() {
return nil, notInitErr
}
// 创建session实例
session, err := concurrency.NewSession(e.client, concurrency.WithTTL(e.ttl))
if err != nil {
return nil, err
}
// /dLock为前缀,加上入参key,组合为mutex的prefix
lockKey := fmt.Sprintf("/dLock/%s", key)
mutex := concurrency.NewMutex(session, lockKey)
if t == lockType {
err = mutex.Lock(ctx)
} else {
err = mutex.TryLock(ctx)
}
if err != nil {
return nil, err
}
// 返回闭包的解锁方法,不用再传递 mutex值了
unlocker := func(ctx2 context.Context) error {
return mutex.Unlock(ctx)
}
return unlocker, nil
}
func (e *etcdLock) init() {
var err error
e.client, err = v3.New(v3.Config{
Endpoints: []string{e.addr},
DialTimeout: time.Second * 10,
})
if err != nil {
fmt.Println(err)
return
}
e.initFlag.Store(true)
}
func (e *etcdLock) isInit() bool {
if !e.initFlag.Load() {
e.init()
}
return e.initFlag.Load()
}
func newEtcdLock(serverAddr string, ttl int) *etcdLock {
lock := &etcdLock{
addr: serverAddr,
ttl: ttl,
}
lock.init()
return lock
}
使用示例:
package main
import (
"context"
"fmt"
"github.com/sauryniu/distributelock"
v3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"strings"
"time"
"unsafe"
)
func testDLock() {
locker := distributelock.NewLocker("10.1.30.79:12379", 30, distributelock.EtcdLock)
unlocker, err := locker.Lock(context.Background(), "testKey")
if err != nil {
panic(err)
}
defer unlocker(context.Background())
}
func main() {
testDLock()
}