Appearance
Redis 分布式锁
分布式锁是微服务架构中解决资源竞争的核心手段。Redis 凭借高性能和原子操作,成为最常用的分布式锁实现方案之一。
一、为什么需要分布式锁?
单机环境(synchronized / ReentrantLock):
┌──────────┐
│ JVM 1 │
│ synchronized(lock) { ... }
│ 一个 JVM 内有效
└──────────┘
分布式环境(多个 JVM):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JVM 1 │ │ JVM 2 │ │ JVM 3 │
│ 线程 A │ │ 线程 B │ │ 线程 C │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│
▼
┌──────────────┐
│ Redis │
│ 分布式锁 │
│ 跨 JVM 互斥 │
└──────────────┘二、分布式锁演进
2.1 第一版:SETNX + DEL
java
// 获取锁
public boolean tryLock(String key, String value, long expire) {
return redis.setnx(key, value) == 1;
}
// 释放锁
public void unlock(String key) {
redis.del(key);
}问题:
- 没有设置过期时间,如果获取锁的线程挂了,锁永远不会释放(死锁)
- 如果线程 A 执行时间过长,锁过期,线程 B 获取到锁,线程 A 释放锁时可能误删 B 的锁
2.2 第二版:SETNX + EXPIRE(原子操作)
java
// 获取锁 + 设置过期时间(原子操作)
public boolean tryLock(String key, String value, long expire) {
// SET key value NX EX expireSeconds
return redis.set(key, value, "NX", "EX", expire) != null;
}
// 释放锁(对比 value 防止误删)
public void unlock(String key, String value) {
String currentValue = redis.get(key);
if (value.equals(currentValue)) {
redis.del(key);
}
}问题: 释放锁的"判断 + 删除"不是原子操作。如果判断通过后、删除前,锁恰好过期,另一个线程获取到锁,仍然会误删。
2.3 第三版:Lua 脚本原子释放
java
// 释放锁(Lua 脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public void unlock(String key, String value) {
redis.eval(script, Collections.singletonList(key),
Collections.singletonList(value));
}问题: 锁过期时间难以确定。如果业务执行时间超过过期时间,锁会被释放,其他线程可以获取锁,出现并发问题。
2.4 第四版:Redisson 看门狗
java
// Redisson 自动续期
RLock lock = redisson.getLock("lock:order:1001");
// 方式一:自动续期(看门狗)—— 默认 30 秒,每 10 秒续期
lock.lock();
try {
// 业务逻辑,即使执行时间超过 30 秒,锁也会自动续期
} finally {
lock.unlock();
}
// 方式二:指定过期时间(不续期)
lock.lock(10, TimeUnit.SECONDS);看门狗机制:
lock() → 获取锁成功 → 启动看门狗(每 10 秒续期到 30 秒)
│
▼
执行完成 → unlock() → 取消看门狗 → 删除锁
如果进程崩溃 → 看门狗停止 → 30 秒后自动过期 → 锁自动释放三、Redisson 完整示例
3.1 依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>3.2 配置
yaml
spring:
redis:
host: localhost
port: 63793.3 使用
java
@Service
public class OrderService {
@Autowired
private RedissonClient redisson;
public void createOrder(Long orderId) {
RLock lock = redisson.getLock("lock:order:" + orderId);
try {
// 尝试加锁,最多等待 10 秒,锁有效期 30 秒
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
createOrderInternal(orderId);
} else {
throw new BusinessException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}3.4 可重入锁
java
// Redisson 的 RLock 支持可重入
public void methodA() {
RLock lock = redisson.getLock("lock:order:1001");
lock.lock();
try {
methodB(); // 同一线程再次获取同一把锁,不会阻塞
} finally {
lock.unlock();
}
}
public void methodB() {
RLock lock = redisson.getLock("lock:order:1001");
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}3.5 公平锁、读写锁、联锁
java
// 公平锁(按请求顺序获取)
RLock fairLock = redisson.getFairLock("lock:fair");
// 读写锁(读读不互斥,读写互斥,写写互斥)
RReadWriteLock rwLock = redisson.getReadWriteLock("lock:rw");
rwLock.readLock().lock(); // 读锁
rwLock.writeLock().lock(); // 写锁
// 联锁(多个锁同时获取,同时释放)
RLock lock1 = redisson.getLock("lock:1");
RLock lock2 = redisson.getLock("lock:2");
RLock multiLock = redisson.getMultiLock(lock1, lock2);
multiLock.lock();四、RedLock 红锁
4.1 单节点 Redis 锁的问题
单节点 Redis 锁的风险:
1. 线程 A 获取 Master 上的锁
2. Master 宕机,锁数据还没同步到 Slave
3. Slave 提升为 Master(锁数据丢失)
4. 线程 B 在新 Master 上获取同一把锁
5. 线程 A 和 B 同时持有锁 → 并发问题4.2 RedLock 原理
RedLock 使用 N 个独立的 Redis 节点(奇数个,如 5 个):
1. 线程 A 向 N 个节点依次获取锁
2. 如果超过 N/2+1 个节点获取成功 → 获得锁
3. 锁的有效时间 = 初始时间 - 获取锁消耗时间
4. 释放锁时向所有节点释放
优势:即使少数节点宕机,只要多数节点存活,锁仍然可靠4.3 RedLock 争议
RedLock 存在争议。Martin Kleppmann(《数据密集型应用》作者)认为 RedLock 在某些极端情况下不安全。实际项目中,大多数场景使用单节点 + 哨兵/集群即可满足需求,真正需要 RedLock 的场景很少。
五、最佳实践
java
// 完整的分布式锁最佳实践
public void executeWithLock(String lockKey, Runnable task) {
RLock lock = redisson.getLock(lockKey);
try {
// 尝试获取锁:等待 5 秒,锁有效期 30 秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
task.run();
} finally {
// 只有当前线程持有锁才释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
// 获取锁失败,走降级逻辑
throw new LockAcquireException("获取锁超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockAcquireException("获取锁被中断");
}
}六、面试要点
Q1:Redis 分布式锁的实现原理?
核心是 SET key value NX EX expire 原子命令。加锁成功返回 OK,失败返回 nil。释放锁时通过 Lua 脚本原子判断 value 是否匹配,匹配才删除,防止误删。
Q2:Redisson 的看门狗机制是什么?
如果 lock() 未指定过期时间,Redisson 自动启动看门狗,每 10 秒续期到 30 秒。只要业务不主动释放,锁就不会过期。进程崩溃时看门狗停止,锁 30 秒后自动过期。
Q3:Redis 分布式锁有哪些问题?
单节点宕机导致锁丢失(RedLock 解决)、锁过期时间难以确定(看门狗解决)、释放锁非原子操作(Lua 脚本解决)。生产环境推荐使用 Redisson 封装好的锁。
Q4:RedLock 是什么?有必要用吗?
RedLock 是 Redis 作者提出的多节点分布式锁方案,需要在 N 个独立 Redis 节点上获取锁,获得 N/2+1 个才算成功。RedLock 存在理论争议,大多数场景单节点 + 哨兵/集群的锁已足够。真正需要 RedLock 的场景极少。
