题目
设计一个高并发场景下的分布式锁服务,基于Redis实现,要求解决锁的互斥性、死锁问题、锁续期以及可重入性,并处理Redis集群环境下的潜在问题
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
分布式锁原理,Redis命令使用,集群环境问题,锁续期机制,可重入性设计
快速回答
实现一个健壮的Redis分布式锁需要解决以下核心问题:
- 互斥性:使用SET命令的NX/EX选项原子性加锁
- 死锁预防:设置合理的过期时间,并通过看门狗机制续期
- 可重入性:使用Hash结构存储线程标识和重入计数
- 集群容错:采用Redlock算法应对主从切换场景
- 原子性保证:所有关键操作使用Lua脚本实现
1. 核心问题与解决方案
互斥性实现:
SET lock_key $unique_value NX EX 30- NX确保仅当key不存在时设置成功
- EX设置自动过期时间(单位秒)
- $unique_value使用UUID+线程ID,避免误删
死锁预防:
- 基础方案:设置过期时间(EX参数)
- 增强方案:看门狗线程定期续期(示例代码见章节3)
2. 可重入锁实现
使用Redis Hash结构存储锁信息:
HSET lock_key thread_id:123 1 # 首次获取
HINCRBY lock_key thread_id:123 1 # 重入释放锁的Lua脚本:
local key = KEYS[1]
local threadId = ARGV[1]
local count = redis.call('HGET', key, threadId)
if not count or tonumber(count) <= 0 then
return 0
end
local newCount = tonumber(count) - 1
if newCount == 0 then
redis.call('HDEL', key, threadId)
return 1
else
redis.call('HSET', key, threadId, newCount)
return newCount
end3. 锁续期机制(看门狗)
Java示例代码:
private void renewExpiration() {
Thread renewalThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 每10秒续期一次
Thread.sleep(10000);
// Lua脚本验证所有权并延长过期时间
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else return 0 end";
redis.eval(script,
Collections.singletonList(lockKey),
Arrays.asList(threadId, "30"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
renewalThread.setDaemon(true);
renewalThread.start();
}4. 集群环境解决方案(Redlock算法)
在Redis集群中实现步骤:
- 获取当前毫秒级时间戳T1
- 在N个独立Redis实例上顺序执行加锁命令(N通常为5)
- 计算获取锁耗时 = 当前时间T2 - T1
- 有效条件:成功实例数 ≥ N/2+1 且 耗时 < 锁有效期
- 若加锁失败,向所有实例发送删除命令
Redlock加锁伪代码:
List<RedisNode> nodes = getRedisNodes();
int successCount = 0;
long startTime = System.currentTimeMillis();
for (RedisNode node : nodes) {
if (setLock(node, resource, uuid, timeout)) {
successCount++;
}
}
long elapsed = System.currentTimeMillis() - startTime;
boolean valid = (successCount >= nodes.size()/2 + 1)
&& (elapsed < lockTime);5. 最佳实践与注意事项
- 时钟跳跃问题: 避免依赖系统时钟,使用Redis时间API
- 网络分区处理: 设置合理的锁有效期,避免脑裂导致的双重加锁
- 性能优化:
- 锁粒度拆分:大锁拆分为小锁
- 尝试锁退避:使用指数退避算法重试
- 错误示例:
// 错误!非原子操作可能导致死锁 jedis.setnx("lock", "value"); jedis.expire("lock", 30);
6. 扩展知识
- Redisson实现: Java客户端提供开箱即用的RLock,支持自动续期和Redlock
- 锁优化方案:
- 锁分段:ConcurrentHashMap思想,如库存锁拆分为10个槽
- 乐观锁:配合Redis事务/WATCH实现CAS操作
- 替代方案对比:
方案 一致性 性能 复杂度 Redis锁 最终 高 中 ZooKeeper 强 中 高 ETCD 强 高 高