直接用Redis SetNX加锁不续期会丢锁,因业务执行时间可能超锁过期时间,导致锁被误释放;匿名函数用于安全捕获token、key、client启动看门狗续期,确保只续期自身持有的锁。

为什么直接用 redis.SetNX 加锁后不续期会丢锁
Redis 分布式锁靠 SET key value NX EX seconds 实现,但业务执行时间不确定,若超时释放,其他节点就可能抢锁——这不是并发问题,是锁生命周期和业务周期错配。匿名函数本身不解决续期,它只是让续期逻辑能绑定在锁持有期间运行,避免额外起 goroutine 时作用域丢失或变量捕获错误。
- 常见错误:在加锁成功后启动独立 goroutine 调用
redis.Expire,但没传入当前锁的唯一 value(随机 token),导致误删别人锁 - 更隐蔽的问题:goroutine 持有闭包变量(比如锁 key 或 client)被外部修改,续期时操作了错误实例
- 正确做法是把续期逻辑封装进匿名函数,并立即启动,且只依赖加锁时生成的 token 和 client 实例
如何用匿名函数启动看门狗并安全续期
看门狗本质是一个定时刷新锁过期时间的 goroutine,必须满足三个条件:只对自己加的锁续期、不干扰主业务流程、能及时退出。匿名函数在这里用来捕获加锁时的 token、key 和 client,避免参数传递遗漏或类型转换出错。
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 使用 Lua 脚本原子判断并续期
script := redis.NewScript(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("EXPIRE", KEYS[1], ARGV[2])
else
return 0
end
`)
result, _ := script.Run(ctx, client, []string{lockKey}, token, "30").Result()
if result != int64(1) {
return // 锁已丢失,退出续期
}
case <-done: // 主业务完成信号
return
}
}
}()- 必须用 Lua 脚本保证“读值 + 续期”原子性,不能先
Get再Expire,否则中间锁可能被别人覆盖 -
done是一个chan struct{},由主业务结束后 close,用于优雅停止看门狗 - 续期间隔建议设为锁 TTL 的 1/3~1/2(如锁 30s,每 10s 续一次),太频繁增加 Redis 压力,太慢有丢锁风险
为什么不用 time.AfterFunc 替代 time.Ticker
time.AfterFunc 是单次延迟执行,而续期必须持续进行,直到锁释放。用它写成递归调用看似简洁,但容易因 panic 或未处理 error 导致续期链断裂,且无法统一响应 done 通道。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 典型陷阱:在
AfterFunc回调里再调AfterFunc,一旦某次续期失败(如网络超时),后续就不会触发 -
Ticker可以配合select多路复用,天然支持退出信号,且每次 tick 都是干净的上下文 - 如果硬要用
AfterFunc,必须在外层包一层循环 + recover,反而更重,得不偿失
实际部署时最容易被忽略的两个点
本地测试跑得通,上线后偶发丢锁,大概率栽在这两处:
立即学习“go语言免费学习笔记(深入)”;
- Redis 客户端连接池配置不合理:
MaxIdleConns和MaxActiveConns过小,续期请求排队,导致续期延迟超过 TTL,锁自动失效 - 没有校验 Lua 脚本返回值:
script.Run返回nil不代表成功,要检查result是否等于int64(1),否则静默失败,看门狗继续跑却没真正续上
续期不是“开了 goroutine 就万事大吉”,它和主业务共享同一个 Redis 连接池,也受网络、序列化、超时配置影响。token 必须是全局唯一且不可预测的字符串(推荐 uuid.NewString()),不能用时间戳或自增 ID。

















