redis公平锁必须用zset+lua脚本,因zset支持按时间戳排序和o(log n)首元素查询,配合lua原子化实现“入队→判首→加锁”流程,避免插队;lpush/lpop无序、incr不绑定锁状态均不可行。

Redis本身不提供原生的公平锁语义,必须靠Lua脚本+有序数据结构(如ZSET)手动实现排队逻辑。直接用SETNX或SET ... NX EX只能做非公平锁——谁抢到算谁,无法保证请求顺序。
为什么公平锁必须用ZSET + Lua 脚本?
公平性本质是「先到先得」的队列行为,而Redis没有内置FIFO锁队列。只有ZSET能按时间戳(或自增序号)排序并支持O(log N)插入/查首元素,配合Lua脚本才能把「入队→检查是否队首→尝试加锁→失败则等待」这一串动作原子化。任何拆成多条命令的操作都会在中间被其他客户端插队。
-
LPUSH+LPOP不行:列表无排序能力,无法判断自己是不是下一个该拿锁的 - 单纯用
INCR生成序号也不行:没和锁状态绑定,无法防止已超时的请求还占着队列位置 - 必须用
ZADD KEYS[2] ARGV[3] ARGV[2]把线程唯一标识(如threadId)按时间戳(ARGV[3])存入ZSET,后续靠zrange KEYS[2] 0 0取队首比对
tryAcquisition Lua脚本里最关键的三段逻辑
这个函数决定当前线程能否立刻拿到锁,不是简单SET,而是分路径处理:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 锁空闲(
exists KEYS[1] == 0):直接设锁 + 写入HSET+ 设置过期 + 把自己ZADD进等待队列末尾 → 返回nil表示成功 - 锁被占用但自己是队首(
zrange KEYS[2] 0 0 == ARGV[2]):说明前面没人排队,可以立即上锁 → 执行HSET+PXPIRE+ZREM→ 返回nil - 其他情况:返回
pttl KEYS[1],让客户端知道还要等多久(用于await休眠),避免忙等
注意:KEYS[1]是锁主键(如lock:order_123),KEYS[2]是对应等待队列(如queue:order_123),两者必须成对出现,不能复用同一key。
解锁时为什么不能只DEL锁key?
公平锁的解锁必须同时清理两处状态:锁本身 + 等待队列中可能残留的自己。如果只删KEYS[1],而没从KEYS[2]中移除线程标识,下次有人入队时会发现队列里有“幽灵节点”,导致永远轮不到真正的新请求。
- 正确做法是在Lua里先
ZREM KEYS[2] ARGV[1](删队列),再DEL KEYS[1](删锁),且整个过程要校验ARGV[1]是否真持有锁(用HGET比对) - 漏掉
ZREM是生产环境最常踩的坑——压测时看着QPS上去了,但实际请求在队列里积压不前,监控显示zcard queue:xxx持续增长却无人出队 - Redisson的
unlockInnerAsync脚本就包含这一步,别自己手写时省略
公平锁真正的复杂点不在加锁,而在「等待唤醒」机制:客户端收到锁释放通知后,必须重新走一遍tryAcquisition流程,而不是直接认为自己能拿锁。因为网络延迟或重试间隙里,可能已有新请求插队进了队列头部——所以每次唤醒后都得用Lua重新查一次zrange ... 0 0。










