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
}