直接用 set key value ex seconds nx 无法支撑长任务锁,因为业务执行超时会导致锁自动过期,其他协程可能同时进入临界区,造成“锁过期但持有者仍在执行”的时间窗口问题。

为什么直接用 SET key value EX seconds NX 无法支撑长任务锁
Redis 分布式锁最简实现是用 SET 命令加过期时间,但一旦业务逻辑执行时间超过 EX 设置的秒数,锁自动失效,其他协程可能同时进入临界区。这不是“锁没生效”,而是“锁过期了但持有者还在干活”——典型的时间窗口问题。
自动续期的本质,是在锁持有期间,周期性地用 GETSET 或 Lua 脚本延长 key 的 TTL,前提是:当前持有者仍是自己(即 value 匹配)。闭包在这里的关键作用是捕获并持续持有这个唯一标识(比如随机 token),避免全局变量或参数传递污染。
- 必须用唯一、不可预测的 value(如
uuid.NewString()),不能用固定字符串或进程 ID - 续期操作本身必须原子:先校验 value 是否匹配,再更新 TTL,否则出现“A 续了 B 的锁”
- 续期 goroutine 必须能被安全取消,否则任务结束锁已释放,续期仍在刷 TTL,导致锁滞留
如何用闭包封装续期逻辑并绑定 token 和 cancel 控制
闭包不是炫技,是把 token、client、key、cancel 这几个生命周期强关联的值封在一起,让续期函数无需反复传参,也不依赖外部状态。
典型结构是返回一个 func(),内部启动 goroutine 执行定时续期,并通过传入的 context.Context 感知锁是否已被释放或超时:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
func newAutoRenewer(client *redis.Client, key, token string, ttl time.Duration) func() {
return func() {
ticker := time.NewTicker(ttl / 3) // 每 1/3 过期时间续一次
defer ticker.Stop()
for {
select {
case
<p>注意:<code>ctx</code> 需由调用方传入(比如带 timeout 的 <code>context.WithTimeout</code>),并在获取锁成功后启动该闭包返回的函数。</p>
<h3>容易被忽略的三个断连与竞态细节</h3>
<p>生产环境出问题,往往不在主流程,而在边界场景。这三个点不处理,自动续期会静默失效:</p>
-
redis.Client连接断开时,script.Run可能返回redis.Nil或网络错误,但不会 panic;必须显式检查err != nil并退出续期 goroutine - 如果业务函数 panic,而 defer 中没做
cancel(),续期 goroutine 会一直运行到进程退出,不断给已失效的 key 续期 - 使用
time.Ticker时,若续期耗时 > tick 间隔(比如 Redis 延迟突增),后续 tick 会被跳过,形成“续期毛刺”。应改用time.AfterFunc链式调用,确保每次续期完成后再安排下一次
要不要用 Redlock?大多数场景不需要
Redlock 是为多 Redis 节点故障模型设计的,但它依赖严格的时间同步,且在 GC STW、NTP 调整等现实条件下难以保证正确性。单节点 Redis + 正确的自动续期 + 合理的超时设置(比如 TTL = 预估最大执行时间 × 2),对绝大多数微服务场景已足够可靠。
真正关键的是:锁的粒度是否合理、业务是否幂等、续期失败后是否有降级路径(比如记录告警 + 强制终止任务)。闭包只是让续期逻辑更内聚,解决不了架构层面的取舍。










