自研 Redis 锁容易翻车的地方
用 SET NX EX 加锁看起来只有一行代码,真正上线后暴露的问题却集中在几个固定位置:锁的粒度太粗、释放时误删别人的锁、TTL 与业务耗时对不上、集群下键落在不同槽、以及把锁当成强一致保证。加锁之前先把「锁谁、锁多久、失败怎么办」想清楚,比事后加班排查划算得多。
基本信息
| 适用平台 | Redis 2.6.12+(SET 支持 NX EX 后) |
|---|---|
| 加锁写法 | SET lock:res value NX EX ttl |
| 释放写法 | Lua 脚本比对 value 后删除 |
| 续期手段 | 看门狗线程定期延长 TTL |
| 集群注意 | 锁键与业务键需要落在同一个槽 |
六个高频坑
- 锁粒度:锁到资源 ID(如订单号),不要一把全局锁把所有请求串行化
- 重入:同一执行流需要重复加锁时要计数,否则自己把自己锁死
- 误删:释放必须校验持有者标识,不能直接 DEL
- 续期:TTL 要覆盖 P99 耗时,长任务配合看门狗续期
- 集群槽位:锁键与业务键用同一 hash tag,避免 CROSSSLOT
- 兜底:锁只降低并发概率,关键写入仍需唯一索引或版本号等强约束
安全释放与代际校验
安全释放与代际校验
# 加锁:value 用唯一标识(请求 ID + 线程 ID)
SET lock:order:42 req-8f2k NX EX 15
# 释放:Lua 脚本保证比对与删除原子执行
# if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end
# 代际编号:让下游拒绝过期持有者的写入
INCR lock:job:fence # 得到 7
# 携编号写入,存储层只接受更大的编号
UPDATE jobs SET data = '...', fence = 7 WHERE id = 42 AND fence < 7锁参数的取舍
| 参数 | 取小 | 取大 |
|---|---|---|
| TTL | 频繁出现双持有者 | 持锁方崩溃后等待时间长 |
| 续期间隔 | 额外开销与网络压力大 | 续期不及时导致锁提前失效 |
| 重试策略 | 请求容易直接失败 | 高并发下重试放大压力 |
| 锁粒度 | 键数量多、管理复杂 | 串行化严重、吞吐下降 |
常见折中是 TTL 十到三十秒配合看门狗续期:既避免崩溃后长时间无法恢复,又能覆盖较大范围的任务耗时。
Redis 锁不是强一致机制:在时钟漂移、进程长时间暂停、主从切换等场景下双持有者窗口始终存在,涉及资金或库存的写入必须有独立的最终校验。