一篇帶給你etcd與分布式鎖
1. 實(shí)現(xiàn)分布式鎖的組件們
在分布式系統(tǒng)中,常用于實(shí)現(xiàn)分布式鎖的組件有:Redis、zookeeper、etcd,下面針對(duì)各自的特性進(jìn)行對(duì)比:
由上圖可以看出三種組件各自的特點(diǎn),其中對(duì)于分布式鎖來(lái)說(shuō)至關(guān)重要的一點(diǎn)是要求CP。但是,Redis集群卻不支持CP,而是支持AP。雖然,官方也給出了redlock的方案,但由于需要部署多個(gè)實(shí)例(超過一半實(shí)例成功才視為成功),部署、維護(hù)比較復(fù)雜。所以在對(duì)一致性要求很高的業(yè)務(wù)場(chǎng)景下(電商、銀行支付),一般選擇使用zookeeper或者etcd。對(duì)比zookeeper與etcd,如果考慮性能、并發(fā)量、維護(hù)成本來(lái)看。由于etcd是用Go語(yǔ)言開發(fā),直接編譯為二進(jìn)制可執(zhí)行文件,并不依賴其他任何東西,則更具有優(yōu)勢(shì)。本文,則選擇etcd來(lái)討論某些觀點(diǎn)。
2. 對(duì)于分布式鎖來(lái)說(shuō)AP為什么不好
在CAP理論中,由于分布式系統(tǒng)中多節(jié)點(diǎn)通信不可避免出現(xiàn)網(wǎng)絡(luò)延遲、丟包等問題一定會(huì)造成網(wǎng)絡(luò)分區(qū),在造成網(wǎng)絡(luò)分區(qū)的情況下,一般有兩個(gè)選擇:CP or AP。
① 選擇AP模型實(shí)現(xiàn)分布式鎖時(shí),client在通過集群主節(jié)點(diǎn)加鎖成功之后,則立刻會(huì)獲取鎖成功的反饋。此時(shí),在主節(jié)點(diǎn)還沒來(lái)得及把數(shù)據(jù)同步給從節(jié)點(diǎn)時(shí)發(fā)生down機(jī)的話,系統(tǒng)會(huì)在從節(jié)點(diǎn)中選出一個(gè)節(jié)點(diǎn)作為新的主節(jié)點(diǎn),新的主節(jié)點(diǎn)沒有老的主節(jié)點(diǎn)對(duì)應(yīng)的鎖數(shù)據(jù),導(dǎo)致其他client可以在新的主節(jié)點(diǎn)上拿到相同的鎖。這個(gè)時(shí)候,就會(huì)導(dǎo)致多個(gè)進(jìn)程/線程/協(xié)程來(lái)操作相同的臨界資源數(shù)據(jù),從而引發(fā)數(shù)據(jù)不一致性等問題。
② 選擇CP模型實(shí)現(xiàn)分布式鎖,只有在主節(jié)點(diǎn)把數(shù)據(jù)同步給大于1/2的從節(jié)點(diǎn)之后才被視為加鎖成功。此時(shí),主節(jié)點(diǎn)由于某些原因down機(jī),系統(tǒng)會(huì)在從節(jié)點(diǎn)中選取出來(lái)數(shù)據(jù)比較新的一個(gè)從節(jié)點(diǎn)作為新的主節(jié)點(diǎn),從而避免數(shù)據(jù)丟失等問題。
所以,對(duì)于分布式鎖來(lái)說(shuō),在對(duì)數(shù)據(jù)有強(qiáng)一致性要求的場(chǎng)景下,AP模型不是一個(gè)好的選擇。如果可以容忍少量數(shù)據(jù)丟失,出于維護(hù)成本等因素考慮,AP模型的Redis可優(yōu)先選擇。
3. 分布式鎖的特點(diǎn)以及操作
對(duì)于分布式鎖來(lái)說(shuō),操作的動(dòng)作包含:
- 獲取鎖
- 釋放鎖
- 業(yè)務(wù)處理過程中過程中,另起線程/協(xié)程進(jìn)行鎖的續(xù)約
4. 關(guān)于etcd
官方文檔永遠(yuǎn)是最好的學(xué)習(xí)資料,官方介紹etcd如是說(shuō):
- 分布式系統(tǒng)使用etcd作為配置管理、服務(wù)發(fā)現(xiàn)和協(xié)調(diào)分布式工作的一致鍵值存儲(chǔ)。許多組織使用etcd來(lái)實(shí)現(xiàn)生產(chǎn)系統(tǒng),如容器調(diào)度器、服務(wù)發(fā)現(xiàn)服務(wù)和分布式數(shù)據(jù)存儲(chǔ)。使用etcd的常見分布式模式包括leader選舉、分布式鎖和監(jiān)視機(jī)器活動(dòng)。
- Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Many organizations use etcd to implement production systems such as container schedulers, service discovery services, and distributed data storage. Common distributed patterns using etcd include leader election, distributed locks, and monitoring machine liveness.
- https://etcd.io/docs/v3.4/learning/why/
分布式鎖僅是etcd可以實(shí)現(xiàn)眾多功能中的一項(xiàng),服務(wù)注冊(cè)與發(fā)現(xiàn)在etcd中用的則會(huì)更多。
官方也對(duì)眾多組件進(jìn)行了對(duì)比,并整理如下:
通過對(duì)比可以看出各自的特點(diǎn),至于具體選擇哪一款,你心中可能也有了自己的答案。
5. etcd實(shí)現(xiàn)分布式鎖的相關(guān)接口
對(duì)于分布式鎖,主要用到etcd對(duì)應(yīng)的添加、刪除、續(xù)約接口。
- // KV:鍵值相關(guān)操作
- type KV interface {
- // 存放.
- Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
- // 獲取.
- Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
- // 刪除.
- Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
- // 壓縮rev指定版本之前的歷史數(shù)據(jù).
- Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
- // 通用的操作執(zhí)行命令,可用于操作集合的遍歷。Put/Get/Delete也是基于Do.
- Do(ctx context.Context, op Op) (OpResponse, error)
- // 創(chuàng)建一個(gè)事務(wù),只支持If/Then/Else/Commit操作.
- Txn(ctx context.Context) Txn
- }
- // Lease:租約相關(guān)操作
- type Lease interface {
- // 分配一個(gè)租約.
- Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
- // 釋放一個(gè)租約.
- Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
- // 獲取剩余TTL時(shí)間.
- TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
- // 獲取所有租約.
- Leases(ctx context.Context) (*LeaseLeasesResponse, error)
- // 續(xù)約保持激活狀態(tài).
- KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
- // 僅續(xù)約激活一次.
- KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
- // 關(guān)閉續(xù)約激活的功能.
- Close() error
- }
6. etcd實(shí)現(xiàn)分布式鎖代碼示例
- package main
- import (
- "context"
- "fmt"
- "go.etcd.io/etcd/clientv3"
- "time"
- )
- var conf clientv3.Config
- // 鎖結(jié)構(gòu)體
- type EtcdMutex struct {
- Ttl int64//租約時(shí)間
- Conf clientv3.Config //etcd集群配置
- Key string//etcd的key
- cancel context.CancelFunc //關(guān)閉續(xù)租的func
- txn clientv3.Txn
- lease clientv3.Lease
- leaseID clientv3.LeaseID
- }
- // 初始化鎖
- func (em *EtcdMutex) init() error {
- var err error
- var ctx context.Context
- client, err := clientv3.New(em.Conf)
- if err != nil {
- return err
- }
- em.txn = clientv3.NewKV(client).Txn(context.TODO())
- em.lease = clientv3.NewLease(client)
- leaseResp, err := em.lease.Grant(context.TODO(), em.Ttl)
- if err != nil {
- return err
- }
- ctx, em.cancel = context.WithCancel(context.TODO())
- em.leaseID = leaseResp.ID
- _, err = em.lease.KeepAlive(ctx, em.leaseID)
- return err
- }
- // 獲取鎖
- func (em *EtcdMutex) Lock() error {
- err := em.init()
- if err != nil {
- return err
- }
- // LOCK
- em.txn.If(clientv3.Compare(clientv3.CreateRevision(em.Key), "=", 0)).
- Then(clientv3.OpPut(em.Key, "", clientv3.WithLease(em.leaseID))).Else()
- txnResp, err := em.txn.Commit()
- if err != nil {
- return err
- }
- // 判斷txn.if條件是否成立
- if !txnResp.Succeeded {
- return fmt.Errorf("搶鎖失敗")
- }
- returnnil
- }
- //釋放鎖
- func (em *EtcdMutex) UnLock() {
- // 租約自動(dòng)過期,立刻過期
- // cancel取消續(xù)租,而revoke則是立即過期
- em.cancel()
- em.lease.Revoke(context.TODO(), em.leaseID)
- fmt.Println("釋放了鎖")
- }
- // groutine1
- func try2lock1() {
- eMutex1 := &EtcdMutex{
- Conf: conf,
- Ttl: 10,
- Key: "lock",
- }
- err := eMutex1.Lock()
- if err != nil {
- fmt.Println("groutine1搶鎖失敗")
- return
- }
- defer eMutex1.UnLock()
- fmt.Println("groutine1搶鎖成功")
- time.Sleep(10 * time.Second)
- }
- // groutine2
- func try2lock2() {
- eMutex2 := &EtcdMutex{
- Conf: conf,
- Ttl: 10,
- Key: "lock",
- }
- err := eMutex2.Lock()
- if err != nil {
- fmt.Println("groutine2搶鎖失敗")
- return
- }
- defer eMutex2.UnLock()
- fmt.Println("groutine2搶鎖成功")
- }
- // 測(cè)試代碼
- func EtcdRunTester() {
- conf = clientv3.Config{
- Endpoints: []string{"127.0.0.1:2379"},
- DialTimeout: 5 * time.Second,
- }
- // 啟動(dòng)兩個(gè)協(xié)程競(jìng)爭(zhēng)鎖
- go try2lock1()
- go try2lock2()
- time.Sleep(300 * time.Second)
- }
總結(jié)
可以提供分布式鎖功能的組件有多種,但是每一種都有自己的脾氣與性格。至于選擇哪一種組件,則要看數(shù)據(jù)對(duì)業(yè)務(wù)的重要性,數(shù)據(jù)要求強(qiáng)一致性推薦支持CP的etcd、zookeeper,數(shù)據(jù)允許少量丟失、不要求強(qiáng)一致性的推薦支持AP的Redis。