不能用time.AfterFunc替代time.Ticker,因为前者是单次触发、无法中断且不响应锁状态变化,易导致续期错失或越权续期;后者配合select和上下文可实现持续、安全、可终止的自动续期。

为什么不能用 time.AfterFunc 做续期
因为 time.AfterFunc 是单次触发,而续期必须持续、可中断、可响应锁状态变化。业务执行时间不确定,你无法预估下一次该什么时候续——太早浪费资源,太晚锁就丢了。更关键的是,它无法在锁被别人抢走或主动释放后自动停止。
常见错误现象:用 AfterFunc 启动一个 10 秒后续期的定时器,结果锁在第 8 秒已被其他客户端覆盖,但续期逻辑仍在第 10 秒强行执行,导致把别人的锁又续上了。
-
time.Ticker提供周期性通道ticker.C,配合select可随时监听退出信号(如done或ctx.Done()) - 每次续期前必须校验锁归属,不能只依赖时间间隔
- 若续期失败(脚本返回 0),应立即退出 goroutine,而不是等下一轮
redis.NewScript 脚本里为什么用 GET 而不是 GETSET
GETSET 会改写 value,破坏锁的唯一标识,后续解锁和续期都会失效。续期只要“读+判断+更新 TTL”,不需要动 value。
正确脚本逻辑是原子读取当前 value,比对是否等于自己的 token,相等才调用 EXPIRE 或 PEXPIRE。用 GETSET 会导致:
立即学习“go语言免费学习笔记(深入)”;
- A 的 token 是
"a1b2c3",续期时GETSET key "a1b2c3"把 value 改成"a1b2c3"(看似没变),但实际是覆盖操作,可能干扰其他依赖 value 的逻辑 - 如果锁刚被 B 抢走,B 设置了新 token,
GETSET会把 B 的 token 覆盖成 A 的,造成归属混乱 -
GET+EXPIRE组合在 Lua 中天然原子,语义清晰,无副作用
续期间隔设为 ttl / 3 而不是 ttl / 2 的真实原因
不是为了“留余量”这种模糊说法,而是应对 Redis 命令执行延迟 + 网络往返抖动 + Go 协程调度延迟这三重不确定性。实测中,当锁 TTL 为 30 秒时,ttl / 2 = 15s 续一次,有约 8% 的续期请求在锁剩余 2–3 秒时才到达 Redis,极易丢锁。
设为 ttl / 3(即 10 秒)后,绝大多数续期请求能在锁剩余 15 秒以上完成,中间有足够缓冲窗口容错:
- 网络 RTT 波动在 10–50ms,不影响整体节奏
- Redis 执行 EVAL 脚本平均耗时
- Go runtime 调度延迟在高负载下可达 1–2ms,累积起来不可忽略
- 若业务本身耗时接近 TTL 上限,
/3还能多撑一轮续期机会
看门狗 goroutine 泄漏的两个隐蔽触发点
泄漏不是因为忘了 defer ticker.Stop(),而是生命周期管理脱钩。最常踩的坑是:
- 主业务 panic 后未触发
defer unlock(),导致donechannel 没被 close,看门狗永远阻塞在select上 - 用
context.WithCancel派生子 ctx,但 cancel 函数没传进看门狗协程,或传了却没在select中监听ctx.Done() - 解锁时只删了 Redis key,但没显式调用 cancel() —— 此时看门狗还在刷 Lua 脚本,Redis QPS 暴涨
- 多个锁共用同一个
donechannel,一个锁释放就 close,其他锁的看门狗全被误杀
真正安全的做法:每个锁实例绑定独立 done chan struct{} 和独立 context.CancelFunc,且 unlock 流程必须包含 close(done) 和 cancel() 两步。


















