etcd实现分布式锁分析

3篇关于分布式锁的文章,可以结合看:
consul实现分布式锁:https://www.cnblogs.com/jiujuan/p/10527786.html
redis实现分布式锁:https://www.cnblogs.com/jiujuan/p/10595838.html
etcd实现分布式锁:https://www.cnblogs.com/jiujuan/p/12147809.html

分布式锁简介#


在单机情况下,锁的环境比较简单,因为都是在单机的环境里。

而在分布式情况下,多机环境里。由原来的单机系统变成了分布式系统。分布式系统的多线程、多进程分布在不同的机器上,在加上网络这个因素,要控制一个共享资源的使用就复杂得多。比如,网络超时怎么办?网络不可用怎么办?发生死锁怎么办? 等等... ... 一系列问题。

在分布式情况下,需要设计一种分布式锁,来解决这些问题。


分布式锁问题和特性#


设想一下,如果是你来设计一个分布式锁,你会怎样考虑?锁应该具有哪些特性?获取锁过程中会出现什么问题?要解决哪些问题?

经过长时间google后 O(∩_∩)O!,一般会出现下面这些主要问题:

死锁:什么是死锁?就是资源抢占的各方,彼此都在等待对方释放资源,以便自己能获取系统资源,但是没有哪一方退出,这时候就死锁了

惊群效应:多线程/多进程等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群

脑裂:当集群中出现 脑裂 的时候,往往会出现多个 master 的情况,这样数据的一致性会无法得到保障,从而导致整个服务无法正常运行


下面这些特性的:

高可用:也就是可靠性。组成集群的分布式锁系统,某一台机器锁不能提供服务了,其他机器仍然可以提供锁服务。

互斥性:就像单机系统的锁特性一样具有互斥性。不过分布式系统是由多个机器节点组成的。如果有一个节点获取了锁,其他节点必须等待锁释放或者锁超时后,才可以去获取锁资源。

可重入:一个节点获取了锁之后,还可以再次获取整个锁资源。

高效和锁超时:高效是指获取和释放锁高效。 锁超时,防止死锁的发生。

公平锁:节点依次获取锁资源。


etcd如何实现分布式锁#


etcd是怎么解决上面这些问题?它提供了哪些功能来解决上述的特性。

  • 1.raft

raft,是工程上使用较为广泛,强一致性、去中心化、高可用的分布式协议。

raft提供了分布式系统的可靠性功能。

读者可以自行查阅raft相关的资料。比如这个 raft网站,它不仅介绍了raft算法,还在网站最下面提供了不同语言的raft实现。raft算法比paxos算法好理解一些。

  • 2.lease功能

lease功能,就是租约机制(time to live)。

1、etcd可以对存储key-value的数据设置租约,也就是给key-value设置一个过期时间,当租约到期,key-value将会失效而被etcd删除。

2、etcd同时也支持续约租期,可以通过客户端在租约到期之间续约,以避免key-value失效;

3、etcd还支持解约,一旦解约,与该租约绑定的key-value将会失效而删除。

Lease 功能可以保证分布式锁的安全性,为锁对应的 key 配置租约,即使锁的持有者因故障而不能主动释放锁,锁也会因租约到期而自动释放。

  • 3.watch功能

监听功能。watch 机制支持监听某个固定的key,它也支持watch一个范围(前缀机制),当被watch的key或范围发生变化时,客户端将收到通知。

在实现分布式锁时,如果抢锁失败,可通过 Prefix 机制返回的 KeyValue 列表获得 Revision 比自己小且相差最小的 key(称为 pre-key),对 pre-key 进行监听,因为只有它释放锁,自己才能获得锁,如果 Watch 到 pre-key 的 DELETE 事件,则说明pre-ke已经释放,自己已经持有锁。

  • 4.prefix功能

前缀机制。也称目录机制,如两个 key 命名如下:key1=“/mykey/key1″ , key2=”/mykey/key2″,那么,可以通过前缀-“/mykey”查询,返回包含两个 key-value 对的列表。可以和前面的watch功能配合使用。

例如,一个名为 /mylock 的锁,两个争抢它的客户端进行写操作,实际写入的 key 分别为:key1=”/mylock/UUID1″,key2=”/mylock/UUID2″,其中,UUID 表示全局唯一的 ID,确保两个 key 的唯一性。很显然,写操作都会成功,但返回的 Revision 不一样,那么,如何判断谁获得了锁呢?通过前缀 /mylock 查询,返回包含两个 key-value 对的的 KeyValue 列表,同时也包含它们的 Revision,通过 Revision 大小,客户端可以判断自己是否获得锁,如果抢锁失败,则等待锁释放(对应的 key 被删除或者租约过期),然后再判断自己是否可以获得锁。

lease 功能和 prefix功能,能解决上面的死锁问题。

  • 5.revision功能

每个 key 带有一个 Revision 号,每进行一次事务加一,因此它是全局唯一的,如初始值为 0,进行一次 put(key, value),key 的 Revision 变为 1;同样的操作,再进行一次,Revision 变为 2;换成 key1 进行 put(key1, value) 操作,Revision 将变为 3。

这种机制有一个作用:

通过 Revision 的大小就可以知道进行写操作的顺序。在实现分布式锁时,多个客户端同时抢锁,根据 Revision 号大小依次获得锁,可以避免 “羊群效应” (也称 “惊群效应”),实现公平锁。


etcd的V3版本分布式锁分析#


在etcd的v3的client里有一个concurrency的包,里面实现了分布式锁。
源代码在mutex.go

/clientv3/concurrency/session.go

Copy
type Session struct { client *v3.Client opts *sessionOptions id v3.LeaseID cancel context.CancelFunc donec <-chan struct{} }

/clientv3/concurrency/mutex.go 分布锁实现分析

Copy
// Mutex implements the sync Locker interface with etcd type Mutex struct { s *Session //上面的Session struct pfx string //前缀 myKey string //key myRev int64 //Revision hdr *pb.ResponseHeader } func NewMutex(s *Session, pfx string) *Mutex { return &Mutex{s, pfx + "/", "", -1, nil} } // Lock locks the mutex with a cancelable context. If the context is canceled // while trying to acquire the lock, the mutex tries to clean its stale lock entry. func (m *Mutex) Lock(ctx context.Context) error { s := m.s //上面的Session struct client := m.s.Client() //m.pfx是前缀,比如"myresource/lock/" //s.Lease()是一个64位的整数值,etcd v3引入了lease(租约)的概念,concurrency包基于lease封装了session, //每一个客户端都有自己的lease,也就是说每个客户端都有一个唯一的64位整形值 //m.myKey类似于"myresource/lock/12345" m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease()) //etcdv3新引入的多键条件事务,替代了v2中Compare-And-put操作。 //etcdv3的多键条件事务的语意是先做一个比较(compare)操作, //如果比较成立则执行一系列操作,如果比较不成立则执行另外一系列操作。 //接下来的这部分实现了如果不存在这个key,则将这个key写入到etcd,如果存在则读取这个key的值这样的功能。 //下面这一句,是构建了一个compare的条件,比较的是key的createRevision(createRevision是表示这个key创建时被分配的这个序号。 //当key不存在时,createRevision是0。),如果revision是0,则存入一个key,如果revision不为0,则读取这个key。 //revision是etcd一个全局的序列号,全局唯一且递增,每一个对etcd存储进行改动都会分配一个这个序号,在v2中叫index cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0) //cmp 比较Revision, 当key不存在时,createRevision是0。 // put self in lock waiters via myKey; oldest waiter holds lock put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease())) // reuse key in case this session already holds the lock get := v3.OpGet(m.myKey) // 如果revision为0,则存入,否则获取 resp, err := client.Txn(ctx).If(cmp).Then(put).Else(get).Commit() if err != nil { return err } // 本次操作的revision m.myRev = resp.Header.Revision // 操作失败,则获取else返回的值,即已有的revision if !resp.Succeeded { m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision } ownerKey := resp.Responses[1].GetResponseRange().Kvs if len(ownerKey) == 0 || ownerKey[0].CreateRevision == myRev { m.hdr = resp.Header return nil //成功获取锁 } //如果上面的code操作成功了,则myRev是当前客户端创建的key的revision值。 //waitDeletes等待匹配m.pfx ("/myresource/lock/")这个前缀(可类比在这个目录下的)并且createRivision小于m.myRev-1所有key被删除 //如果没有比当前客户端创建的key的revision小的key,则当前客户端者获得锁 //如果有比它小的key则等待,比它小的被删除 hdr, werr = waitDeletes(ctx, client, m.pfx, m.myRev-1) // release lock key if wait failed if werr != nil { m.Unlock(client.Ctx()) } else { m.hdr = hdr } return werr }

参考:#


posted @   九卷  阅读(10501)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示
CONTENTS