Golang基于Redis实现可重入分布式锁详解
本文介绍如何在Golang项目中利用Redis实现具备自动续期能力的可重入分布式锁。通过引入“看门狗”机制,解决因网络波动或进程崩溃导致的锁资源死锁问题,并提供完整的结构体设计与核心代码。
什么是可重入锁
在分布式系统中,为了确保同一时刻只有一个线程能执行特定任务,通常采用“加锁 → 执行 → 解锁”的逻辑。但由于网络不稳定、进程被杀或服务宕机等不可控因素,Unlock()操作可能无法执行,导致锁永久持有。
常见的兜底方案是在Lock时设置过期时间,但在DoJob()执行时长不确定的情况下,固定超时可能导致任务未完成前锁即释放,引发并发冲突;或者设置过长超时导致其他等待者长时间阻塞。
续期解决方案
为了平衡任务执行不确定性与资源竞争效率,业界常采用两种策略:
- 限制任务时长:对
DoJob设置超时(如Golang中的TimeoutContext),并据此设定锁过期时间。 - 动态续期(可重入锁):设置一个较短的基础锁有效期,同时启动一个后台协程(称为WatchDog/看门狗)不断检测任务状态并进行锁续期。这种不断重新获取有效期的机制即为“可重入锁”的核心特征。
Golang具体实现
Golang原生支持协程(goroutine),非常适合实现非阻塞的后台续期逻辑。下面展示基于Redis的标准实现,主要依赖SET NX EX命令加锁,利用context控制看门狗的退出。
完整代码逻辑
该实现包含以下核心要素:
- 数据结构:存储锁Key、过期时间、当前状态及用于取消后台任务的
CancelFunc。 - TryLock:执行原子性SET操作申请锁,成功后启动看门狗并标记上锁成功。
- startWatchDog:新建协程循环执行,每隔一段时间调用Redis
EXPIRE命令延长锁生命周期,直至主流程调用Unlock触发取消信号。 - Unlock:首先触发
cancelFun停止看门狗,随后在Redis中删除Key释放锁。
// DistributeLockRedis 基于redis的分布式可重入锁,自动续租
type DistributeLockRedis struct {
key string // 锁的key
expire int64 // 锁超时时间
status bool // 上锁成功标识
cancelFun context.CancelFunc // 用于取消自动续租携程
redis redis.Client // redis句柄
}
// NewDistributeLockRedis 初始化锁实例
func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
return &DistributeLockRedis{
key : key,
expire : expire,
}
}
// TryLock 尝试获取锁并启动看门狗
func (dl *DistributeLockRedis) TryLock() (err error) {
if err = dl.lock(); err != nil {
return err
}
// 创建带取消功能的上下文
ctx, cancelFun := context.WithCancel(context.Background())
dl.cancelFun = cancelFun
// 启动后台协程进行自动续期
dl.startWatchDog(ctx)
dl.status = true
return nil
}
// lock 底层竞争锁逻辑
func (dl *DistributeLockRedis) lock() error {
// 使用 SET key value NX EX seconds 保证原子性
res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire))
if err != nil {
return err
}
if res == "OK" {
return nil
}
return fmt.Errorf("lock failed")
}
// startWatchDog 守护协程,自动续期
func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
// 此处省略 safeGo 包装,实际应使用安全的协程调度函数
go func() {
for {
select {
case <-ctx.Done():
return // 收到取消信号,退出续期
default:
// 只有处于锁定状态才续期
if dl.status {
// 调用 Redis EXPIRE 命令续期
_, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire))
if err != nil {
return
}
}
// 续期间隔为总过期时间的一半
time.Sleep(time.Duration(dl.expire/2) * time.Second)
}
}
}()
}
// Unlock 释放锁,同时关闭看门狗
func (dl *DistributeLockRedis) Unlock() (err error) {
// 第一步:必须优先取消后台续期协程
if dl.cancelFun != nil {
dl.cancelFun()
}
var res int
if dl.status {
// 第二步:执行 DEL 命令释放Redis锁
res, err = redis.Int(dl.redis.Do(context.Background(), "Del", dl.key))
if err != nil || res != 1 {
return fmt.Errorf("释放锁失败")
}
dl.status = false
}
return nil
}
使用方法
在实际业务逻辑中,可通过defer语句确保锁资源的最终释放,即使在发生panic时看门狗也会被正确终止。
func Foo() error {
key := "my_task_key"
// 1. 创建锁实例,设置过期时间为10秒
dl := NewDistributeLockRedis(key, 10)
// 2. 争抢锁
err := dl.TryLock()
if err != nil {
return err // 没抢到锁直接返回
}
// 3. 确保业务结束(或异常)时一定释放锁
defer dl.Unlock()
// 4. 执行业务逻辑
DoJob()
return nil
}
上述代码展示了Golang中实现可重入分布式锁的标准范式,重点在于利用协程的生命周期管理与主业务逻辑绑定,确保服务异常时资源能安全回归。
以上就是Golang实现可重入锁的详细内容,更多关于Golang可重入锁的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









