Redis单实例公平锁需用ZSET维护有序等待队列,通过Lua脚本原子执行“入队+校验队首”,结合Pub/Sub监听前序节点并辅以超时重检,确保FIFO顺序;禁用Redlock,因其破坏顺序性且性能差。
Redis 单实例下怎么实现公平性
redis 本身不提供队列式排队能力,set nx ex 是非公平的:谁先抢到谁赢,没有“先来后到”保障。要模拟公平锁,必须用有序集合(zset)维护等待队列,并配合 lua 脚本原子执行「入队 + 检查是否队首」两步。
常见错误现象:只靠 SET 加锁 + 客户端轮询重试 → 高并发下大量请求同时发现锁空闲,一拥而上,实际仍是抢锁,毫无公平可言。
- 加锁脚本必须把客户端唯一标识(如
uuid.NewString())按当前时间戳插入ZSET,例如 key 为{fairlock}:order(带 hash tag 防 Redis Cluster 路由分裂) - 紧接着调用
ZRANGE queue 0 0拿队首,比对是否为自己 —— 这一步不能拆成两次网络调用,否则中间可能被别人挤掉 - 若不是队首,不阻塞,直接返回失败;业务层应监听前一个节点(见下节),而非无脑重试
- 锁释放时,除了删主 key,还要从
ZSET中移除自己:ZREM queue client_id,否则队列持续膨胀
如何让等待者只监听前一个排队者
避免“羊群效应”——即锁释放时所有等待者同时被唤醒、一起抢锁。正确做法是每个客户端只订阅紧邻自己的上一个排队者(类似 ZooKeeper 临时顺序节点的 Watch 机制)。
使用场景:高并发短任务(如库存扣减)+ 严格 FIFO 要求(如金融类事务排序)。
- 入队后,用
ZRANK queue client_id获取自己序号rank - 若
rank > 0,则取ZRANGE queue (rank-1) (rank-1)拿前一个 client_id,再用PUBLISH订阅对应频道(如fairlock:wait:) - 前一个 client 释放锁时,主动
PUBLISH fairlock:wait:,触发后续者重试"released" - 注意:Redis Pub/Sub 不保证消息送达,必须搭配超时重检(比如每 500ms
ZRANGE queue 0 0看自己是否已到队首)
为什么 Redlock 不适合公平锁
Redlock 的设计目标是容错与可用性,不是顺序保障。它要求在多数 Redis 实例上成功加锁,但各实例之间无状态同步 —— A 在实例 1 排第 1,B 在实例 2 排第 1,最终谁能拿到锁取决于网络延迟和时钟漂移,天然破坏 FIFO。
性能影响:Redlock 需要至少 3–5 个独立 Redis 实例,每次加锁要发 N 次请求、做耗时校验,平均延迟比单实例高 3–5 倍,且无法与 ZSET 队列协同。
- 如果你需要公平性,就别用 Redlock;它解决的是“主节点挂了怎么办”,不是“谁该先执行”
- 真正需要跨机房容灾的公平锁,应考虑 Etcd(CAS + Lease + Watch 天然支持顺序等待)或自研基于 Raft 的锁服务
- 单机房内,一个带持久化配置的 Redis 主节点 + 正确的
ZSET脚本,已足够支撑万级 QPS 的公平调度
续期逻辑必须绑定队列位置
普通分布式锁续期只看 key 是否还存在;公平锁续期还必须确认自己仍在队列中、且仍为队首 —— 否则可能给一个早已被挤出队列的 client 续期,导致锁错乱。
容易踩的坑:启动 goroutine 定期调 EXPIRE,但没检查 ZRANK,结果锁值还在,人早就不是队首了。
- 续期前先
ZRANK queue client_id,返回 -1 表示已被踢出队列,立即停止续期并清理本地状态 - 只有
rank == 0时才允许续期主 key 的 TTL,否则只续期自己在ZSET中的 score(即刷新等待时间戳) - 业务函数 return 前必须显式 cancel 续期 goroutine 的
context,否则变成“幽灵续期”:锁早被别人拿走,你还在给一个无效 score 续命
公平锁最易被忽略的一点:它不解决「持有者崩溃后队列如何推进」。ZSET 里的 client_id 只是字符串,没绑定会话生命周期。真正在生产环境用,得配合心跳上报或租约机制(比如把 client_id 关联到 Etcd lease),否则一个假死 client 会卡住整个队列。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











