etcd 租约、Watch功能、分布式锁的golang实践

背景

本文使用 Golang语言的SDK包 go.etcd.io/etcd/clientv3 实践etcd的租约、Watch等功能,并且实现分布式锁的业务场景。

etcd 租约

etcd过期时间可以通过设置ttl的方式, 通过租约可以控制一组key的过期时间,可以通过续租的方式保持key不过期

//etcd 租约与续约实践
func LeaseTest(env string, ttl int64) (err error) {
	cli, err := getEtcdCli(env)  //封装的clientv3客户端,不用太关心
	if err != nil {
		return
	}
	lease := clientv3.NewLease(cli)    
	leaseGrant, err := lease.Grant(context.Background(), ttl)  //声明一个租约,并且设置ttl
	if err != nil { 
		return
	}

	if _, err = cli.Put(context.Background(), "ping", "pong", clientv3.WithLease(leaseGrant.ID)); err != nil {  //设置key value 并且绑定租约
		return
	}
	/*
		保持长链接,每s续租一次 
	*/
	keepRespChan, err := lease.KeepAlive(context.TODO(), leaseGrant.ID)
	if err != nil {
		fmt.Println(err)
		return
	}
	go func() {        
		//查看续期情况 非必需,帮助观察续租的过程
		for {
			select {
			case resp := <-keepRespChan:
				if resp == nil {
					fmt.Println("租约失效")
					return
				} else {
					fmt.Println("租约成功", resp)
				}
			}
		}
	}()

	for {            //持续检测key是否过期
		values, err := cli.Get(context.Background(), "ping")
		if err != nil {
			break
		}
		if values.Count == 0 {
			fmt.Println("已经过期")
		} else {
                        fmt.Println("没过期", values.Kvs)
                 }
		time.Sleep(time.Second * 1)
	}
	return
}

不自动续约

把 lease.KeepAlive 去掉

ttl时间之后,租约过期后key删除

自动续约

使lease.KeepAlive生效,以及打印测试

可以看出自动续约就是没秒续约一次。

取消续约

两种形式
第一种:

lease.Revoke(context.Background(), leaseGrant.ID)

测试:

租约失效之后,租约的key会立马被删掉
第二种:

ctx, cancelFunc := context.WithCancel(context.TODO())
keepRespChan, err := lease.KeepAlive(ctx, leaseGrant.ID)
···
cancelFunc()  

测试:

租约失效之后,key的ttl到之后删除key

Watch 机制

watch机制可以使客户端监听etcd的某个key的变化,可以实现配置推送,主动下发等业务场景

//etcd 的watch功能

func WatchTest(env string) (err error) {
	ctx := context.Background()
	cli, err := getEtcdCli(env)
	if err != nil {
		return err
	}
	go func() {
		for {  //模拟key的变化
			cli.Put(ctx, "ping", "pong")
			cli.Delete(ctx, "ping")
			time.Sleep(time.Second)
		}
	}()

	pingVal, err := cli.Get(ctx, "ping")
	if err != nil || len(pingVal.Kvs) == 0 {
		return err
	}
	watchStartRevision := pingVal.Header.Revision + 1  //获取revision,观察这个revision之后的变化
	fmt.Println(watchStartRevision)
	watcher := clientv3.NewWatcher(cli)
	ctx, cancelFunc := context.WithCancel(context.TODO())
	time.AfterFunc(5*time.Second, func() {
		cancelFunc()
	})
	watchRespChan := watcher.Watch(ctx, "ping", clientv3.WithRev(watchStartRevision))
	for watchResp := range watchRespChan {
		for _, event := range watchResp.Events {
			switch event.Type {
			case mvccpb.PUT:
				fmt.Println("修改为:", string(event.Kv.Value), "Revision:", event.Kv.CreateRevision, event.Kv.ModRevision)
			case mvccpb.DELETE:
				fmt.Println("删除了", "Revision:", event.Kv.ModRevision)
			}
		}
	}
	return
}

可以看出Watch返回是一个chan,可以持续的监听
测试:

使用txn实现分布式锁

//锁的简单封装
type Lock struct {
	lease      clientv3.Lease
	leaseId    clientv3.LeaseID
	ctx        context.Context
	cancelFunc context.CancelFunc
}

func (l *Lock) Lock() (lock bool, err error) {
	cli, err := getEtcdCli("open")
	if err != nil {
		return false, err
	}
	l.lease = clientv3.NewLease(cli)
	l.ctx, l.cancelFunc = context.WithCancel(context.TODO())
	leaseGrant, err := l.lease.Grant(context.TODO(), 5)
	if err != nil {
		return false, err
	}
	l.leaseId = leaseGrant.ID
	kv := clientv3.NewKV(cli)
	txn := kv.Txn(l.ctx)          
	txn.If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)).
		Then(clientv3.OpPut("lock", "g", clientv3.WithLease(l.leaseId)))
	txnResp, err := txn.Commit()
	if err != nil {
		return false, err
	}
	if !txnResp.Succeeded {
		return false, nil
	}
	//自动续约
	keepRespChan, err := l.lease.KeepAlive(l.ctx, l.leaseId)
	_ = keepRespChan
	return true, nil
}
func (l *Lock) Unlock() {
	//l.cancelFunc()
	l.lease.Revoke(l.ctx, l.leaseId)
}

txn通过简单的"If-Then-Else"实现了原子操作,这里我们租期过期之后需要立刻将key删除,所以使用Revoke。
测试:

func LockTest() {
	go Node("node1", 5)
	go Node("node2", 3)
	select {}
}

func Node(node string, t time.Duration) {
	l := Lock{}
	for {
		getLock, err := l.Lock()
		if err != nil || !getLock {
			continue
		}
		fmt.Println("i get the lock: ", node)
		time.Sleep(time.Second * t)
		l.Unlock()
		fmt.Println("i release the lock: ", node)
		time.Sleep(time.Second)
	}
}

本次代码:https://github.com/zhaoshoucheng/hodgepodge/tree/main/etcd

posted @ 2023-03-20 15:16  zscbest  阅读(860)  评论(0编辑  收藏  举报