必须用SET key value NX PX ms原子命令加锁,避免SETNX+EXPIRE拆分导致死锁;解锁须用Lua脚本校验value再删;锁粒度按业务ID细化;锁超时应基于P99耗时×1.5并配合续期。

加锁用 SET key value NX PX ms,别拆成 SETNX + EXPIRE
网络往返次数直接拉低吞吐量。拆成两步时,如果 SETNX 成功但 EXPIRE 失败(比如网络抖动或 Redis 瞬间满载),就会留下一个永不过期的锁——后续所有请求全卡住,吞吐归零。
- 必须用单条原子命令:
SET lock:order:123 8f4a7b2d PX 30000 NX -
PX(毫秒级)比EX(秒级)更精准,尤其在超时时间要压到 10–20 秒以内的场景 - 客户端 SDK 如
redis-py的.set(key, value, nx=True, px=30000)就是安全封装,别自己拼命令
解锁必须走 Lua 脚本,不能 GET 后再 DEL
“先查再删”在高并发下必然出错:A 查到锁是自己的,还没来得及删,锁过期了;B 立刻抢到锁;A 还按老值执行 DEL,就把 B 的锁干掉了——业务数据开始错乱,而你看到的只是吞吐波动或偶发失败。
- 固定脚本(必须用):
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end - 调用时传入
lock:order:123和当初设的client_id(比如 UUID),缺一不可 - 别图省事写个
if redis.get(...) == ...: redis.delete(...)—— 这不是优化,这是埋雷
锁粒度要细,别一把锁锁整个商品库
用 lock:product 这种粗粒度键,所有商品更新都排队,QPS 直接被压到个位数。实际热点往往集中在几个 SKU,其余冷数据白白等锁。
- 按业务 ID 分片:锁键写成
lock:product:10086、lock:product:20099,互不影响 - 对用户维度操作,用
lock:user:789;订单类用lock:order:5566,和资源强绑定 - 如果真要保护一组资源(如“某店铺全部商品库存”),优先考虑用 Redis 的
EVALSHA批量校验+更新,而不是串行加多把锁
锁超时 ≠ 业务超时,得配续期或预估余量
设锁 10 秒,但业务平均耗时 8.5 秒、P99 达 12 秒——那每 10 次请求就有 1–2 次锁提前释放,多个线程同时写 DB,吞吐看似上去了,数据一致性已经崩了。
- 公式参考:
锁过期时间 = max(业务 P99 耗时, 最小安全阈值) × 1.5,最小阈值建议不低于 5 秒 - 长任务(>5 秒)务必启用后台续期:拿到锁后立即起一个守护线程,每
过期时间 / 3(如 10 秒锁就每 3 秒续一次),用PEXPIRE延长 TTL - 续期前必须用 Lua 校验当前锁仍属于自己,否则可能续了别人的锁
锁本身不慢,慢的是误用。最常被忽略的点是:锁键没按资源隔离、解锁不用 Lua、超时拍脑袋定——这三处一错,性能数字看着还行,线上故障已倒计时。










