Context不传递租约时间,仅传播取消信号;租约时间需显式传给锁实现,与Context超时独立配置,续期须脱离原Context生命周期。

Context 本身不传递租约时间,它只负责传播取消信号
很多人误以为 context.WithDeadline 或 context.WithTimeout 能“把租约时间传给分布式锁客户端”,其实不是。Context 的 Deadline() 返回的是本机当前 goroutine 的截止时间,和 Redis 或 Etcd 中锁的实际过期时间(TTL)没有自动同步关系。分布式锁的租约时间必须显式传给锁实现,Context 只用来协调“这个 goroutine 还值不值得等锁”。
用 WithTimeout 包裹加锁操作,但租约时间要单独传
典型错误是只依赖 Context 超时,却没给锁设置足够长的 TTL,导致锁提前释放;或者反过来,锁 TTL 很长,但 Context 很快超时,造成加锁失败却没人清理残留锁。
- 加锁前用
context.WithTimeout(ctx, 5*time.Second)控制「等待锁的最大时长」 - 调用锁客户端时,必须额外传入租约 TTL(比如
10 * time.Second),这个值通常要比 Context 超时长(留出网络延迟、执行时间余量) - 如果使用
redislock库,得手动设redislock.Options.TTL;用etcd/client/v3的Session,则传给clientv3.NewSession的TTL参数
示例(Redis 锁):
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) defer cancel() <p>// 注意:这里 10s 是锁在 Redis 中的存活时间,和上面 5s 不同 lock, err := redislock.Lock(ctx, "order:123", 10*time.Second)
租约续期(renewal)必须脱离原 Context 生命周期
一旦拿到锁,若业务处理可能超时,就得后台续期——但不能用原始 Context,否则 Context 一取消,续期 goroutine 就停了,锁直接过期。必须用 context.WithCancel(context.Background()) 启动独立生命周期。
- 续期逻辑应基于锁自身的健康状态(如 Redis 中 key 是否还存在),而不是原 Context 的
Done() - 常见坑:在
defer unlock()里启动续期,但 defer 执行时 Context 已关闭,续期协程无法安全退出 - 更稳的做法是:加锁成功后立即 spawn 一个不依赖用户 Context 的续期 goroutine,并用 channel 或原子变量控制其退出
Etcd 的 Lease + KeepAlive 更适配 Context 模式
Etcd 原生 Lease 机制比 Redis SETNX + EXPIRE 更契合 Context 语义:你可以用 clientv3.NewLease 创建带 TTL 的 lease,再用 lease.KeepAlive 返回的 channel 监听续约结果,同时把 Context 传给 KeepAlive 控制监听生命周期。
-
KeepAlive内部会自动重连和续期,Context 仅控制「监听这个 lease 是否还有效」 - 但注意:
KeepAlive返回的 stream channel 关闭 ≠ lease 过期(可能是网络抖动),需结合resp.TTL > 0判断 - 锁释放时,调用
lease.Revoke主动销毁 lease,避免依赖 TTL 自然过期
关键点就在这儿:租约时间不是 Context 的属性,而是锁资源本身的配置项;Context 只管“我等不等得及”,不管“锁能活多久”。这两者错配,是分布式锁失效最隐蔽的根源。

















