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
    }
    

    参考:


  • 相关阅读:
    安全编码1
    VPP tips
    VPP概述汇总
    C语言安全编码摘录
    TCP-proxy
    Scipy Lecture Notes学习笔记(一)Getting started with Python for science 1.4. Matplotlib: plotting
    Scipy Lecture Notes学习笔记(一)Getting started with Python for science 1.3. NumPy: creating and manipulating numerical data
    Scipy Lecture Notes学习笔记(一)Getting started with Python for science 1.2. The Python language
    Scipy Lecture Notes学习笔记(一)Getting started with Python for science 1.1. Python scientific computing ecosystem
    25马5跑道,求最快的五匹马的需要比赛的次数
  • 原文地址:https://www.cnblogs.com/jiujuan/p/12147809.html
Copyright © 2011-2022 走看看