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
type Session struct {
client *v3.Client
opts *sessionOptions
id v3.LeaseID
cancel context.CancelFunc
donec <-chan struct{}
}
/clientv3/concurrency/mutex.go 分布锁实现分析
// 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
}