摘要:本文系统性剖析Redis分布式锁的五大核心特性——避免死锁、可重入性、自动续期、加锁失败重试、锁失效处理,结合Lua脚本、Redisson框架及RedLock算法,通过原理分析与代码实践,构建工业级可靠的分布式锁解决方案。
传统的SETNX + EXPIRE
存在非原子性风险,Redis 2.6.12后支持SET key value NX EX expireTime
命令实现原子操作。例如:
javaJedis jedis = new Jedis("localhost");
jedis.set("lockKey", "threadId", SetParams.setParams().nx().ex(30)); // NX=不存在时设置,EX=过期时间
释放锁时需验证线程标识,通过Lua脚本确保原子性:
luaif redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
通过设置合理的expireTime
避免死锁,若业务执行时间超过超时时间,锁将自动释放。例如:
javapublic boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
return stringRedisTemplate.opsForValue().setIfAbsent("lockKey", threadId, timeoutSec, TimeUnit.SECONDS);
}
通过Redis Hash结构存储线程标识与重入次数(UUID+ThreadId):
lua-- 加锁Lua脚本
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', 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]) -- 返回剩余时间
解锁时递减计数器,仅当计数器归零时删除锁:
lualocal counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if counter > 0 then
redis.call('pexpire', KEYS[1], ARGV[2]) -- 未完全释放则续期
else
redis.call('del', KEYS[1]) -- 彻底删除锁
end
Redisson通过RReentrantLock
类封装可重入逻辑,内部使用RedissonLockEntry
维护线程计数器:
javaRLock lock = redisson.getLock("myLock");
lock.lock(); // 可重入加锁
try {
// 业务逻辑
lock.lock(); // 同一线程可重复加锁
} finally {
lock.unlock(); // 需调用相同次数的unlock
}
Redisson通过后台线程定期检测锁状态并续期,防止业务执行超时释放:
javaprivate void renewExpiration() {
String key = "lockKey";
while (isHeld()) {
CompletionStage<Boolean> future = renewExpirationAsync(key);
future.whenComplete((res, e) -> {
if (res) scheduleRenewal(); // 续期成功则继续调度
});
}
}
默认每10秒检测一次(internalLockLeaseTime/3),通过Lua脚本刷新过期时间:
luaif (redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('pexpire', KEYS[1], ARGV[2])
else
return nil
end
若Redis采用主从架构,在主节点崩溃未同步从节点时可能导致锁失效。解决方案:
RedissonLockWatchdogService
)提供强一致性保障。通过循环尝试获取锁,并设置最大重试次数与重试间隔:
javapublic boolean tryLockWithRetry(String key, String value, long expireTime, int maxRetries, long retryInterval) {
for (int i = 0; i < maxRetries; i++) {
if (lock(key, value, expireTime)) return true;
try { Thread.sleep(retryInterval); } catch (InterruptedException e) {}
}
return false;
}
避免大量线程同时竞争导致惊群效应,采用指数退避:
javaint retryCount = 0;
long backoff = 100;
while (!tryLock() && retryCount++ < MAX_RETRIES) {
long sleepTime = backoff * Math.pow(2, retryCount);
Thread.sleep(sleepTime);
}
部署N个独立Redis节点(N=2n+1),要求至少n+1个节点加锁成功且总耗时小于锁TTL:
javaRLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock(); // 等待N/2+1个节点加锁成功
场景 | 影响 | 解决方案 |
---|---|---|
网络分区 | 锁状态不一致 | RedLock算法预留时间缓冲区(默认5ms) |
业务超时 | 锁自动释放后被抢占 | WatchDog动态续期 |
时钟漂移 | 超时计算偏差 | RedLock算法容忍时钟漂移 |
maxRetries
(建议3次)与retryInterval
(建议100ms),避免资源浪费。附录:完整代码示例详见Redisson官方GitHub与CSDN博客