2025-05-11
Redis
0

目录

一、避免死锁:原子性操作与超时控制
1.1 原子性加锁——SET命令与Lua脚本
1.2 超时熔断机制
二、可重入性:Hash结构与计数器设计
2.1 可重入锁的底层实现
2.2 Redisson的实现方案
三、自动续期:WatchDog机制与后台线程
3.1 WatchDog续期原理
3.2 主从一致性问题
四、加锁失败重试:自旋与退避策略
4.1 自旋重试实现
4.2 指数退避算法
五、锁失效处理:RedLock与集群容错
5.1 RedLock算法流程
5.2 锁失效场景与应对策略
六、总结与生产建议

摘要:本文系统性剖析Redis分布式锁的五大核心特性——避免死锁、可重入性、自动续期、加锁失败重试、锁失效处理,结合Lua脚本、Redisson框架及RedLock算法,通过原理分析与代码实践,构建工业级可靠的分布式锁解决方案。


一、避免死锁:原子性操作与超时控制

1.1 原子性加锁——SET命令与Lua脚本

传统的SETNX + EXPIRE存在非原子性风险,Redis 2.6.12后支持SET key value NX EX expireTime命令实现原子操作。例如:

java
Jedis jedis = new Jedis("localhost"); jedis.set("lockKey", "threadId", SetParams.setParams().nx().ex(30)); // NX=不存在时设置,EX=过期时间

释放锁时需验证线程标识,通过Lua脚本确保原子性:

lua
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

1.2 超时熔断机制

通过设置合理的expireTime避免死锁,若业务执行时间超过超时时间,锁将自动释放。例如:

java
public boolean tryLock(long timeoutSec) { String threadId = ID_PREFIX + Thread.currentThread().getId(); return stringRedisTemplate.opsForValue().setIfAbsent("lockKey", threadId, timeoutSec, TimeUnit.SECONDS); }

二、可重入性:Hash结构与计数器设计

2.1 可重入锁的底层实现

通过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]) -- 返回剩余时间

解锁时递减计数器,仅当计数器归零时删除锁:

lua
local 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

2.2 Redisson的实现方案

Redisson通过RReentrantLock类封装可重入逻辑,内部使用RedissonLockEntry维护线程计数器:

java
RLock lock = redisson.getLock("myLock"); lock.lock(); // 可重入加锁 try { // 业务逻辑 lock.lock(); // 同一线程可重复加锁 } finally { lock.unlock(); // 需调用相同次数的unlock }

三、自动续期:WatchDog机制与后台线程

3.1 WatchDog续期原理

Redisson通过后台线程定期检测锁状态并续期,防止业务执行超时释放:

java
private void renewExpiration() { String key = "lockKey"; while (isHeld()) { CompletionStage<Boolean> future = renewExpirationAsync(key); future.whenComplete((res, e) -> { if (res) scheduleRenewal(); // 续期成功则继续调度 }); } }

默认每10秒检测一次(internalLockLeaseTime/3),通过Lua脚本刷新过期时间:

lua
if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('pexpire', KEYS[1], ARGV[2]) else return nil end

3.2 主从一致性问题

若Redis采用主从架构,在主节点崩溃未同步从节点时可能导致锁失效。解决方案:

  • RedLock算法:在多个独立Redis节点加锁,需半数以上节点成功。
  • 服务端模块:如Redisson的Redis Module(如RedissonLockWatchdogService)提供强一致性保障。

四、加锁失败重试:自旋与退避策略

4.1 自旋重试实现

通过循环尝试获取锁,并设置最大重试次数与重试间隔:

java
public 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; }

4.2 指数退避算法

避免大量线程同时竞争导致惊群效应,采用指数退避:

java
int retryCount = 0; long backoff = 100; while (!tryLock() && retryCount++ < MAX_RETRIES) { long sleepTime = backoff * Math.pow(2, retryCount); Thread.sleep(sleepTime); }

五、锁失效处理:RedLock与集群容错

5.1 RedLock算法流程

部署N个独立Redis节点(N=2n+1),要求至少n+1个节点加锁成功且总耗时小于锁TTL:

java
RLock 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个节点加锁成功

5.2 锁失效场景与应对策略

场景影响解决方案
网络分区锁状态不一致RedLock算法预留时间缓冲区(默认5ms)
业务超时锁自动释放后被抢占WatchDog动态续期
时钟漂移超时计算偏差RedLock算法容忍时钟漂移

六、总结与生产建议

  1. 原子性保障:加锁与释放锁必须通过Lua脚本实现原子性。
  2. 可重入优化:使用Hash结构存储线程标识与计数器,避免递归调用死锁。
  3. 高可用设计:RedLock算法在5节点集群下可容忍2节点故障。
  4. 性能调优:合理设置maxRetries(建议3次)与retryInterval(建议100ms),避免资源浪费。

附录:完整代码示例详见Redisson官方GitHubCSDN博客