在单机系统中,我们习惯使用 synchronized 或 ReentrantLock 来保证线程安全。然而,在微服务架构普及的今天,服务实例往往部署在多台机器上,JVM(Java Virtual Machine,Java 虚拟机) 级别的锁无法跨进程生效。当多个客户端同时竞争共享资源(如秒杀库存、数据库行记录、定时任务调度)时,分布式锁便应运而生。
Redis 凭借其高性能、原子操作特性以及天然的支持过期时间(TTL),成为了实现分布式锁的首选中间件。
一、初识分布式锁------SETNX + EXPIRE
最直观的实现方式是利用 Redis 的 SETNX (SET if Not Exists) 命令。
原理:
利用 SETNX 的互斥性。当 Key 不存在时,设置成功,代表获取锁;当 Key 已存在时,设置失败,代表锁被占用。为了防止客户端宕机导致锁无法释放,必须配合 EXPIRE 设置过期时间。
伪代码逻辑:
java
if (jedis.setnx(lock_key, "1") == 1) {
jedis.expire(lock_key, 10); // 设置10秒过期
try {
doSomething();
} finally {
jedis.del(lock_key);
}
}
存在的问题:
- 原子性缺失:
SETNX和EXPIRE是两条独立的指令。如果客户端在SETNX成功后、EXPIRE执行前崩溃(如进程被杀、断电),这把锁将永远无法释放(死锁),导致后续所有请求阻塞。
二、原子性优化------Lua 脚本与 SET 扩展指令
为了解决 SETNX 与 EXPIRE 非原子执行的问题,业界提出了两种优化方案。
方案 A:Lua 脚本
利用 Redis 执行 Lua 脚本的原子性,将加锁和设置过期时间封装在一个脚本中。
方案 B:SET 扩展参数(推荐)
Redis 2.6.12 版本后,SET 命令支持了扩展参数,能够原子地完成"不存在则设置"和"设置过期时间"两个动作。
命令格式:
bash
SET key value [EX seconds] [PX milliseconds] [NX]
参数解析:
- NX: Not Exists,仅当键不存在时执行。
- EX/PX: 设置过期时间(秒/毫秒)。
演进对比表:
| 方案 | 原子性 | 复杂度 | 推荐指数 | 备注 |
|---|---|---|---|---|
| SETNX + EXPIRE | 否 | 低 | ⭐ | 存在死锁风险,生产环境禁用的基础版 |
| Lua 脚本 | 是 | 中 | ⭐⭐⭐ | 灵活,但代码维护成本稍高 |
| SET ... NX PX | 是 | 低 | ⭐⭐⭐⭐⭐ | 官方推荐,简洁高效 |
三、安全性与误删问题------唯一标识与 Lua 解锁
在第二阶段的基础上,解决了加锁的原子性,但解锁环节依然存在隐患。
场景描述:
客户端 A 获取了锁,设置了 10 秒过期。由于业务逻辑卡顿(如 Full GC),执行了 15 秒。
- 第 10 秒时,锁自动过期释放。
- 客户端 B 成功获取锁。
- 第 15 秒时,客户端 A 执行完毕,执行
DEL命令释放锁。 - 结果: 客户端 A 误删了客户端 B 的锁!
解决方案:
- Value 唯一化: 在加锁时,Value 设置为一个唯一标识(如
UUID + ThreadID)。 - 解锁校验: 删除锁之前,先判断 Value 是否匹配。
- 原子性保障: "判断 Value" 和 "删除 Key" 必须原子执行,因此必须使用 Lua 脚本。
解锁 Lua 脚本:
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
四、高可用挑战------Redlock 算法
上述方案在 Redis 单机或"主从复制"架构下仍存在缺陷。如果 Redis Master 节点在获取锁后、同步给 Slave 之前宕机,Slave 晋升为新 Master,锁信息丢失,导致多个客户端同时持有锁。
为了解决这个问题,Redis 作者提出了 Redlock 算法。
核心思想:
部署 N 个(通常 5 个)独立的 Redis Master 节点。获取锁时,客户端需向所有节点发起请求。
执行流程:
- 获取当前时间(毫秒)。
- 向 N 个节点依次发送加锁请求(设置较短的超时时间)。
- 计算获取锁的耗时。
- 成功条件: 客户端在至少
N/2 + 1个节点上获取锁成功,且总耗时小于锁的有效期。
Redlock 的争议:
尽管 Redlock 提高了安全性,但也存在争议(如 Martin Kleppmann 提出的时钟跳跃问题)。在实际工程中,如果不需要极致的强一致性,通常使用"单机 Redis + 主从复制 + 等待从库同步"或 Zookeeper/Etcd 等 CP 系统作为替代。
五、常见面试题与解析
Q1: 既然有了 SETNX,为什么还需要 Redlock?
A:
SETNX方案通常基于单点 Redis。如果 Redis Master 宕机且数据未同步到 Slave,会导致锁丢失,破坏互斥性。Redlock 通过多数派原则(Quorum)解决了单点故障带来的数据一致性问题,适用于对锁安全性要求极高的场景。
Q2: 业务执行时间超过了锁的过期时间怎么办?
A: 这是一个经典问题,通常有三种解法:
- 调大过期时间: 预估业务最大耗时,但这会导致资源浪费。
- 看门狗机制: 类似 Redisson 的实现,启动一个后台线程,每隔一段时间(如 10s)检测锁是否还持有,如果持有则自动续期(Reset TTL)。
- 快速失败: 如果获取锁失败或执行超时,直接抛出异常,不进行重试。
Q3: Redis 分布式锁和 Zookeeper 分布式锁有什么区别?
A:
- Redis: 基于 AP (Availability + Partition Tolerance)模型(通常),性能极高,适合高并发、对锁安全性要求不是绝对严苛的场景(如缓存重建、普通秒杀)。
- Zookeeper: 基于 CP (Consistency + Partition Tolerance)模型,利用临时顺序节点实现,强一致性,但性能略低于 Redis,适合对数据一致性要求极高的场景(如 Leader 选举、配置管理)。
CP (Consistency + Partition Tolerance):"宁可宕机,不可出错"(严谨派)
AP (Availability + Partition Tolerance):"宁可数据旧点,也要一直服务"(实用派)