etcd分佈式鎖及事務

前言

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,經常須要協調他們的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證一致性,在這種狀況下,便須要使用到分佈式鎖。git

etcd分佈式鎖設計

  1. 排他性:任意時刻,只能有一個機器的一個線程能獲取到鎖。

經過在etcd中存入key值來實現上鎖,刪除key實現解鎖,參考下面僞代碼:github

func Lock(key string, cli *clientv3.Client) error {
    //獲取key,判斷是否存在鎖
	resp, err := cli.Get(context.Background(), key)
	if err != nil {
		return err
	}
	//鎖存在,返回上鎖失敗
	if len(resp.Kvs) > 0 {
		return errors.New("lock fail")
	}
	_, err = cli.Put(context.Background(), key, "lock")
	if err != nil {
		return err
	}
	return nil
}
//刪除key,解鎖
func UnLock(key string, cli *clientv3.Client) error {
	_, err := cli.Delete(context.Background(), key)
	return err
}

當發現已上鎖時,直接返回lock fail。也能夠處理成等待解鎖,解鎖後競爭鎖。算法

//等待key刪除後再競爭鎖
func waitDelete(key string, cli *clientv3.Client) {
	rch := cli.Watch(context.Background(), key)
	for wresp := range rch {
		for _, ev := range wresp.Events {
			switch ev.Type {
			case mvccpb.DELETE: //刪除
				return
			}
		}
	}
}
  1. 容錯性:只要分佈式鎖服務集羣節點大部分存活,client就能夠進行加鎖解鎖操做。
    etcd基於Raft算法,確保集羣中數據一致性。安全

  2. 避免死鎖:分佈式鎖必定能獲得釋放,即便client在釋放以前崩潰。
    上面分佈式鎖設計有缺陷,假如client獲取到鎖後程序直接崩了,沒有解鎖,那其餘線程也沒法拿到鎖,致使死鎖出現。
    經過給key設定leases來避免死鎖,可是leases過時時間設多長呢?假如設了30秒,而上鎖後的操做比30秒大,會致使如下問題:session

  • 操做沒完成,鎖被別人佔用了,不安全mvc

  • 操做完成後,進行解鎖,這時候把別人佔用的鎖解開了分佈式

解決方案:給key添加過時時間後,以Keep leases alive方式延續leases,當client正常持有鎖時,鎖不會過時;當client程序崩掉後,程序不能執行Keep leases alive,從而讓鎖過時,避免死鎖。看如下僞代碼:ui

//上鎖
func Lock(key string, cli *clientv3.Client) error {
    //獲取key,判斷是否存在鎖
	resp, err := cli.Get(context.Background(), key)
	if err != nil {
		return err
	}
	//鎖存在,等待解鎖後再競爭鎖
	if len(resp.Kvs) > 0 {
		waitDelete(key, cli)
		return Lock(key)
	}
    //設置key過時時間
	resp, err := cli.Grant(context.TODO(), 30)
	if err != nil {
		return err
	}
	//設置key並綁定過時時間
	_, err = cli.Put(context.Background(), key, "lock", clientv3.WithLease(resp.ID))
	if err != nil {
		return err
	}
	//延續key的過時時間
	_, err = cli.KeepAlive(context.TODO(), resp.ID)
	if err != nil {
		return err
	}
	return nil
}
//經過讓key值過時來解鎖
func UnLock(resp *clientv3.LeaseGrantResponse, cli *clientv3.Client) error {
	_, err := cli.Revoke(context.TODO(), resp.ID)
	return err
}

通過以上步驟,咱們初步完成了分佈式鎖設計。其實官方已經實現了分佈式鎖,它大體原理和上述有出入,接下來咱們看下如何使用官方的分佈式鎖。線程

etcd分佈式鎖使用

func ExampleMutex_Lock() {
	cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	// create two separate sessions for lock competition
	s1, err := concurrency.NewSession(cli)
	if err != nil {
		log.Fatal(err)
	}
	defer s1.Close()
	m1 := concurrency.NewMutex(s1, "/my-lock/")

	s2, err := concurrency.NewSession(cli)
	if err != nil {
		log.Fatal(err)
	}
	defer s2.Close()
	m2 := concurrency.NewMutex(s2, "/my-lock/")

	// acquire lock for s1
	if err := m1.Lock(context.TODO()); err != nil {
		log.Fatal(err)
	}
	fmt.Println("acquired lock for s1")

	m2Locked := make(chan struct{})
	go func() {
		defer close(m2Locked)
		// wait until s1 is locks /my-lock/
		if err := m2.Lock(context.TODO()); err != nil {
			log.Fatal(err)
		}
	}()

	if err := m1.Unlock(context.TODO()); err != nil {
		log.Fatal(err)
	}
	fmt.Println("released lock for s1")

	<-m2Locked
	fmt.Println("acquired lock for s2")

	// Output:
	// acquired lock for s1
	// released lock for s1
	// acquired lock for s2
}

此代碼來源於官方文檔,etcd分佈式鎖使用起來很方便。設計

etcd事務

順便介紹一下etcd事務,先看這段僞代碼:

Txn(context.TODO()).If(//若是如下判斷條件成立
	Compare(Value(k1), "<", v1),
	Compare(Version(k1), "=", 2)
).Then(//則執行Then代碼段
	OpPut(k2,v2), OpPut(k3,v3)
).Else(//不然執行Else代碼段
	OpPut(k4,v4), OpPut(k5,v5)
).Commit()//最後提交事務

使用例子,代碼來自官方文檔

func ExampleKV_txn() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   endpoints,
		DialTimeout: dialTimeout,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	kvc := clientv3.NewKV(cli)

	_, err = kvc.Put(context.TODO(), "key", "xyz")
	if err != nil {
		log.Fatal(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
	_, err = kvc.Txn(ctx).
		// txn value comparisons are lexical
		If(clientv3.Compare(clientv3.Value("key"), ">", "abc")).
		// the "Then" runs, since "xyz" > "abc"
		Then(clientv3.OpPut("key", "XYZ")).
		// the "Else" does not run
		Else(clientv3.OpPut("key", "ABC")).
		Commit()
	cancel()
	if err != nil {
		log.Fatal(err)
	}

	gresp, err := kvc.Get(context.TODO(), "key")
	cancel()
	if err != nil {
		log.Fatal(err)
	}
	for _, ev := range gresp.Kvs {
		fmt.Printf("%s : %s\n", ev.Key, ev.Value)
	}
	// Output: key : XYZ
}

總結

若是發展到分佈式服務階段,且對數據的可靠性要求很高,選etcd實現分佈式鎖不會錯。介於對ZooKeeper好感度不強,這裏就不介紹ZooKeeper分佈式鎖了。通常的Redis分佈式鎖,可能出現鎖丟失的狀況(若是你是Java開發者,可使用Redisson客戶端實現分佈式鎖,聽說不會出現鎖丟失的狀況)。

相關文章
相關標籤/搜索