如何使用 Redis 快速实现分布式锁?
0. 引言
Redis 分布式锁是互联网最常用的互斥方案:快(微秒级)、简单(一条命令)、生态成熟(Redisson)。但"简单"背后藏着不少坑:误删他人锁、锁自动过期业务未完成、主从切换丢锁、RedLock 的争议……本文从手工实现演进讲到 Redisson 的正确姿势,帮你写出经得起面试与生产考验的锁。
1. 演进史:从两条命令到一条命令
1.1 错误写法:SETNX + EXPIRE(非原子)
bash
SETNX lock:order:1001 uuid-001 # ① 加锁
EXPIRE lock:order:1001 30 # ② 设置过期(独立命令)问题:① 与 ② 之间进程崩溃 → 锁永不过期 → 死锁。
1.2 正确写法:SET NX EX(原子)
bash
SET lock:order:1001 uuid-001 NX EX 30一条命令原子完成"不存在才设置 + 过期时间",加锁阶段的问题解决。
1.3 释放:Lua 保证"只释放自己的锁"
lua
-- 释放锁前先校验 value 是否是自己(防止误删他人锁)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end为什么必须 Lua?
GET判断与DEL之间若并发,可能删掉其他线程刚抢到的锁(A 的锁过期后 B 拿到锁,A 的 DEL 把 B 的锁删了)。Lua 脚本在 Redis 中原子执行,杜绝竞态。
2. 核心问题:锁过期而业务未完成
图表渲染中…
解法:看门狗(Watchdog)续期——持有锁期间定期延长过期时间(如每 10 秒续到 30 秒),业务结束显式释放;进程崩溃则续期停止,锁自然过期。
java
// Redisson 默认行为:看门狗每 10s 续期,锁默认 30s 过期
RLock lock = redisson.getLock("lock:order:1001");
lock.lock(30, TimeUnit.SECONDS); // 手动指定 leaseTime 时看门狗不生效
try {
// 业务逻辑
} finally {
lock.unlock(); // 显式释放
}3. 完整正确姿势清单
| 要点 | 说明 |
|---|---|
| 原子加锁 | SET key value NX EX(或 Redisson API) |
| 唯一标识 | value 用 UUID/线程标识,释放时校验,防误删 |
| 原子释放 | Lua 脚本(GET 校验 + DEL) |
| 自动过期 | 防死锁;配合看门狗续期防"业务未完成锁先过期" |
| 可重入 | Redisson 用 Hash 结构 + 计数实现重入 |
| 等待机制 | 未抢到锁时阻塞等待(tryLock 支持超时),而非轮询风暴 |
| 时钟回拨 | 依赖 EX 过期时间,服务器时钟大幅回拨会影响锁生命周期(生产需 NTP 守护) |
4. 主从切换丢锁与 RedLock 争议
4.1 丢锁场景
锁写入 master 后,master 宕机、数据未同步到 slave → slave 晋升为 master → 锁丢失,另一个客户端可再次加锁成功 → 互斥失效。
4.2 RedLock 方案与争议
RedLock(Redisson RedissonRedLock):对 N 个独立 Redis 实例(如 5 个)逐个加锁,多数派(≥3)成功且总耗时 < 锁过期时间 才算加锁成功。
- 支持者:分布式系统的多数派思想,抗单点;
- 反对者(Martin Kleppmann 等):分布式系统下没有全局时钟,无法证明"锁实际过期时间"绝对安全;GC 停顿期间锁可能同时失效;工程上反而引入更多复杂度;
- 官方回应(Salvatore Sanfilippo):承认理论边界,但认为实践可用;
- 结论:生产上绝大多数场景单主 Redis + 看门狗足够;要求绝对互斥(如资金扣减)应改用 ZooKeeper/etcd 锁(共识机制无此问题)。
5. 应用层兜底:fencing token
即使锁可靠,也要防"持锁者被 GC 暂停后继续执行过期操作"——给锁加单调递增的 token,写数据时带 token,服务端校验 token 是否最新(旧 token 拒绝):
text
加锁成功 → token = 42(Redis INCR 全局计数)
释放后 B 加锁 → token = 43
A(GC 恢复)带着 token=42 提交写操作 → 服务端拒绝这是"锁的正确性最终要靠业务幂等/版本号兜底"的经典体现——分布式锁保证互斥,幂等保证正确。
6. 面试 Q&A
- SETNX 和 SET NX EX 的区别? 后者原子 = NX + 过期;前者需配合 EXPIRE,两步非原子会死锁;
- 为什么释放锁要用 Lua? 保证"判断是自己 + 删除"原子执行,防止误删他人锁;
- 锁过期了业务没执行完怎么办? 看门狗续期(Redisson 自动),或业务内主动续期;
- Redis 锁和 ZK 锁怎么选? 性能场景 Redis;绝对互斥场景 ZK/etcd(无主从丢锁问题);
- RedLock 可靠吗? 理论上有争议(无全局时钟),工程上单主 + 看门狗 + 幂等兜底是主流。
7. 小结
- 加锁:
SET key uuid NX EX原子命令;释放:Lua 校验 + 删除; - 看门狗续期解决"锁过期业务未完成";唯一标识防误删;
- 主从切换可能丢锁 → 极端场景用 RedLock(有争议)或 ZK/etcd;
- 终极防线:业务幂等 + token 校验,锁只是第一道闸。
下一章进入服务治理篇:如何理解 RPC 远程服务调用?