Redis 分布式锁
Redis 分布式锁常用于控制分布式环境下的互斥访问。理解它时不能停留在“会用 SETNX”,还要关注锁的安全性、释放正确性、过期续期、Redisson 和适用边界。
1. 为什么需要分布式锁
单机锁只能控制单个 JVM 内部线程互斥,例如 synchronized、ReentrantLock。
分布式场景下,请求可能落到多个服务实例:
服务 A 实例 1
服务 A 实例 2
服务 A 实例 3这时本地锁无法互斥,需要一个外部共享组件保存锁状态。
2. 正确的基础加锁方式
推荐使用一条 Redis 命令同时完成“加锁 + 设置过期时间”:
SET lock:order:1 unique-value NX EX 30含义:
NX:key 不存在时才设置,保证互斥。EX 30:设置 30 秒过期时间,避免客户端宕机后死锁。unique-value:唯一值,用于安全释放锁。
不要用下面这种非原子写法:
SETNX lock value
EXPIRE lock 30如果 SETNX 成功后服务宕机,EXPIRE 没执行,就可能死锁。
3. 为什么 value 要唯一
释放锁时必须确认:
这把锁是我加的,才能由我释放否则可能出现:
- 线程 A 获取锁,业务执行超时,锁自动过期。
- 线程 B 获取同一把锁。
- 线程 A 执行结束,直接
DEL lock。 - 线程 A 删除了线程 B 的锁。
所以 value 应该是唯一标识,例如:
clientId + threadId + randomUUID4. Lua 脚本释放锁
释放锁需要“判断 value + 删除 key”原子执行。
Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end不能先 GET 再 DEL,因为两条命令之间锁可能过期并被别人获取。
5. 锁过期问题
锁必须设置过期时间,但过期时间也会带来问题:
业务执行时间 > 锁过期时间结果是业务还没执行完,锁已经被其他线程获取。
解决思路:
- 合理估算锁 TTL。
- 业务尽量短小。
- 使用自动续期机制。
- 对关键资源增加幂等和状态校验。
6. Redisson 看门狗机制
Redisson 的可重入锁默认有看门狗机制。
大致逻辑:
获取锁成功
-> 设置默认过期时间
-> 后台定时续期
-> 业务执行完成后主动释放只要持锁客户端还活着,看门狗会持续续期,避免业务未执行完锁就过期。
但要注意:
- 如果服务发生长时间 STW,续期可能不及时。
- 如果网络分区,Redis 侧可能认为锁已过期。
- 分布式锁不能替代业务幂等。
7. RedLock 简介和争议
RedLock 是 Redis 官方文档中提出的多 Redis 节点分布式锁算法。
大致思路:
向多个独立 Redis 节点尝试加锁
超过半数成功,并且总耗时小于锁有效期,则认为加锁成功争议点:
- 它比单实例锁复杂。
- 对时钟、网络延迟、故障模型有要求。
- 如果业务需要强一致互斥,通常更适合使用数据库约束、ZooKeeper、etcd 等一致性组件。
理解它的边界时要谨慎:Redis 锁适合效率型互斥,不适合承载强一致正确性的唯一防线。
8. 分布式锁适用边界
适合:
- 防止重复提交。
- 控制定时任务多实例并发。
- 热点资源短时间互斥。
- 缓存重建互斥。
不适合单独承担:
- 金融级强一致扣款。
- 复杂长事务。
- 无幂等保护的数据修改。
关键业务要配合:
- 数据库唯一索引。
- 状态机。
- 幂等表。
- 乐观锁版本号。
9. 理解检查
为什么不能只用 SETNX?
因为 SETNX 和 EXPIRE 分开执行不是原子的,可能导致死锁。应该用 SET key value NX EX seconds。
为什么释放锁要用 Lua?
因为判断 value 和删除 key 必须原子执行,避免删除别人的锁。
Redis 分布式锁安全吗?
它能解决很多效率型互斥问题,但在网络分区、主从切换、长时间 GC 等场景下仍有风险。强一致场景不能只依赖 Redis 锁。
10. 总结
Redis 分布式锁的最低正确姿势是:SET NX EX 原子加锁、唯一 value 标识持有者、Lua 原子释放、合理 TTL 和续期机制。真正重要的业务还要有幂等和数据库层兜底。