使用ETCD官方提供的API实现分布式锁
此文的目的是我在使用etcd分布式锁的一些记录。
简单锁
此前了解到使用etcd分布式锁,是通过一个课程,这里是代码:JobLock.go。
大致流程是这样的(如何自己来创建一把锁的逻辑也就是这样了):
1 | // 尝试上锁 |
ETCD Concurrency
但我到现在才知道,原来etcd golang库里头已经把这部分给实现了,Concurrency ,什么TryLock
\ Lock
都给实现了。
创建租约,自动续约的操作,则是函数concurrency.NewSession
直接给封装好了。
不过这里锁的还有点不同,课程中的锁,是将一个key当作一把锁,大家去抢这把锁。
而库中实现的是将key prefix
作为一把锁。
所以,在调用lock时,租约session需要为每把锁都创建一个,否则会报错:
1 | s1, err := concurrency.NewSession(cli, concurrency.WithTTL(5)) |
TryLock\Lock
可以看到这里有两个上锁方式:TryLock
\ Lock
。
TryLock
比Lock
,多调用了一个waitDeletes
函数,这个函数模拟了一种公平的先来后到的排队逻辑,等待所有当前比当前 key 的 revision 小的 key 被删除后,锁释放后才返回。
另外此包还封装了一个sync.Locker
的接口函数,提供我们使用:clientv3\concurrency
。
1 | type lockerMutex struct{ *Mutex } |
本文并未对源码,以及相关联知识进行延申的讲解,这里贴上我认为对ETCD分布式锁讲解的比较好的博客:Etcd 应用开发之分布式锁。(博客中对revision 和 CreateRevision 似乎有些搅浑了,我后面查看代码时已改正)
群体效应问题 2020年9月21日
在学习论文ZooKeeper: Wait-free coordination for Internet-scale systems
时,看到了群体效应问题
,简单理解就是:
1 | 如果很多客户端等待锁,对这个锁的竞争就会很激烈,当锁释放时,仅仅有一个等待的客户端获得锁。 |
例如,在博客开头,我们实现的伪代码就是一种有群体效应问题
的简单锁结构。而etcd中实现的锁则规避了这一问题。移除一个锁
仅仅会唤起一个客户端抢锁,通过先来先得
的概念,避免了群体效应。
整体代码
这里结合着来看一下ETCD的代码:
concurrency.NewSession
创建一个ETCD的租约,包含自动续租的操作;注意此处的租约(lease)
每一次创建均不相同,所以即使是相同的程序,不同时间,每次使用的锁,也不是一样的。
1 | s, err := concurrency.NewSession(client, concurrency.WithTTL(5)) |
大体明白了锁的操作,接下来深入源码来看看。
1 | // Mutex implements the sync Locker interface with etcd |
WithFirstCreate
此代码中getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
。
1 | // WithFirstCreate gets the key with the oldest creation revision in the request range. |
看到上面的代码,WithPrefix/WithSort,所以 getOwner 的具体执行效果是会把虽有以 lockkey 开头的 key-value 都拿到,且按照 CreateRevision 升序排列,取第一个值,这个意思就很明白了,就是要拿到当前以 lockkey 为 prefix 的且 CreatereVision 最小的那个 key,就是目前已经拿到锁的 key;
waitDeletes
再看看 waitDeletes 函数的行为, waitDeletes 模拟了一种公平的先来后到的排队逻辑,等待所有当前比当前 key 的 revision 小的 key 被删除后,锁释放后才返回。
1 | func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) { |
Etcd 分布式锁的步骤
分析完 concurrency 的主要代码,不难总结出用 Etcd 构造(公平式长期)分布式锁的一般流程如下:
-
假设锁的 name 为 /root/lockname,用来控制某个共享资源,concurrency 会自动将其转换为目录形式:/root/lockname/
-
客户端 A 连接 Etcd,创建一个租约 Leaseid_A,并设置 TTL(以业务逻辑来定), 以 /root/lockname 为前缀创建全局唯一的 key,该 key 的组织形式为 / root/lockname/{leaseid_A};设置TXN
条件事务
:判断条件:比较
/root/lockname/{leaseid_A}
的 CreateRevision 是否为 0:等于 0 表示目前不存在该 key,Then(put, getOwner) :客户端 A 将此 Key 绑定租约写入 Etcd,同时调用 TXN 事务查询写入的情况和具有相同前缀 / root/lockname / 的 CreateRevision的排序情况;后续直接返回此key相关信息;
不等于 0 表示对应的 key 已经创建了,Else(get, getOwner) :以前缀 /root/lockname/ 读取 keyValue 列表(keyValue 中带有 key 对应的 CreateRevision),判断自己 key 的 CreateRevision是否为当前列表中最小的,如果是则认为获得锁;否则阻塞监听列表中前一个 CreateRevision比自己小的 key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。
-
执行业务逻辑,操作共享资源
-
释放分布式锁,现网的程序逻辑需要实现在正常和异常条件下的释放锁的策略,如捕获 SIGTERM 后执行 Unlock,或者异常退出时,有完善的监控和及时删除 Etcd 中的 Key 的异步机制,避免出现 “死锁” 现象
-
当客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务进行续约续期。如果持有锁期间客户端崩溃,心跳停止,Key 将因租约到期而被删除,从而锁释放,避免死锁
本文标题:使用ETCD官方提供的API实现分布式锁
文章作者:小师
发布时间:2020-08-01
最后更新:2022-05-04
原始链接:chunlife.top/2020/08/01/使用ETCD的分布式锁/
版权声明:本站所有文章均采用知识共享署名4.0国际许可协议进行许可