直接用 redis.client 实现分布式锁容易出错,因为必须同时保障加锁原子性(set key value nx ex seconds)、释放时 token 校验(lua 脚本比对 value)、以及合理续期机制,任一环节缺失都会导致锁失效、误删或死锁。

为什么直接用 redis.Client 实现分布式锁容易出错
因为 SET 命令的原子性、过期时间设置、锁释放校验这三件事,手工拼凑极易漏掉任一环节。比如只用 SET key value EX 10 而没加 NX,就可能覆盖别人持有的锁;又或者用 DEL 直接删 key,结果删了别人刚续上的锁。
常见错误现象包括:多个服务同时进入临界区、锁自动过期后被误删、续期时判断失效导致提前释放。
- 必须用
SET key value NX EX seconds保证获取锁的原子性 - 释放锁必须用 Lua 脚本比对 value(即唯一 token),不能靠业务层判断后
DEL - 锁超时不是“保险丝”,而是兜底机制,业务仍需配合主动释放或续期
选 go-redsync 还是手写基于 redis-go 的封装
go-redsync 封装了 Redlock 算法,适合跨多个 Redis 实例的高可用场景,但多数中小系统只连单个 Redis,它反而引入冗余逻辑和额外网络开销;而纯 redis-go + 自定义封装更轻量、可控性强,也更容易调试。
如果你的 Redis 是单节点或主从(不跨哨兵/集群分片),推荐直接基于 redis.Client 封装,关键点如下:
- 生成唯一 token 用
uuid.NewString(),避免用时间戳或 PID(易冲突) - 加锁用
client.Set(ctx, key, token, expiration),并检查返回值是否为"OK" - 解锁必须走 Lua:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 续期用
client.Expire(ctx, key, newExpiry),但要先GET校验 token 是否仍属于自己
redigo 和 redis-go 在锁实现中的实际差异
两者都能用,但 redis-go(即 github.com/go-redis/redis/v9)的 API 更符合 Go 习惯,支持 context 取消、pipeline、更清晰的错误分类(如 redis.Nil 表示 key 不存在);redigo 需手动管理连接池、拼接命令字符串,容易在 Lua 脚本参数传递时出错。
例如用 redis-go 执行解锁脚本:
script := redis.NewScript(`if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end`)
result, err := script.Run(ctx, client, []string{key}, token).Result()
而 redigo 需显式 Do 调用,且参数类型易错(比如把 string 当 []interface{} 传)。
-
redis-go的SetNX方法可替代原始SET ... NX,语义更明确 -
redigo的连接复用逻辑需自行把控,锁操作若混用连接可能引发 pipeline 冲突 - 两者都支持连接池,但
redis-go默认配置更合理(如MinIdleConns可设为 5)
分布式锁在真实服务中常被忽略的边界条件
锁本身只是工具,真正难的是怎么嵌入业务流程。比如 HTTP handler 中加锁后 panic,锁没释放;又或者 goroutine 拿到锁后调用下游超时,锁已过期但业务还在跑。
- 务必用
defer unlock(),且 unlock 函数内部要忽略redis.Nil错误(锁可能已被自动释放) - 不要在锁内做不确定耗时的操作(如调用外部 HTTP、未设 timeout 的 DB 查询)
- 如果业务需要长时间持有锁,必须启用续期机制,并监听 context Done() 主动退出
- 测试时一定要模拟网络分区——比如 kill 掉 Redis 后观察客户端是否快速失败,而不是卡死在
SET上
最麻烦的不是写对锁,而是确认什么时候该用锁、什么时候该用数据库唯一约束或消息队列削峰。锁只是最后防线,不是银弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











