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
}

参考:


posted @ 2019-04-26 01:53  九卷  阅读(10461)  评论(0编辑  收藏  举报