redis互斥锁正确起点是set(key,val,['nx','px'=>ms]),必须原子执行判断不存在、写入、设过期;禁用裸setnx或get+set;value须唯一标识,px需大于p99耗时,解锁须lua校验,加锁后需二次get防重复查库。

redis.set() 带 NX + PX 参数才是互斥锁的正确起点
裸用 SETNX 或先 GET 再 SET 都会引入竞态窗口,根本挡不住击穿。真正能原子性“判断不存在 + 写入 + 设过期”的只有 redis.set(key, value, ['NX', 'PX' => 30000]) 这种调用方式。
常见错误包括:
- 只传
NX不带PX→ 进程崩溃后锁永远残留,后续所有请求卡死 - value 写死为
"1"或true→ 无法校验锁归属,释放时可能误删别人持有的锁 - 过期时间设成 1 秒 → 实际重建缓存耗时 800ms,锁提前释放,多个线程同时查库
过期时间必须略大于 P99 重建耗时(比如实测 1200ms,就设 PX 3000),且 value 必须是随机唯一标识,如 uniqid('', true) 或 random_bytes(16)。
为什么必须两次检查缓存(double-check)
加锁成功后不立刻查库,而是再 $redis->get($key) 一次——这是防止“锁排队间隙已有线程写完缓存”的关键动作。漏掉这步,高并发下多个线程会在毫秒级内先后拿到锁、重复执行 DB 查询。
典型流程是:
- 第一次
get:缓存空 → 进入加锁逻辑 -
set($mutexKey, $secret, ['NX', 'PX' => 30000])成功 → 拿到锁 - 第二次
get:立刻再查一遍原 key → 若此时已有值,直接返回,不查库 - 仅当第二次仍为空,才查库、序列化、
setex写缓存
这个二次检查不是可选项,是避免 DB 重复压力的硬性门槛。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
LUA 脚本释放锁才能避免误删
用 DEL $mutexKey 直接删锁?危险。A 拿锁还没执行完,B 超时后也拿到锁,A 执行 DEL 删掉的是 B 的锁,B 后续操作就会和 C 同时查库。
必须用 Lua 封装校验 + 删除,保证原子性:
-- KEYS[1] = mutexKey, ARGV[1] = secret
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
PHP 中调用:$redis->eval($luaScript, 1, $mutexKey, $secret)。任何绕过该脚本的释放方式,都等于没锁。
加锁失败后不该轮询 DB,而应等待并重试 get
看到 set(..., ['NX', ...]) 返回 false,第一反应不是“我来查库”,而是“等别人写好”。立刻降级查 DB 会把击穿变成雪崩。
合理做法是:
- 短延时(如
usleep(10000),即 10ms) - 最多重试 3 次
- 之后再
get一次缓存,仍空才考虑降级(如查 DB 或返回默认值)
注意:重试期间不要释放锁、不要新建连接、不要记录冗余日志——这些都会放大 Redis 压力。真正的难点不在加锁,而在“等”这个动作怎么等得既轻量又可靠。










