Go 如何實現分佈式鎖

    分佈式鎖原則上是一種可以解決不同節點上的程序避免資源衝突的一種鎖。實現一個分佈式鎖可以有很多中方式,下面以 Redis 爲例,說明如何在 Go 中實現一個分佈式鎖。

首先你需要一個 Redis 客戶端,這裏可以選擇 go-redis 庫,這是一個功能齊全的 redis 客戶端。安裝這個庫的命令是:

go get -u github.com/go-redis/redis

然後你可以使用下面的代碼實現一個分佈式鎖:

package main
import (
  "fmt"
  "github.com/go-redis/redis"
  "time"
)
var client = redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", 
  DB:       0,  
})
func Lock(key string, value string, expiration time.Duration) (bool, error) {
  return client.SetNX(key, value, expiration).Result()
}
func Unlock(key string, value string) error {
  val, err := client.Get(key).Result()
  if err != nil {
    return err
  }
  if val == value {
    return client.Del(key).Err()
  }
  return fmt.Errorf("key %s is locked by another value %s", key, val)
}
func main() {
  ok, err := Lock("my-key", "my-value", 10*time.Second)
  if err != nil {
    // handle error
  }
  if !ok {
    fmt.Println("Lock failed")
  } else {
    fmt.Println("Lock success")
    // do your job
    Unlock("my-key", "my-value")
  }
}

在上面的代碼中,我們定義了兩個函數:Lock 和 Unlock。Lock 函數會嘗試在 Redis 中設置一個鍵值對,並設置一個過期時間。如果這個鍵值對已經被其他地方設置了,那麼 SetNX 函數會返回 false,否則返回 true。

Unlock 函數則嘗試刪除這個鍵值對,但是在刪除之前,會檢查這個鍵值對的值是否符合輸入的值,如果不符合,那麼認爲這個鎖已經被其他地方獲取,這時就不應該刪除這個鍵值對。

進階

    上面例子,鎖缺失兩個重要的性質一個是可重入一個是如何實現到期自動續簽邏輯。

    在上面的代碼中,我們定義了兩個函數:Lock 和 Unlock。Lock 函數會嘗試在 Redis 中設置一個鍵值對,並設置一個過期時間。如果這個鍵值對已經被其他地方設置了,那麼 SetNX 函數會返回 false,否則返回 true。

Unlock 函數則嘗試刪除這個鍵值對,但是在刪除之前,會檢查這個鍵值對的值是否符合輸入的值,如果不符合,那麼認爲這個鎖已經被其他地方獲取,這時就不應該刪除這個鍵值對。

package main
import (
  "sync"
  "time"
  "github.com/go-redis/redis"
  "github.com/satori/go.uuid"
)
var client = redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", // no password set
  DB:       0,  // use default DB
})
type Lock struct {
  key        string
  value      string
  expiration time.Duration
  mu         sync.Mutex
  isLocked   bool
  count      int
}
func NewLock(key string, expiration time.Duration) *Lock {
  return &Lock{
    key:        key,
    value:      uuid.NewV4().String(),
    expiration: expiration,
  }
}
func (l *Lock) Lock() (bool, error) {
  l.mu.Lock()
  defer l.mu.Unlock()
  if l.isLocked {
    l.count++
    return true, nil
  }
  ok, err := client.SetNX(l.key, l.value, l.expiration).Result()
  if err != nil || !ok {
    return false, err
  }
  l.isLocked = true
  l.count++
  go l.renew()
  return true, nil
}
func (l *Lock) Unlock() error {
  l.mu.Lock()
  defer l.mu.Unlock()
  if !l.isLocked {
    return nil
  }
  l.count--
  if l.count > 0 {
    return nil
  }
  val, err := client.Get(l.key).Result()
  if err != nil {
    return err
  }
  if val != l.value {
    return nil
  }
  l.isLocked = false
  return client.Del(l.key).Err()
}
func (l *Lock) renew() {
  ticker := time.NewTicker(l.expiration / 2)
  for range ticker.C {
    l.mu.Lock()
    if !l.isLocked {
      ticker.Stop()
      l.mu.Unlock()
      break
    }
    client.Expire(l.key, l.expiration)
    l.mu.Unlock()
  }
}
func main() {
  lock := NewLock("my-key", 10*time.Second)
  locked, err := lock.Lock()
  if err != nil {
    panic(err)
  }
  if !locked {
    return
  }
  defer lock.Unlock()
  // do something
}

這段代碼中,Lock 結構體中新加入了 mu、isLocked 和 count 字段,分別表示互斥鎖、是否已經鎖定還有重入次數。當再次獲取鎖的時候,已經鎖定則重入次數增加,否則嘗試獲取鎖。在 unlock 時,如果重入次數大於零,則直接減少重入次數而不釋放鎖。

同時加入了 renew 函數,這個函數會每過一段時間檢查這個鎖是否已經被釋放,未被釋放則續期,並在鎖釋放後停止續期。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Nr4Rmva0hmHDFbQqEB3XiQ