分布式锁与看门狗机制
分布式锁是一种在分布式环境下协调多个节点共享资源访问的机制。它的目的是在分布式系统中实现对共享资源的互斥访问,确保在任何时候只有一个节点能够获得锁并对资源进行操作。
在分布式系统中,看门狗机制通常用于检测和恢复系统的异常状态。对于分布式锁而言,它可以用于监控持有锁的线程或进程是否正常工作。例如,如果持有锁的线程在一段时间内没有响应或未能释放锁,看门狗会认为该线程可能已经失效,这时会强制释放锁以避免死锁。
代码分析
数据结构设计
type RedisLock struct {
client *redis.Client
key string
value string
expiration time.Duration // 锁的过期时间
renewPeriod time.Duration // 看门狗续期周期
ctx context.Context
cancelFunc context.CancelFunc
}
- client:Redis 客户端,用于与 Redis 服务器通信
- key:锁的键名,在 Redis 中作为键存在
- value:锁的值,使用 UUID 生成的唯一标识,确保只有锁的持有者才能释放锁
- expiration:锁的过期时间,防止死锁
- renewPeriod:看门狗续期的时间间隔,通常设置为过期时间的 1/3
- ctx 和cancelFunc:用于控制看门狗协程的生命周期
锁的获取(Acquire 方法)
func (l *RedisLock) Acquire(blocking bool, timeout time.Duration) (bool, error) {
ctx, cancel := context.WithTimeout(l.ctx, timeout)
defer cancel()
startTime := time.Now()
for {
// 使用SET命令原子性地获取锁
set, err := l.client.SetNX(ctx, l.key, l.value, l.expiration).Result()
if err != nil {
return false, err
}
if set {
// 获取锁成功,启动看门狗
l.startWatchdog()
return true, nil
}
if !blocking {
return false, nil
}
if timeout > 0 && time.Since(startTime) >= timeout {
return false, nil
}
// 短暂休眠避免频繁重试
time.Sleep(100 * time.Millisecond)
}
}
关键技术点:
- 原子操作:使用 Redis 的
SetNX
命令原子性地设置锁,等效于SET key value NX EX timeout
- 阻塞模式:通过循环尝试获取锁实现阻塞模式
- 超时控制:使用
context.WithTimeout
控制获取锁的最长时间 - 看门狗启动:获取锁成功后启动看门狗协程,自动续期锁
锁的释放(Release 方法)
func (l *RedisLock) Release() (bool, error) {
// 停止看门狗
if l.cancelFunc != nil {
l.cancelFunc()
}
// 使用Lua脚本原子性地验证并释放锁
luaScript := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
result, err := l.client.Eval(l.ctx, luaScript, []string{l.key}, l.value).Result()
if err != nil {
return false, err
}
return result.(int64) == 1, nil
}
关键技术点:
- 原子性验证与释放:使用 Lua 脚本确保验证锁持有者和释放锁的操作是原子性的
- 防止误释放:只有锁的持有者(value 匹配)才能释放锁
- 停止看门狗:释放锁前先停止看门狗协程,避免不必要的续期操作
看门狗自动续期机制
func (l *RedisLock) startWatchdog() {
ctx, cancel := context.WithCancel(l.ctx)
l.cancelFunc = cancel
go func() {
ticker := time.NewTicker(l.renewPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 续期锁
err := l.renew()
if err != nil {
log.Printf("看门狗续期失败: key=%s, error=%v", l.key, err)
return
}
case <-ctx.Done():
log.Printf("看门狗停止: key=%s", l.key)
return
}
}
}()
}
func (l *RedisLock) renew() error {
// 使用Lua脚本验证并续期锁,确保操作的原子性
luaScript := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
return 0
end
`
// 将过期时间转换为毫秒
expirationMs := int64(l.expiration / time.Millisecond)
result, err := l.client.Eval(l.ctx, luaScript, []string{l.key}, l.value, expirationMs).Result()
if err != nil {
return err
}
if result.(int64) == 0 {
return fmt.Errorf("锁已过期或被其他客户端持有: key=%s", l.key)
}
return nil
}
看门狗机制的关键技术点:
- 定期续期:按照
renewPeriod
设置的时间间隔定期续期锁 - 原子操作:使用 Lua 脚本确保只有锁的持有者才能续期
- 异常处理:续期失败时记录日志并退出协程
- 上下文控制:通过
context
控制协程的生命周期,确保锁释放后看门狗能停止
接口兼容性与最佳实践
- 实现 sync.Locker 接口
func (l *RedisLock) Lock() {
_, err := l.Acquire(true, 0)
if err != nil {
log.Printf("获取锁失败: key=%s, error=%v", l.key, err)
}
}
func (l *RedisLock) Unlock() {
_, err := l.Release()
if err != nil {
log.Printf("释放锁失败: key=%s, error=%v", l.key, err)
}
}
这使得该分布式锁可以无缝集成到使用sync.Locker
接口的 Go 代码中,例如:
lock := NewRedisLock(rdb, "resource_lock", 30*time.Second)
lock.Lock()
defer lock.Unlock()
// 执行关键业务逻辑