目录
- 分布式锁的基本需求
- 第一阶段:SETNX的简单实现
- 第二阶段:加入过期时间
- 第三阶段:原子性操作
- 第四阶段:Lua脚本保证原子性
- 第五阶段:看门狗机制
- 第六阶段:可重入锁
- 第七阶段:红锁算法
- 常见问题总结
分布式锁的基本需求
核心要求
- 互斥性: 任意时刻只有一个客户端能持有锁
- 安全性: 只有持有锁的客户端才能释放锁
- 活性: 不会发生死锁,最终一定能获取到锁
- 容错性: 部分节点宕机后,锁服务依然可用
第一阶段:SETNX的简单实现
实现方式
SETNX lock_key "client_id"
代码示例
public boolean tryLock(String lockKey, String clientId) {
// 尝试设置锁
String result = jedis.set(lockKey, clientId, "NX");
return "OK".equals(result);
}
public void unlock(String lockKey, String clientId) {
// 简单删除锁
jedis.del(lockKey);
}
存在的问题
1. 死锁问题
场景:客户端获取锁后宕机,锁永远不会被释放
问题:其他客户端永远无法获取锁
影响:系统完全阻塞
2. 误删锁问题
// 客户端A获取锁
SETNX lock_key "client_A"
// 客户端A业务执行时间过长
// 客户端B在客户端A还在执行时删除了锁
DEL lock_key
// 客户端C获取到锁,与客户端A同时执行
SETNX lock_key "client_C"
第二阶段:加入过期时间
实现方式
SETNX lock_key "client_id"
EXPIRE lock_key 30
代码示例
public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
// 设置锁
String result = jedis.set(lockKey, clientId, "NX");
if ("OK".equals(result)) {
// 设置过期时间
jedis.expire(lockKey, expireSeconds);
return true;
}
return false;
}
存在的问题
1. 非原子性操作
时序问题:
1. SETNX 成功
2. 客户端宕机(EXPIRE未执行)
3. 锁永远不会过期
结果:死锁问题依然存在
2. 过期时间设置困难
问题:
- 设置太短:业务未完成锁就过期
- 设置太长:异常情况下锁释放慢
- 业务执行时间不确定
第三阶段:原子性操作
实现方式
SET lock_key "client_id" NX EX 30
代码示例
public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
// 原子性设置锁和过期时间
String result = jedis.set(lockKey, clientId, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public void unlock(String lockKey, String clientId) {
// 检查是否是自己的锁
String value = jedis.get(lockKey);
if (clientId.equals(value)) {
jedis.del(lockKey);
}
}
存在的问题
1. 锁误删问题
// 时序问题
String value = jedis.get(lockKey); // 1. 获取锁值
if (clientId.equals(value)) { // 2. 判断是自己的锁
// 此时锁可能已经过期,被其他客户端获取
jedis.del(lockKey); // 3. 删除锁(可能删除了别人的锁)
}
2. 锁过期问题
场景:业务执行时间 > 锁过期时间
问题:锁在业务执行过程中过期,其他客户端获取到锁
结果:多个客户端同时执行临界区代码
第四阶段:Lua脚本保证原子性
释放锁的Lua脚本
-- 释放锁的Lua脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
代码示例
public class DistributedLock {
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
String result = jedis.set(lockKey, clientId, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public boolean unlock(String lockKey, String clientId) {
Object result = jedis.eval(UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
Collections.singletonList(clientId));
return "1".equals(result.toString());
}
}
解决的问题
仍存在的问题
1. 锁续期问题
场景:业务执行时间不确定
问题:
- 锁过期时间设置困难
- 业务执行中锁过期导致并发问题
- 无法动态调整锁的持有时间
第五阶段:看门狗机制
Redisson的看门狗实现原理
1. 自动续期机制
// Redisson内部实现原理
public class RedissonLock {
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
entry.addThreadId(threadId);
// 每10秒续期一次(默认30秒过期时间的1/3)
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
// 续期Lua脚本
renewExpiration();
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
}
2. 续期Lua脚本
-- 续期脚本
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
return redis.call('pexpire', KEYS[1], ARGV[1])
else
return 0
end
看门狗机制特点
优势:
✅ 自动续期,避免业务执行中锁过期
✅ 客户端宕机时,看门狗停止,锁自然过期
✅ 无需预估业务执行时间
工作原理:
1. 获取锁时启动看门狗
2. 定时检查锁是否还被当前线程持有
3. 如果持有,则续期锁的过期时间
4. 释放锁或客户端宕机时,看门狗停止
代码示例
// Redisson使用示例
public void businessLogic() {
RLock lock = redissonClient.getLock("myLock");
try {
// 获取锁,启动看门狗
boolean acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑,无需担心锁过期
doBusinessLogic();
}
} finally {
// 释放锁,停止看门狗
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
第六阶段:可重入锁
可重入锁的需求
public void methodA() {
lock.lock();
try {
methodB(); // 需要再次获取同一把锁
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 同一线程再次获取锁
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
Redisson可重入锁实现
1. 数据结构
# 使用Hash结构存储锁信息
HSET lock_key thread_id count
# 例如:
HSET myLock "thread_123" 2 # 线程123持有锁,重入次数为2
2. 获取锁的Lua脚本
-- 可重入锁获取脚本
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 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)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
return redis.call('pttl', KEYS[1])
3. 释放锁的Lua脚本
-- 可重入锁释放脚本
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2])
return 0
else
redis.call('del', KEYS[1])
return 1
end
第七阶段:红锁算法
单点故障问题
问题:Redis主从架构下的锁丢失
场景:
1. 客户端A在Master上获取锁
2. Master宕机,锁信息未同步到Slave
3. Slave提升为Master
4. 客户端B在新Master上获取到同一把锁
结果:两个客户端同时持有锁
红锁算法原理
1. 多实例部署
部署N个独立的Redis实例(通常N=5)
每个实例都是独立的,不存在主从关系
2. 获取锁流程
1. 获取当前时间戳 start_time
2. 依次向N个实例请求锁,设置超时时间
3. 计算获取锁的总耗时
4. 判断是否获取锁成功:
- 获取锁的实例数 > N/2
- 总耗时 < 锁的有效时间
5. 如果成功,锁的实际有效时间 = 原有效时间 - 总耗时
6. 如果失败,向所有实例释放锁
3. 代码示例
public class RedLock {
private List<RedisClient> redisClients;
public boolean tryLock(String lockKey, String clientId, long expireTime) {
long startTime = System.currentTimeMillis();
int successCount = 0;
// 向所有实例请求锁
for (RedisClient client : redisClients) {
try {
boolean success = client.set(lockKey, clientId, "NX", "PX", expireTime);
if (success) {
successCount++;
}
} catch (Exception e) {
// 忽略异常,继续下一个实例
}
}
long costTime = System.currentTimeMillis() - startTime;
// 判断是否获取锁成功
if (successCount >= (redisClients.size() / 2 + 1) &&
costTime < expireTime) {
return true;
} else {
// 释放已获取的锁
unlock(lockKey, clientId);
return false;
}
}
}
常见问题总结
1. 死锁问题
原因:锁没有过期时间或客户端宕机
解决:设置合理的过期时间 + 看门狗机制
2. 锁误删问题
原因:删除锁时没有验证锁的所有者
解决:使用Lua脚本原子性验证和删除
3. 锁过期问题
原因:业务执行时间超过锁的过期时间
解决:看门狗机制自动续期
4. 可重入问题
原因:同一线程无法多次获取同一把锁
解决:使用Hash结构记录重入次数
5. 主从切换问题
原因:主从异步复制导致锁丢失
解决:红锁算法使用多个独立实例
6. 性能问题
原因:频繁的网络交互和Lua脚本执行
优化:
- 合理设置锁粒度
- 使用连接池
- 批量操作
- 监控锁的获取成功率和耗时
7. 时钟偏移问题
原因:不同服务器时钟不同步
影响:锁的过期时间计算不准确
解决:使用NTP同步时钟,或使用相对时间
最佳实践建议
1. 锁设计原则
- 锁粒度要合适,避免粗粒度锁
- 锁的key要有业务含义
- 设置合理的超时时间
2. 异常处理
- 必须在finally块中释放锁
- 处理获取锁超时的情况
- 记录详细的日志便于排查
3. 监控告警
- 监控锁的获取成功率
- 监控锁的持有时间
- 监控Redis的性能指标
4. 降级策略
- Redis不可用时的降级方案
- 锁获取失败时的业务处理
- 本地锁作为备选方案
#分布式锁##redis#