核心概念
Redis 分布式锁的核心是利用 SET NX(SET if Not eXists) 命令的原子性来实现互斥,配合 过期时间 防止死锁,使用 Lua 脚本 保证加锁和解锁操作的原子性。
一、基础实现原理
1.1 加锁:SET NX + EX
# SET key value NX EX seconds
# NX:仅当key不存在时设置
# EX:设置过期时间(秒)
SET lock:resource:123 uuid-value-xxx NX EX 30
返回值:
OK:加锁成功nil:key已存在,加锁失败
关键点:
SET NX EX是一条原子命令(Redis 2.6.12 之后),避免分两步操作时客户端崩溃导致死锁value通常使用 UUID,用于解锁时校验持锁者身份
public boolean tryLock(String key, String requestId, int expireTime) {
// 使用 SET NX EX 一次性完成加锁和设置过期时间
String result = jedis.set(key, requestId,
SetParams.setParams().nx().ex(expireTime));
return "OK".equals(result);
}
1.2 解锁:Lua 脚本保证原子性
错误示范:
// ❌ 非原子操作,可能误删其他客户端的锁
if (redis.get(lockKey).equals(requestId)) {
redis.del(lockKey); // 可能在这之间锁过期,其他客户端获得锁
}
正确做法:使用 Lua 脚本
-- 解锁脚本
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
public boolean unlock(String key, String requestId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(script,
Collections.singletonList(key),
Collections.singletonList(requestId));
return Long.valueOf(1L).equals(result);
}
Lua 脚本的优势:
- Redis 保证脚本执行的原子性
- 避免「检查-删除」之间的时间窗口问题
二、基础方案存在的问题
问题1:锁过期时间难以设置
如果业务执行时间超过锁的过期时间,锁会自动释放,导致并发问题。
解决方案:看门狗机制(Watch Dog)
问题2:不可重入
同一个线程无法多次获取同一把锁。
解决方案:使用 Hash 结构记录重入次数
问题3:主从架构下的锁丢失
Redis 主从复制是异步的,主节点宕机时,从节点可能未同步锁数据:
1. 客户端 A 在主节点获取锁
2. 主节点宕机,锁数据未同步到从节点
3. 从节点提升为主节点
4. 客户端 B 在新主节点获取相同的锁 ✓(锁丢失)
解决方案:RedLock 算法(后续介绍)
三、Redisson 完整解决方案
Redisson 是一个成熟的 Redis 客户端,提供了完整的分布式锁实现。
3.1 基本使用
@Autowired
private RedissonClient redissonClient;
public void doSomething() {
// 获取锁对象
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试加锁
// waitTime: 等待获取锁的最大时间
// leaseTime: 锁自动释放时间
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务逻辑
processBusinessLogic();
} else {
// 获取锁失败
throw new RuntimeException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(只有持锁线程才能释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
3.2 核心特性
(1)看门狗机制(Watch Dog)
问题: 业务执行时间超过锁的过期时间怎么办?
Redisson 的解决方案:
// 不指定 leaseTime,使用默认的看门狗机制
lock.lock(); // 默认 30 秒过期
// 后台线程每隔 10 秒(leaseTime / 3)检查一次
// 如果锁还被持有,自动续期到 30 秒
看门狗工作原理:
- 加锁时不指定过期时间,Redisson 会设置默认的 30 秒
- 启动一个后台定时任务,每 10 秒检查一次
- 如果当前线程还持有锁,就将过期时间续期到 30 秒
- 直到业务执行完毕,手动释放锁
源码关键逻辑:
// Redisson 加锁源码简化
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
// 每隔 internalLockLeaseTime / 3 执行一次
Timeout task = commandExecutor.getConnectionManager().newTimeout(
timeout -> {
// 续期 Lua 脚本
renewExpiration();
// 递归调用,持续续期
scheduleExpirationRenewal(threadId);
},
internalLockLeaseTime / 3,
TimeUnit.MILLISECONDS
);
}
(2)可重入锁
实现原理: 使用 Redis Hash 结构存储锁信息
# 锁的数据结构
HSET myLock thread-1 1 # 第一次加锁,重入次数为 1
HSET myLock thread-1 2 # 第二次加锁,重入次数为 2
加锁 Lua 脚本(简化版):
-- 锁不存在
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置重入次数为 1
redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间
return nil;
end;
-- 锁存在且是当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数 +1
redis.call('pexpire', KEYS[1], ARGV[1]); -- 刷新过期时间
return nil;
end;
-- 锁被其他线程持有
return redis.call('pttl', KEYS[1]); -- 返回剩余过期时间
解锁 Lua 脚本(简化版):
-- 锁不存在或不是当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- 重入次数 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 重入次数 > 0,只刷新过期时间
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 重入次数为 0,删除锁
redis.call('del', KEYS[1]);
return 1;
end;
(3)公平锁
Redisson 提供了公平锁实现,保证先到先得:
RLock fairLock = redissonClient.getFairLock("fairLock");
fairLock.lock();
try {
// 业务逻辑
} finally {
fairLock.unlock();
}
实现原理: 使用 Redis List 维护等待队列
四、RedLock 算法(高可靠方案)
4.1 为什么需要 RedLock?
Redis 主从架构下,异步复制可能导致锁丢失:
主节点宕机 → 从节点提升 → 锁数据未同步 → 新主节点可以再次加锁
4.2 RedLock 原理
核心思想: 在多个独立的 Redis 实例(≥3个,通常5个)上同时加锁,超过半数成功才算加锁成功。
加锁流程:
- 获取当前时间戳 T1
- 依次向 N 个 Redis 实例请求加锁(使用相同的 key 和随机值)
- 设置较短的超时时间(如 5-50ms),避免阻塞在宕机的节点上
- 计算加锁耗时:T2 - T1
- 判断是否成功:
- 在超过半数的实例上加锁成功(如 5 个实例中成功 3 个)
- 总耗时 < 锁的有效时间
- 如果成功,锁的实际有效时间 = 初始有效时间 - 加锁耗时
- 如果失败,向所有实例发送解锁请求
Redisson 使用 RedLock:
RLock lock1 = redissonClient1.getLock("myLock");
RLock lock2 = redissonClient2.getLock("myLock");
RLock lock3 = redissonClient3.getLock("myLock");
// 创建 RedLock
RLock redLock = redissonClient.getRedLock(lock1, lock2, lock3);
try {
redLock.lock();
// 业务逻辑
} finally {
redLock.unlock();
}
4.3 RedLock 的争议
争议点: Redis 作者 Antirez 与分布式系统专家 Martin Kleppmann 的辩论
- Antirez:RedLock 在多数节点正常的情况下是安全的
- Martin:RedLock 无法解决所有分布式一致性问题(如时钟漂移、长时间 GC)
结论: RedLock 在大多数场景下足够可靠,但不能替代基于共识算法(如 Paxos、Raft)的强一致性方案。
五、实战注意事项
1. 锁的粒度要合理
// ❌ 锁粒度过大,影响并发
lock("global-lock");
// ✅ 根据业务细化锁粒度
lock("order:" + orderId);
2. 设置合理的超时时间
// 考虑业务执行时间,设置合理的过期时间
// 或使用 Redisson 的看门狗机制
lock.tryLock(5, 30, TimeUnit.SECONDS);
3. 避免死锁
// ✅ 必须在 finally 中释放锁
try {
lock.lock();
// 业务逻辑
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
4. 监控告警
- 监控锁的持有时间,发现异常长时间持锁
- 监控锁的竞争情况,优化锁粒度
- 监控 Redis 主从切换对锁的影响
答题总结
Redis 分布式锁的核心原理:
- 基础实现:使用
SET NX EX原子命令加锁,Lua 脚本原子性解锁 - 核心命令:
- 加锁:
SET lock:key uuid NX EX 30 - 解锁:Lua 脚本保证「检查持锁者 + 删除」的原子性
- 加锁:
- Redisson 增强:
- 看门狗机制:自动续期,避免业务未执行完锁就过期
- 可重入锁:基于 Hash 结构记录重入次数
- 公平锁:基于 List 维护等待队列
- 高可用方案:RedLock 算法,在多个独立 Redis 实例上加锁,超过半数成功才算成功
面试加分项:
- 提到 Lua 脚本保证原子性的重要性
- 了解 Redisson 的看门狗机制
- 知道 RedLock 算法及其适用场景
- 能够分析 Redis 主从架构下锁丢失的风险
推荐方案: 大多数场景使用 Redisson + Redis 哨兵/集群 即可,极端情况下考虑 RedLock 或 Zookeeper。