直接用 time.sleep 测不出锁续期问题,因其无法精确观测 ttl 是否刷新或覆盖续期中途崩溃等边界;应通过检查 ttl、模拟租约失效或 mock renewer 接口控制续期行为来白盒验证。

为什么直接用 time.Sleep 测不出锁续期问题
分布式锁的续期(renewal)依赖后台 goroutine 定期调用 Extend 或类似接口,而测试时若只靠 time.Sleep 等待,容易错过竞态窗口:续期 goroutine 可能还没启动、已被调度延迟、或在测试断言前刚执行完。真实场景中,续期失败往往表现为锁提前过期(其他客户端抢锁成功),但单测里看不到这个“外部抢入”行为。
关键不是“等多久”,而是“怎么观测续期是否生效”。你需要主动检查锁的 TTL 是否被刷新,或模拟租约失效来触发续期失败路径。
- Redis 实现(如
redis/go-redis+github.com/go-redsync/redsync)中,锁值通常含随机 token 和过期时间戳,TTL 可通过PTTL命令查 - etcd 实现(如
go.etcd.io/etcd/client/v3)中,lease 的剩余 TTL 可通过LeaseTimeToLive获取 - 不要依赖日志或 sleep 后重查 key 存在性——这既不精确,也无法覆盖“续期中途崩溃”的边界
如何用 mock 控制续期行为做白盒验证
把续期逻辑抽成可注入的接口(例如 Renewer),测试时用 mock 替换真实 Redis/etcd 调用,就能精确控制每次续期的返回结果。
示例结构:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
// 锁实例持有 renewer
type DistributedLock struct {
client LockClient
renewer Renewer
}
type Renewer interface {
Extend(ctx context.Context, lockKey, token string, ttl time.Duration) (bool, error)
}
- mock
Extend第一次返回true,第二次返回false,验证锁是否按预期触发释放回调或 panic - mock 返回
context.DeadlineExceeded,检查超时路径是否 clean up lease - 避免在 mock 中 sleep —— 改用 channel 控制时序,比如
renewCh := make(chan struct{}, 1),主流程select { case ,测试端发信号驱动步骤
用真实 Redis + redis-cli --raw 手动验证续期 TTL
集成测试阶段,需要确认续期真的写回了服务端。别信代码里的 “extend called”,要亲眼看到 key 的 TTL 在增长。
步骤(以 redis-cli 为例):
- 启动锁并获取成功后,立刻执行:
redis-cli --raw PTTL my:lock:key,记下初始 TTL(比如 29980) - 等待 5 秒,再执行一次:
redis-cli --raw PTTL my:lock:key,若续期生效,TTL 应接近原始值(如 29950,而非跌到 24950) - 手动 kill 续期 goroutine(比如用
runtime.Goexit()注入 panic),再观察 TTL 是否持续下降直至归零 - 注意:Redis 集群模式下,
PTTL必须打到对应 slot 的节点,否则报MOVED;单节点直连最稳
释放逻辑里最容易漏掉的两个点
释放不是简单删 key,尤其在 Redis 场景下,必须用 Lua 脚本保证“判断 token + 删除”原子性。测试时常见疏忽:
- 没校验
DEL返回值:脚本返回 1 表示删除成功,0 表示 token 不匹配(别人已续期或抢锁),但很多实现忽略返回值,导致误以为释放成功 - 释放时没传原始 token:从 context 或结构体里取错字段,比如用了
lock.Token却实际存的是lock.token(大小写/导出问题),Lua 脚本比对永远失败 - etcd 场景下,
Revokelease 后立即调用Grant复用 same ID 是危险的——新 lease 可能被其他客户端绑定,应确保 lease ID 全局唯一且不复用
真正难测的不是“能不能释放”,而是“释放有没有被并发干扰绕过”。这类问题只能靠 chaos 测试:在释放前几毫秒,用另一个 client 强行 SET my:lock:key new_token EX 30 NX,看原 client 是否仍认为自己持有锁。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










