Go 中分佈式鎖學習筆記

【導讀】本文介紹了分佈式鎖和基於 etcd 的分佈式鎖實現。

分佈式鎖

分佈式鎖的特點

  1. 互斥性:和我們本地鎖一樣互斥性是最基本的,但是分佈式鎖需要保證在不同節點的不同線程的互斥。

  2. 可重入性:同一個節點上的同一個線程如果獲取鎖之後那麼也可以再次獲取這個鎖。

  3. 鎖超時:和本地鎖一樣支持鎖超時,防止死鎖。

  4. 高效,高可用:加鎖和解鎖需要高效,同時也需要保證高可用防止分佈式鎖失效,可以增加降級。

  5. 支持阻塞和非阻塞:和 ReentrantLock 一樣支持 lock 和 trylock 以及 tryLock(long timeOut)。

  6. 支持公平鎖和非公平鎖 (可選):公平鎖的意思是按照請求加鎖的順序獲得鎖,非公平鎖就相反是無序的。這個一般來說實現的比較少。

分佈式鎖的實現

鎖的可靠性

爲了確保分佈式鎖可用,我們至少要確保鎖的可靠性,要滿足以下四個條件:

代碼實例

加鎖的目的就是爲了防止競態。

Q:代碼中有j.l = newChanMutex(),在 +646L 有 j.locker.TryLock(),這兩個 lock(l 和 locker) 有何區別?

分佈式鎖具有互斥性,也就是 locker 是在 l 上實現的?

我們在 baseJob 中可以看到相關的定義,如

// baseJob中的Struct裏有如下定義
// 互斥鎖,用來實現讀寫互斥。當某線程無法獲取互斥鎖時,該線程會被直接掛起,不再消耗CPU時間,當其他線程釋放互斥鎖後,操作系統會喚醒那個被掛起的線程
l                     *chanMutex    

// etcd分佈式鎖,爲分佈式應用各節點對共享資源的排他式訪問而設定的鎖,主要目的是爲了保障節點的最終一致性
locker             etcd.Locker   

// chanMutex是一個結構體,作爲接收者,間接實現了Lock/Unlock/TryLock
type chanMutex struct {
    ch chan struct{}
}

// Locker是一個接口,直接實現了Lock/Unlock/TryLock
type Locker interface {
    // Lock acquire lock in etcd
    // it will be blocked if the lock is acquired by other locker with the same key
    Lock() error
    // TryLock try to acquire lock
    // it will return immediately if the lock is already acquired
    TryLock() (bool, error)
    Unlock() error
    // SetExpiration will stop refreshing and reset ttl with expiration
    SetExpiration(time.Time) error
    // StopRefresh make sure refresh() exists
    StopRefresh()
}

chanMutex 作爲接收者實現的 Lock 相關方法是通過通道實現的,而 Locker 中的相關方法就是正常實現的,etcdLock 作爲上述方法的接收者

type etcdLock struct {
    manager       *Manager
    kapi          client.KeysAPI
    ctx           context.Context
    cancel        context.CancelFunc
    acquireCtx    context.Context
    acquireCancel context.CancelFunc
    refreshCtx    context.Context
    refreshCancel context.CancelFunc
    refreshStopC  chan struct{}
    locked        bool

    key             string
    value           string
    ttl             time.Duration
    timeout         time.Duration
    refreshEnabled  bool
    refreshing      bool
    refreshInterval time.Duration
    acceptPrevValue bool
    backoff         Backoff
}

我們看下 newChanMutex 的實現

func newChanMutex() *chanMutex {
    m := &chanMutex{
        ch: make(chan struct{}, 1),
    }
    m.ch <- struct{}{}
    return m
}

Q: JobManager 中也有一個 lock 和 mLock,二者有何區別?

JobManager struct 的定義如下

type JobManager struct {
    mode           string
    lock           sync.Mutex
    m              *models.DemonModel
    em             *external.Manager
    etm            *etcd.Manager
    o              orm.Ormer
    registeredJobs map[string]Job
    c              *cron.Cron
    host           *models.Host
    refreshTicker  *time.Ticker
    mLock          sync.RWMutex
}

其中,lock 是原生的互斥鎖,mLock 是原生的讀寫互斥鎖。RWMutex 是在 Mutex 基礎上實現的。

Mutex 和 RWMutex 的定義如下

// Mutex可理解爲全局鎖,只允許一個讀或者寫的情況
type Mutex struct {
    state int32
    sema  uint32
}

// RWMutex可以加多個讀鎖和一個寫鎖,用於讀次數遠大於寫次數的場景
type RWMutex struct {
    w           Mutex  // held if there are pending writers
    writerSem   uint32 // semaphore for writers to wait for completing readers
    readerSem   uint32 // semaphore for readers to wait for completing writers
    readerCount int32  // number of pending readers
    readerWait  int32  // number of departing readers
}

我們看看在哪裏調用了 lock 和 mLock

func (manager *JobManager) registerJob(jobs ...Job) {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    if manager.registeredJobs == nil {
        manager.registeredJobs = make(map[string]Job)
    }
    //....
}

可以看到在這個代碼塊,我們用到了 lock,也就是在註冊 job 的時候。

func (manager *JobManager) model() *models.DemonModel {
    manager.mLock.RLock()
    defer manager.mLock.RUnlock()
    return manager.m
}

func (manager *JobManager) newModel() error {
    manager.mLock.Lock()
    defer manager.mLock.Unlock()

    m, err := models.NewModel()
    if err != nil {
        return errors.Trace(err)
    }
    manager.m = m
    manager.em = m.EM()
    manager.etm = m.ETM()
    manager.o = m.O()
    manager.host = m.Host()
    return nil
}

而在和數據庫打交道的時候,我們會用到 mLock。

結合這兩個鎖的使用場景,我們可以發現二者的區別在於他們的使用場景不同,lock 適用於讀寫場景單一的情況,而 mLock 適用於讀多寫少的情況下。另一方面,lock 即原生的互斥鎖對其他的讀寫操作都會進行阻塞,但事實上我們並不需要阻塞讀,所以有了 mLock,也就是 RWMutex,該讀寫鎖不會對讀進行阻塞。

分佈式鎖設計的要點

轉自:

juejin.cn/post/6943492133860933663

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