基于GO语言实现分布式定时任务学习(二)---- etcd学习笔记并完成分布式乐观锁case

etcd 基础入门:


一)etcd 功能介绍

  1. 数据存储在集群中的高可用K-V存储。
  2. 允许应用实时监听存储中的K-V变化。
  3. 可以容忍单点故障,并支持网络分区

在这里插入图片描述

在这里插入图片描述
在传统的存储模型中:

  • 如果存储节点是单点存储,呢么遇到宕机,即刻不可用;
  • 如果是主从架构,当主库不可用的使用,虽然可以继续基于从库来读,单主从同步时延容忍度又是新的问题。

etcd 基于抽屉理论来解决该点,所谓的抽屉理论指:

假如我们有一个30人的班级,我将一个秘密告诉其中的16位同学,呢么随便挑选16个同学中,必然有一个是知道我秘密的同学。呢么假如班里一直会有一半以上的同学正常上课,呢么我这个秘密就能正确获取;

etcd 与 Raft 的关系

  • Raft 是强一致的集群日志同步算法
  • etcd是一个分布式KV存储
  • etcd利用raft算法在集群中同步key-value的
Raft 日志概念、异常安全

名词:

replication: 日志在leader生成,向follower赋值,达到各个节点的日志序列组中一致;
term: 任期,重新选举产生的leader, term会单调递增;
log index: 日支行在日志序列的下标;

选举:

  • raft选举leader需要半数以上的节点参与;
  • 节点commit日志最多的选举为Leader;
  • commit日志同样多,则term、index越大的允许选举为leader;
quorum(大多数) 模型

在这里插入图片描述
该模型要求集群中至少有2N+1个节点;

  1. 调用者向leader写入信息后,leader并不会立马同步返回给调用者,而是会follower进行同步。
  2. 同步的follower+leader至少占半数以上的时候(既大于等于N+1个节点后),leader完成本地提交,此时才会返回客户端。
  3. 随后leader会异步通知follower自己完成提交操作,所以该模型也是两阶段提交。

二) etcd相关特性

  1. 交互协议支持GRPC,内部基于ProtoBuffer;
  2. 底层存储是按key有序排列,支持顺序遍历;
  3. 因为key有序,所以etcd天然支持按目录结构高效遍历;
  4. 支持复杂事物,提供类型if…then…else…的事务能力;
  5. 基于租约机制实现key的TTL过期;
  6. etcd 支持MVCC多版本控制(提交会在version单调递增,同key维护多个历史版本),以实现watch机制;
  7. 对于多版本控制,可以执行compact命令完成删除。
watch 工作原理

在这里插入图片描述

lease 租约

在这里插入图片描述

  1. 调用者通过sdk向etcd申请一个单位时长的租约,etcd返回该租约的id;
  2. 随后调用者带着这个租约ID,向etcd申请K-V存储;
  3. K-V存储引擎与租约建立了关联,当该租约过期的时候,便会想K-V存储引擎删除该记录;
  4. 而续租面向的仍旧是租约,需要调用者想租约申请 ‘续租’ ;

三)etcd 功能实践

安装:为了方便学习,本地安装单机环境;

启动日志如下:
在这里插入图片描述


相关ctl 指令介绍 :

在这里插入图片描述

基础API:

1: put
在这里插入图片描述
2:get
在这里插入图片描述
3: 根据前缀查询
在这里插入图片描述
4: watch
在这里插入图片描述
第一个ctl watch 指定模糊前缀,第二个ctl 对其进行变更;于是 ctl1 收到了变更。


相关sdk指令case :

这个etcd 的依赖包直接使用go get 下载不下来,我是使用 gopath + copy 相关的进行的引入;

第一步:创建一个空的文件夹,copy 绝对路径
第二步:export GOPATH= 刚copy的绝对路径
第三步:于上面的绝对路径下创建src包
第四步:将偷渡过来的第三方包放到src下;

在这里插入图片描述
GO 代码启动测试:

import (
	"fmt"
	"go.etcd.io/etcd/clientv3"
	"time"
)

func main() {

	var (
		config clientv3.Config
		client *clientv3.Client
		err    error
	)

	config = clientv3.Config{
		Endpoints:   []string{"127.0.0.1:2379"},
		DialTimeout: 1 * time.Second,
	}

	if client, err = clientv3.New(config); err != nil {
		fmt.Println(err)
		return
	}

	client = client

}

PUT操作

	//用于读写etcd中的kv对
	kv = clientv3.NewKV(client)

	//put
	if putResp, err = kv.Put(context.TODO(), "/cron/jobs/job1", "helloOld"); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("revision ", putResp.Header.Revision)
	}

	//put and get perv
	if putResp, err = kv.Put(context.TODO(), "/cron/jobs/job1", "hello", clientv3.WithPrevKV()); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("revision ", putResp.Header.Revision)
		if putResp.PrevKv != nil {
			fmt.Printf("prevValue : k = %s, v = %s ", string(putResp.PrevKv.Key), string(putResp.PrevKv.Value))
		}
	}

GET操作

	if getResp, err = kv.Get(context.TODO(), "/cron/jobs/job1"); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(getResp.Kvs)
	}

kv.Get(context.TODO(), “/cron/jobs/job1” ,OpOption) ;
Opoption中可以下达查询是否要仅查个数、是否要查当前游标下几个元素等。

	if getResp, err = kv.Get(context.TODO(), "/cron/jobs/",clientv3.WithPrefix()); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(getResp.Kvs)
	}

DEL操作

	if delResp, err = kv.Delete(context.TODO(), "/cron/jobs/job1", clientv3.WithPrevKV()); err != nil {
		fmt.Println(err)
	} else {
		if len(delResp.PrevKvs) != 0 {
			for idx, kvPair = range delResp.PrevKvs {
				fmt.Printf(" index =%d ,del key =%s  del value =%s: ", idx, string(kvPair.Key), string(kvPair.Value))
			}
		}
	}

del中,也同样支持追加Opoption行为,比如从某个Key开始,删除limit个;

租约

case 1 : 创建一个简单的KV,并挂接租约

	//通过客户端申请租约
	lease = clientv3.NewLease(client)
	//默认时间是秒
	if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
		fmt.Println(err)
		return
	}

	leaseId = leaseGrantResp.ID

	kv = clientv3.NewKV(client)
	if putResp, err = kv.Put(context.TODO(), "/cron/lock/job1", "default", clientv3.WithLease(leaseId)); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("写入成功", putResp.Header.Revision)

	for {

		if getResp, err = kv.Get(context.TODO(), "/cron/lock/job1"); err != nil {
			fmt.Println(err)
			return
		}

		if getResp.Count == 0 {
			fmt.Println("kv 被移除了")
		} else {
			fmt.Println(getResp.Kvs)
		}

		time.Sleep(2 * time.Second)

	}

case 2: 创建一个简单的KV,并对其进行续约

	if keepRespChan, err = lease.KeepAlive(context.TODO(), leaseId); err != nil {
		fmt.Println(err)
		return
	}

	go func() {
		for {
			select {
			case keepResp = <-keepRespChan:
				if keepResp == nil {
					fmt.Println("因为长时间失联等异常情况,租约已过期,无法续约")
					goto END
				} else {
					fmt.Println("续约成功", keepResp.ID)
				}
			}
		}
	END:
	}()

keepRespChan的类型是chan of *clientv3.LeaseKeepAliveResponse,在lease.KeepAlive()的返回值中被初始化过了,keepRespChan一直会是一个地址,KeepAlive函数将会close chan。此时<-keepRespChan返回nil,基于此点判断续约是否成功

监听
 
	//创建一个协程,用于变化指定key
	go func() {
		for {
			kv.Put(context.TODO(), "/cron/jobs/job7", "i am job7")

			kv.Delete(context.TODO(), "/cron/jobs/job7")

			time.Sleep(1 * time.Second)
		}
	}()

	// 先GET到当前的值,并监听后续变化
	if getResp, err = kv.Get(context.TODO(), "/cron/jobs/job7"); err != nil {
		fmt.Println(err)
		return
	}

	// 现在key是存在的
	if len(getResp.Kvs) != 0 {
		fmt.Println("当前值 kvs:", getResp.Kvs)              // [key:"/cron/jobs/job7" create_revision:24 mod_revision:24 version:1 value:"i am job7" ]
		fmt.Println("当前值:", string(getResp.Kvs[0].Value)) //i am job7
	}

	// watch当前 revision之后的变化记录
	watchStartRevision = getResp.Header.Revision + 1

	// 创建一个watcher
	watcher = clientv3.NewWatcher(client)

	// 启动监听
	fmt.Println("从该版本向后监听:", watchStartRevision)
	// 监听5秒后进行删除移除
	ctx, cancelFunc := context.WithCancel(context.TODO())
	time.AfterFunc(5*time.Second, func() {
		cancelFunc()
	})

	watchRespChan = watcher.Watch(ctx, "/cron/jobs/job7", 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)
			}
		}
	}
clientv3.Op Get/Put/Del 操作

同样的, 后面也可以加WithPrefix 等其他op操作。

// 创建Op
	putOp = clientv3.OpPut("/cron/jobs/job8", "123123123")

	// 执行OP
	if opResp, err = kv.Do(context.TODO(), putOp); err != nil {
		fmt.Println(err)
		return
	} else {
		fmt.Println("写入Revision:", opResp.Put().Header.Revision)
	}

	// 创建Op
	getOp = clientv3.OpGet("/cron/jobs/job8")

	// 执行OP
	if opResp, err = kv.Do(context.TODO(), getOp); err != nil {
		fmt.Println(err)
		return
	} else {
		fmt.Println("数据Revision:", opResp.Get().Kvs[0].ModRevision) // create rev == mod rev
		fmt.Println("数据value:", string(opResp.Get().Kvs[0].Value))
	}

乐观锁 case

基于etcd 的乐观锁 与 java+zookeeper/redis 的同步抢锁,整体思路一致,回答清楚下面的问题,剩下的基于上述的case学习,可以独立完成。(以后有时间可以试试写一个tryLock(Timeout time) 的方法试试)

F:若单次加锁时间为1S,但是作业任务超过了1秒,如何保证在接下来的时间仍旧作业的时候,资源依然独占?
A:在Java+redis 的分布式锁中, 可以通过当前作业线程创建相关子线程进行定时重置声明周期。而在etcd中可以通过向租约进行续约,来保证租约不过期;

F: 向redis中进行事务操作依赖lua编排指令成’原子‘执行 ,或者通过redission 才可以做到。呢么etcd如何保证事务呢?
A:天然支持,有蛮好用的API;

F:当任务执行完毕后,如何进行释放lock;
A:java+redis 中,往往是在finally中做del操作; etcd 中可以通过defer 函数,当函数退时进行租约进行取消;

F:这种分布式锁都会确保一个前提,若加锁节点宕机,相关的key或path要如何删除呢?
A:在redis中设置expireetcd中通过租约可以达成同样的目的。


代码关键部分:

  1. 申请一个租约,并对做到两点。
    a:租约可进行续约,并且当前函数退出的时候,取消续约;
    b:租约可被取消,触发条件为当前函数退出的时候;
	// 1: 申请租约
	
	lease = clientv3.NewLease(client)
	if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
		fmt.Println(err)
		return
	}

	leaseId = leaseGrantResp.ID
	
	//创建一个用于取消自动续约的context
	ctx, cancelFunc = context.WithCancel(context.TODO())
	//确保函数退出后,自动续租会停止
	defer cancelFunc()
	
	if keepRespChan, err = lease.KeepAlive(ctx, leaseId); err != nil {
		fmt.Println(err)
		return
	}

	go func() {
		for {
			select {
			case keepResp = <-keepRespChan:
				if keepResp == nil {
					fmt.Println("因为长时间失联等异常情况,租约已过期,无法续约")
					goto END
				} else {
					fmt.Println("续约成功", keepResp.ID)
				}
			}
		}
	END:
	}()
defer lease.Revoke(context.TODO(), leaseId)
  1. 基于txn事务进行相关操作
	//如key不存在,则说明为新建事务
	txnResp, err = txn.If(clientv3.Compare(clientv3.CreateRevision("/cron/lock/job9"), "=", 0)).
		Then(clientv3.OpPut("/cron/lock/job9", "", clientv3.WithLease(leaseId))).
		Else(clientv3.OpGet("/cron/lock/job9")).Commit()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值