Go中分布式锁学习笔记

1,518 阅读5分钟

分布式锁

分布式锁的特点

  1. 互斥性:和我们本地锁一样互斥性是最基本的,但是分布式锁需要保证在不同节点的不同线程的互斥。
  2. 可重入性:同一个节点上的同一个线程如果获取锁之后那么也可以再次获取这个锁。
  3. 锁超时:和本地锁一样支持锁超时,防止死锁。
  4. 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  5. 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
  6. 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

分布式锁的实现

  • MySQL
    • 通过隔离性,唯一索引
    • 排他锁
    • 乐观锁
  • ZoomKeeper
    • 利用顺序临时节点的特性来实现
  • Redis
    • Setnx()
  • 自研分布式锁,Chubby

锁的可靠性

为了确保分布式锁可用,我们至少要确保锁的可靠性,要满足以下四个条件:

  • 互斥性,在任意时刻,只能有一个客户端(或者说业务请求)获得锁,并且也只能由该客户端请求解锁成功
  • 避免死锁,即使获取了锁的客户端崩溃没有释放锁,也要保证锁正常过期,后续的客户端能正常加锁
  • 容错性,只要大部分Redis节点可用,客户端就能正常加锁
  • 自旋重试,获取不到锁时,不要直接返回失败,而是支持一定的周期自旋重试,设置一个总的超时时间,当过了超时时间以后还没有获取到锁则返回失败

代码实例

加锁的目的就是为了防止竞态。

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,该读写锁不会对读进行阻塞。

分布式锁设计的要点

  • 锁的时效,应避免单点故障造成死锁
  • 可重入锁:需要我们在关键节点处进行检查。
  • 减少获取锁的操作
  • 加锁的事物或者操作尽量粒度小,减少其他客户端申请锁的等待时间
  • 持锁客户端解锁后要能通知到其他等待锁的节点
  • 考虑可能出现的其他异常,不能因为一个节点解锁失败而影响整个等待任务队列
  • 服务器宕机或者网络异常要有其他备份方案,以此保证最终一致性

Ref