Redis 分布式锁与 Redisson 原理深度解析
分布式锁是分布式系统中保证资源互斥访问的核心技术。本文深入剖析 Redis 分布式锁的实现原理、Redisson 的看门狗机制、RedLock 算法与争议、以及生产环境中的最佳实践与避坑指南。
一、分布式锁概述
1.1 为什么需要分布式锁
分布式锁要求
互斥性: 任意时刻只能一个客户端持有
可重入: 同一客户端可重入
死锁避免: 锁要能自动释放
性能: 高并发下性能要足够好
分布式问题
多进程/多机器竞争同一资源
JVM 锁失效
需要分布式锁
单机问题
多线程竞争同一资源
synchronized / ReentrantLock
只在单机有效
1.2 分布式锁的实现方式
实现方式
数据库
表记录锁
悲观锁
ZooKeeper
临时有序节点
CP 系统,强一致
Redis
SETNX + 过期时间
AP 系统,高性能
二、Redis 分布式锁基础
2.1 简单实现
释放锁
是
否
释放锁
unlink lock_key
是 owner?
释放成功
释放失败
加锁
是
否
SET lock_key unique_value NX EX 30
返回 OK?
获取锁成功
获取锁失败
2.2 基础实现代码
java
// 基础分布式锁实现
public class SimpleRedisLock {
private RedisTemplate<String, String> redisTemplate;
public boolean tryLock(String lockKey, String requestId, int expireTime) {
// SET key value NX EX seconds
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId,
Duration.ofSeconds(expireTime));
return Boolean.TRUE.equals(result);
}
public boolean releaseLock(String lockKey, String requestId) {
// 使用 Lua 脚本保证原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
return result != null && result > 0;
}
}
2.3 Lua 脚本释放锁
lua
-- 释放锁的 Lua 脚本
-- KEYS[1]: 锁的 key
-- ARGV[1]: 请求的唯一标识
-- 只有当锁的值等于请求 ID 时才删除
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
2.4 为什么需要唯一值?
解决方案
每个客户端使用唯一值作为锁的值
释放时检查值是否匹配
只有自己的锁才能释放
问题场景
客户端A 获取锁,过期时间 30s
业务执行时间 > 30s
锁自动过期
客户端B 获取同一把锁
客户端A 业务执行完成
客户端A 释放锁
锁被错误释放!(B 的锁)
三、Redisson 分布式锁
3.1 Redisson 概述
优势
开箱即用
功能完善
社区活跃
Redisson核心
提供易用的分布式锁 API
实现可重入锁
看门狗自动续期
公平锁/读写锁等多种锁
3.2 看门狗机制
看门狗(Watchdog)是 Redisson 的核心特性,解决锁续期问题:
续期时机
业务执行中
锁快过期
自动续期
业务继续执行
业务完成后释放锁
看门狗流程
是
否
加锁时设置过期时间 30s
启动定时任务
每 10s 检查一次
锁还存在?
续期 30s
停止续期
3.3 看门狗源码解析
java
// Redisson 看门狗核心代码
public class RedissonLock {
// 默认锁过期时间
private static final long lockWatchdogTimeout = 30 * 1000;
// 续期间隔 = 过期时间 / 3
private static final long internalLockLeaseTime = lockWatchdogTimeout / 3;
// 续期定时任务
private void scheduleExpirationRenewal(long threadId) {
// 定时任务,每 10 秒执行一次
renewalTask = commandExecutor.getConnectionManager()
.getSchedulerService()
.schedule(new Runnable() {
@Override
public void run() {
// 续期命令
renewExpiration();
}
}, internalLockLeaseTime, TimeUnit.MILLISECONDS);
}
// 续期
private void renewExpiration() {
// 使用 Lua 脚本续期
Long result = evalWriteAsync(getName(), LongCodec.INSTANCE, RawValue.INSTANCE,
"if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"else " +
" return 0; " +
"end",
Collections.singletonList(getName()),
lockWatchdogTimeout, getLockName());
if (result > 0) {
// 续期成功,继续调度
scheduleExpirationRenewal(getHolderId());
}
}
}
3.4 Redisson 使用示例
java
// 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.0</version>
</dependency>
// 配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
// 使用可重入锁
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder(Order order) {
RLock lock = redissonClient.getLock("order:create");
try {
// tryLock() 尝试获取锁,最多等待 10s,锁自动续期
boolean locked = lock.tryLock(10, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁失败");
}
// 业务逻辑
doCreateOrder(order);
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
3.5 可重入锁实现
java
// Redisson 可重入锁核心
// 使用 Redis Hash 存储锁信息
// key: 锁名
// field: 线程标识
// value: 重入次数
// 加锁时
HSET lock_name thread_id 1 // 首次加锁,次数为 1
// 重入时
HINCRBY lock_name thread_id 1 // 次数 +1
// 释放时
HINCRBY lock_name thread_id -1 // 次数 -1
// 如果次数为 0,删除锁
四、RedLock 算法
4.1 RedLock 原理
释放
向所有 Redis 实例释放锁
不管是否获取成功
RedLock算法
是
否
向 N 个 Redis 实例请求锁
使用相同的 key 和随机值
计算获取锁的时间
成功获取 >= N/2+1?
成功获取锁
获取锁失败
4.2 RedLock 代码实现
java
public class RedLock {
public String lock(String resourceName, int ttlMillis) {
// 生成随机值
String token = UUID.randomUUID().toString();
int n = redissonServers.size();
int成功 = 0;
long startTime = System.currentTimeMillis();
// 向所有实例请求锁
for (RedissonInstance instance : redissonServers) {
try {
if (instance.tryLock(token, ttlMillis)) {
成功++;
}
} catch (Exception e) {
// 继续尝试其他实例
}
}
long elapsedTime = System.currentTimeMillis() - startTime;
// 检查是否获取超过半数实例的锁
if (成功 >= (n / 2 + 1)) {
// 计算锁的有效期
long clientClockTimeout = ttlMillis - elapsedTime - 重试时间;
return token;
} else {
// 释放所有获取到的锁
for (RedissonInstance instance : redissonServers) {
try {
instance.unlock(token);
} catch (Exception e) {
// 忽略
}
}
return null;
}
}
}
4.3 RedLock 争议
替代方案
单机 Redis + 哨兵/集群
足够大多数场景
Redisson 单机模式足够
质疑点
时钟跳跃问题
各服务器时钟不同步
可能导致锁提前失效
性能问题
需要请求多个 Redis 实例
延迟增加
复杂度
部署多个 Redis 实例
运维成本增加
五、生产环境最佳实践
5.1 锁的粒度控制
示例
// 反例: 锁住整个方法
public void doSomething() {
lock.lock(); // 整个方法加锁
try { ... } finally { lock.unlock(); }
}
// 正例: 只锁关键代码
public void doSomething() {
// 前置处理
lock.lock();
try { 关键代码 } finally { lock.unlock(); }
// 后置处理
}
粒度过小
只锁必要的代码
性能好
并发能力强
粒度过大
锁住整个方法
性能差
并发能力差
5.2 最佳实践代码
java
@Service
public class InventoryService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private InventoryMapper inventoryMapper;
// 最佳实践: 合理的锁粒度
public void decreaseStock(Long productId, Integer count) {
String lockKey = "stock:decrease:" + productId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
// 尝试获取锁,最多等待 5 秒,锁自动续期
locked = lock.tryLock(5, -1, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 只在锁内执行关键操作
Inventory inventory = inventoryMapper.selectByProductId(productId);
if (inventory.getStock() < count) {
throw new BusinessException("库存不足");
}
inventoryMapper.decreaseStock(productId, count);
} finally {
// 确保锁被释放
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
5.3 锁超时设置
java
// 错误示例: 锁超时设置过短
lock.tryLock(1, TimeUnit.SECONDS); // 只等待 1 秒
// 业务执行 30 秒,锁在 30 秒时自动释放
// 正确示例: 锁超时设置合理
lock.tryLock(10, 30, TimeUnit.SECONDS); // 等待 10 秒,锁 30 秒过期
// 使用看门狗自动续期
// 正确示例: 不设置过期时间,使用看门狗
lock.tryLock();
// 看门狗每 10 秒检查一次,锁自动续期
// 正确示例: 设置合理的过期时间
lock.tryLock(10, 60, TimeUnit.SECONDS); // 等待 10 秒,锁 60 秒过期
// 看门狗会自动续期
5.4 常见问题处理
问题3: 主从切换丢锁
Redis 主从切换
从库未同步锁信息
新主库丢失锁
解决方案: 使用 RedLock 或红锁
问题2: 锁重试风暴
获取锁失败后大量重试
Redis 压力增大
解决方案: 添加随机延迟
使用分布式信号量控制并发
问题1: 锁续期失败
看门狗定时任务失败
锁被提前释放
解决方案: 设置合理的过期时间
不要依赖看门狗作为唯一续期机制
六、面试高频问题
6.1 Redis 分布式锁需要注意哪些问题?
1. 原子性
- 使用 SETNX + EX 保证原子性
- 释放锁使用 Lua 脚本保证原子性
2. 过期时间
- 设置合理的过期时间
- 防止死锁
3. 唯一值
- 使用 UUID 等唯一值
- 防止误删其他客户端的锁
4. 可重入
- 同一线程可多次获取锁
- 使用计数器实现
5. 锁续期
- 看门狗机制
- 防止业务未完成锁过期
6.2 如何实现可重入锁?
实现方式:使用 Redis Hash
加锁时:
- key: 锁名
- field: 线程标识(UUID + 线程ID)
- value: 重入次数
- 首次加锁:HSET lock field 1
- 重入:HINCRBY lock field 1
释放时:
- HINCRBY lock field -1
- 如果值为 0,删除 key
6.3 Redisson 的看门狗机制原理?
原理:
1. 加锁时设置默认过期时间 30 秒
2. 看门狗定时任务每 10 秒(30/3)执行一次
3. 检查锁是否还存在
4. 如果存在,续期 30 秒
5. 业务完成后释放锁,停止看门狗
注意:
- 只有使用 tryLock() 不指定过期时间时才启用看门狗
- tryLock(time, timeUnit) 指定过期时间时不启用看门狗
6.4 RedLock 算法的原理?
原理:
1. 向 N 个独立的 Redis 实例请求锁
2. 计算获取锁的时间
3. 如果在有效期内,成功获取 >= N/2+1 个实例的锁
4. 获取锁成功,有效期 = TTL - 获取时间
5. 释放时向所有实例发送释放命令
为什么需要多个实例:
- 防止单点故障
- 提高可用性
注意:RedLock 有争议,主要问题是时钟同步假设
6.5 分布式锁 vs 分布式信号量?
分布式锁:
- 同一时刻只有一个客户端持有
- 用于互斥访问
分布式信号量:
- 同一时刻可有 N 个客户端持有
- 用于限流控制
Redisson 示例:
RLock lock = redissonClient.getLock("resource");
// 互斥锁
RSemaphore semaphore = redissonClient.getSemaphore("resource");
// 信号量,允许 N 个并发
semaphore.trySetPermits(10); // 设置 10 个许可
semaphore.acquire(); // 获取许可
semaphore.release(); // 释放许可
七、总结
7.1 分布式锁选择
选择建议
Redis 单机
足够大多数场景
Redis 集群 + Redisson
高可用要求
ZooKeeper
强一致性要求
etcd
配置中心 + 分布式锁
7.2 最佳实践
分布式锁最佳实践:
1. 原子性
✅ SETNX + EX 原子性加锁
✅ Lua 脚本原子性释放
2. 过期时间
✅ 设置合理的过期时间
✅ 使用看门狗自动续期
3. 唯一标识
✅ 使用 UUID 作为锁值
✅ 释放前检查值是否匹配
4. 锁粒度
✅ 只锁关键代码
✅ 避免锁粒度过大
5. 异常处理
✅ finally 中释放锁
✅ 检查是否持有锁后再释放