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
}

參考:



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM