05.3 - 分布式锁实现
定位: 利用 Redis 实现跨进程/跨机器的互斥锁——保障多节点下同一时刻只有一台机器执行某个操作 面试高频度: ⭐⭐⭐⭐ 考查方式: SETNX 的正确用法、分布式锁要素、RedLock、看门狗机制、续期问题
一、这是什么?为什么需要它?
是什么
分布式锁是控制在分布式系统中多个节点互斥访问共享资源的一种机制。Redis 凭借其高性能和原子操作,是分布式锁的常用实现方案。
为什么需要 Redis 分布式锁?
二、原理拆解
2.1 分布式锁的三大要素
分布式锁的三大要素
- 互斥性:同一时刻只有一个客户端持有锁
- 安全性:不会发生死锁(最终释放)
- 容错性:锁服务可用(多数 Redis 节点存活)
为什么解锁要用 Lua 脚本? GET 和 DEL 两次操作不是原子:GET 判断是自己的锁 → 锁刚好过期 → 另一个客户端加锁 → DEL 删了别人的锁
2.2 基础锁的实现(SETNX + Lua)
lua
-- 加锁(使用 SET 的 NX 选项原子操作)
SET lock:order <uuid> NX EX 30
-- NX: 不存在才设(互斥)
-- EX 30: 30 秒自动过期(防死锁)
-- <uuid>: 唯一 ID(解锁时验证身份)
-- 解锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end2.3 看门狗(Watch Dog)——自动续期问题
分布式锁的核心难题:锁超时时间怎么定?
- 时间太短 → 业务还没执行完,锁就过期了
- 时间太长 → 业务挂了,锁一直占着
解决方案:看门狗(Watch Dog)
看门狗策略(Redisson 实现):
- 持有锁后,每 10 秒检查一次
- 如果还在持有 → 续期(重新 EXPIRE)
- 如果已经解锁 → 停止续期
- 如果客户端挂了 → 不续期 → 锁自然过期
2.4 RedLock 算法
为什么单机 Redis 做锁不够安全? 主节点 SET lock NX → 同步到从节点前主挂 → 从节点升主 → 锁丢失!→ 另一个客户端也加锁成功 → 互斥性被破坏
三、图解全景
四、实战验证
4.1 基础分布式锁(SETNX)
bash
# 终端 1:获取锁
redis-cli SET lock:order "client-1" NX EX 30
# OK ← 加锁成功
# 终端 2:尝试获取同一把锁(会失败)
redis-cli SET lock:order "client-2" NX EX 30
# (nil) ← 加锁失败
# 终端 1:查看锁的持有者
redis-cli GET lock:order
# "client-1"
# 终端 1:完成操作后释放锁
redis-cli EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:order "client-1"
# (integer) 1 ← 释放成功4.2 验证解锁原子性
bash
# 错误解锁示范(非原子操作导致误删他人锁)
# 客户端 A 加锁
redis-cli SET lock:res "uuid-A" NX EX 30
# 模拟锁过期(强制过期)
redis-cli EXPIRE lock:res 1
# 等待 2 秒后锁自动过期
# 客户端 B 加锁(此时 A 的锁已过期)
redis-cli SET lock:res "uuid-B" NX EX 30
# 客户端 A 错误解锁(用之前的 GET+DEL 判断而不是 Lua)
redis-cli GET lock:res
# "uuid-B" ← 已经换了人
# 但代码里 GET 返回值 ≠ "uuid-A"
# 不会执行 DEL(避免误删)
# 如果用 Lua 脚本也会得到同样结果:返回 04.3 Redisson 看门狗使用
java
// Java Redisson 示例(看门狗自动续期)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
// 加锁(看门狗自动续期,默认每 10 秒续到 30 秒)
lock.lock();
// lock.lock(10, TimeUnit.SECONDS); // 手动指定过期时间
try {
// 业务逻辑(即使执行 5 分钟,锁也不会过期)
businessService.process();
} finally {
lock.unlock(); // 解锁 + 停止看门狗
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| 分布式锁的三大要素? | 互斥(同一时刻一个持有)、安全(自动过期不死锁)、容错(多数节点存活可用) |
| SETNX + EXPIRE 为什么不原子? | 两条命令可能 SETNX 成功但 EXPIRE 失败导致死锁;正确:使用 SET key value NX EX seconds |
| 解锁为什么要用 Lua? | GET + DEL 两步非原子:GET 判断是自己的 → 锁过期 → 别人加锁 → DEL 删了别人的锁 |
| 看门狗的作用? | 自动续期,防止业务执行超时导致锁提前过期;客户端挂了则不再续期,锁自然释放 |
| RedLock 和单机锁的区别? | RedLock 跨多个独立 Redis 节点加锁,容忍部分节点故障;但更复杂,对时间敏感 |
| 锁超时时间设多久? | 没有标准答案。通常 10-30 秒 + 看门狗续期。需要评估最大执行时间 |
| Redis 分布式锁和 ZooKeeper 锁的对比? | Redis:高性能(微秒级)、AP 系统(最终一致);ZK:强一致(CP)、性能略低 |
📚 相关链接
- **缓存一致性** — 分布式锁在缓存击穿中的应用
- **Lua脚本** — 解锁脚本的原理
- **ZSet/SkipList** — ZSet 用于实现公平锁的基础
- ← 返回 **缓存设计与应用索引**