zoukankan      html  css  js  c++  java
  • 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后,一般会出现下面这些主要问题:

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

    惊群效应:多线程/多进程等待同一个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
    }
    

    参考:


  • 相关阅读:
    华为云-Centos7.6-部署elasticSearch7.7
    linux下安装nginx
    Redis5.0.8集群安装
    redis5.0.8单机安装
    Mybatis打印SQL
    PostgreSQL新手教程
    Debian install jdk7
    Debian install jre7
    Debian /etc/apt/sources.list
    安装J2EE的SDK报错:could not find the required version of the Java(TM)2 Runtime Environment in '(null)'的解决办法。
  • 原文地址:https://www.cnblogs.com/jiujuan/p/12147809.html
Copyright © 2011-2022 走看看