Redis单实例无法天然实现公平锁,必须依赖客户端主动排队(ZSET维护有序队列)和Lua脚本原子性判首+加锁,否则SETNX+EX仅按请求到达顺序竞争,无法保证先到先得;网络延迟、崩溃或续期失败均会导致顺序错乱。

单 Redis 实例无法天然实现公平锁,必须靠客户端主动排队 + Lua 原子调度,否则“先到先得”只是幻觉——网络延迟、客户端崩溃、续期失败都会让顺序乱掉。
为什么 SETNX + EX 不能保证公平性
SETNX 只判断 key 存不存在,不关心“谁在队列里等”。多个客户端并发抢锁时,谁的请求先抵达 Redis 就可能先成功,和提交顺序无关。更糟的是,如果 A 加锁后卡住、B 和 C 同时重试,B 拿到锁、C 等待,但 A 恢复后直接续期,就把 C 的等待位置跳过了。
- 公平性不是 Redis 自带能力,是业务逻辑层要补足的约束
- 真实场景中,你得自己维护一个有序队列(比如用
ZSET),把 client ID 和入队时间戳一起存进去 - 每次加锁前,必须用 Lua 脚本原子性地:入队 → 查自己是不是队首 → 是则设锁,否则返回排队中
- 若用
redis.call('ZRANGE', queue, 0, 0)拿到的不是自己,说明前面还有人,不能进临界区
用 ZSET 实现排队 + Lua 原子判首
核心是把“等待”显式化,而不是靠反复轮询重试。下面这段 Lua 是关键:
local queue = KEYS[1]
local holder = KEYS[2]
local client = ARGV[1]
local ttl = ARGV[2]
local now = tonumber(ARGV[3])
<p>-- 入队
redis.call('ZADD', queue, now, client)</p><p>-- 检查是否队首
local first = redis.call('ZRANGE', queue, 0, 0)
if first and first[1] == client then
-- 是队首,尝试加锁
if redis.call('SET', holder, client, 'NX', 'PX', ttl) == 'OK' then
return 1 -- 成功获锁
else
return -1 -- 锁被别人占了(极小概率,但需处理)
end
else
return 0 -- 还在排队
end</p>
-
queue是 ZSET 键名,按 score(时间戳)自动排序 -
holder是实际锁 key,只由队首 client 设置 - 所有客户端都走同一段脚本,避免竞态;返回
1表示获锁,0表示排队中,-1表示队首但锁已被抢占(需退避重试) - 注意传入的
now必须是毫秒级时间戳,且各客户端时钟误差应控制在 TTL/10 内,否则排序失准
排队超时与主动出队怎么处理
没人能永远等下去。客户端必须自己管理等待生命周期,不能依赖 Redis 自动清理排队状态。
- 调用 Lua 后返回
0,就该启动一个定时器,比如每 500ms 查一次自己是否变成队首,超过总超时(如 30s)就放弃 - 放弃前必须用另一段 Lua 主动从
ZSET中移除自己:redis.call('ZREM', queue, client),否则队列会越积越长 - 别指望
EXPIRE清理 ZSET 成员——ZSET 本身不支持过期,得靠业务逻辑定期ZREMRANGEBYSCORE扫描陈旧 entry - 如果进程崩溃,残留的排队记录会一直卡位,所以建议给每个 client ID 绑定租约(比如加入 PID + 启动时间哈希),方便后台任务识别并清理“幽灵排队者”
Redis Cluster 下的 key 分片陷阱
如果你用的是 Redis Cluster,ZSET 和锁 key 必须落在同一个分片上,否则 ZRANGE 和 SET 跨节点执行,Lua 脚本直接报错。
- 强制它们路由到同一节点:给两个 key 都加上相同 hash tag,例如
{lock-queue}:order-123和{lock-queue}:holder-order-123 - 千万别写成
lock:queue:order-123和lock:holder:order-123,这会让它们大概率散落在不同节点 - go-redis/v9 默认开启 cluster 模式检测,遇到跨 slot 调用会 panic,错误信息是
CROSSSLOT Keys in request don't hash to the same slot,看到这个就得回头检查 key 命名 - 本地开发用单机 Redis 测试不出这个问题,上线前务必在集群环境跑通排队全流程
公平锁最难的不是代码怎么写,而是所有人对“公平”的理解是否一致——客户端时钟漂移、网络抖动、进程意外退出,都会让队列顺序和真实意图脱节。与其强求绝对公平,不如明确超时策略、做好残留清理、并在业务层接受“最多一次”的语义约束。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











