Redisson 进阶实战:看门狗机制与分布式锁完全指南

聊到 Redis 就有一个话题始终绕不开------分布式锁

用原生 Redis 手写分布式锁,需要自己处理锁超时、锁重入、锁释放等一系列边界问题,稍有不慎就是线上事故。Redisson 正是为解决这些问题而生的------它是 Redis 的 Java 客户端,但远不止"客户端"这么简单。

这篇文章从 Redisson 最核心的看门狗机制讲起,再展开它的完整能力矩阵。

主线:从"锁会过期"到"锁会自己续命"

第一站:看门狗机制------锁快过期了,谁来续命?

为什么需要看门狗?

分布式锁必须设置过期时间,否则持锁线程宕机后锁永远无法释放,造成死锁。

但过期时间设多少合适?

  • 设短了 :业务还没执行完,锁就自动过期了,其他线程趁虚而入,数据不一致
  • 设长了 :持锁线程宕机后,其他线程要等很久才能拿到锁,系统可用性下降

看门狗(Watch Dog) 就是来解决这个矛盾的------它让锁的过期时间随业务执行动态调整

核心机制:30 秒 + 10 秒

看门狗的核心逻辑可以用三个数字概括:

数字 含义 说明
30 秒 锁的初始过期时间 lockWatchdogTimeout 默认值
10 秒 续期检查间隔 30 秒的 1/3,即 internalLockLeaseTime / 3
30 秒 每次续期后的新过期时间 每次续期都把 TTL 重置回 30 秒

续期操作的底层实现

看门狗的续期不是简单发一个 EXPIRE 命令,而是通过一段 Lua 脚本在 Redis 端原子完成:

lua 复制代码
-- 检查锁是否存在且被当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 重新设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1;
end;
return 0;

为什么必须用 Lua 脚本?

因为续期操作包含"检查 + 设置"两个步骤。如果分两次请求发送,中间可能发生:

  1. 客户端 A 检查锁存在 → 返回 OK
  2. 此时网络延迟,锁已过期被释放
  3. 客户端 B 抢到了锁
  4. 客户端 A 收到"锁存在"的响应,继续执行 EXPIRE给 B 的锁续期了!

Lua 脚本在 Redis 中原子执行,杜绝了这种竞态问题。

续期的触发条件

看门狗并非在所有加锁场景下都会启动 。只有满足以下条件之一时才会触发

这意味着 :如果你调用了 lock.lock(10, TimeUnit.SECONDS),看门狗就不会工作,锁在 10 秒后无条件释放 ------即使业务还没执行完

核心要点 :使用 lock() 无参方法才能享受看门狗的自动续期。如果你明确知道业务最长执行时间,可以手动指定 leaseTime,此时需要自己确保业务能在超时前完成。

源码核心逻辑

看门狗的启动逻辑位于 tryAcquire 方法中

java 复制代码
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    Long ttl = null;
    if (leaseTime != -1) {
        // 指定了过期时间,直接加锁,不启动看门狗
        ttl = tryLockInnerAsync(leaseTime, unit, threadId, ...);
    } else {
        // 未指定过期时间,使用看门狗超时时间(默认30秒)
        ttl = tryLockInnerAsync(
            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, ...
        );
        // 启动看门狗
        scheduleExpirationRenewal(threadId);
    }
    return ttl;
}

续期任务的调度采用递归调用 的方式实现循环续期

java 复制代码
private void renewExpiration() {
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ee == null) return;
            // 通过Lua脚本延长锁的过期时间
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((success, e) -> {
                if (success) {
                    // 续期成功,递归调用,实现循环续期
                    renewExpiration();
                }
            });
        }
        // 续期间隔为看门狗超时时间的1/3
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    ee.setTimeout(task);
}

看门狗什么时候停止?

看门狗非常"守规矩",只在锁真正被持有期间工作

停止场景 触发方式
正常释放锁 业务代码调用 unlock(),从全局 EXPIRATION_RENEWAL_MAP 中移除续期任务
线程异常中断 当前线程被中断或异常退出,续期任务自动清理
客户端实例关闭 Redisson 客户端关闭时自动清理所有续期任务
锁被其他客户端强制删除 下次续期脚本检测失败,递归终止

看门狗的底层定时器

看门狗不是靠"轮询线程池"或"额外守护线程"实现的,而是使用 Netty 的 HashedWheelTimer 来实现轻量、精准的异步续期。这也是 Redisson 底层基于 Netty 通信架构的一个体现------它复用了 Netty 的时间轮来调度续期任务,不需要额外创建线程。

关键配置参数

java 复制代码
Config config = new Config();
// 看门狗超时时间(默认 30000 毫秒)
config.setLockWatchdogTimeout(30000);

注意lockWatchdogTimeout 设置过短(如 100ms)可能导致锁频繁续期失败,建议保持默认值 30 秒或适当调大。

第二站:Redisson 锁家族------不只是可重入锁

Redisson 提供了一整套分布式锁实现,通过统一的 RLock 接口暴露。不同锁类型在可重入性、公平性、互斥范围 上各有取舍

可重入锁(Reentrant Lock)

这是 Redisson 默认提供的分布式锁,也是使用最广泛的一种。核心特性:

  • 可重入:同一个线程可以多次获取同一把锁,不会自己阻塞自己。
  • 看门狗续期:默认启用自动续期。
  • 阻塞等待:获取不到锁时,通过 Pub/Sub 等待锁释放通知,而非空轮询。

底层实现 :Redisson 的锁本质上是一个 Hash 结构 ------Key 是锁名,Field 是 UUID:ThreadId,Value 是重入次数

text

lua 复制代码
锁名: "myLock"
+--------------------------------------+
| UUID:ThreadId  |  重入次数           |
| abc123:1       |  2                  |  ← 同一线程重入了2次
+--------------------------------------+
TTL: 30秒(看门狗每10秒续期一次)

使用示例

java 复制代码
@Autowired
private RedissonClient redissonClient;

public void doBusiness(Long goodsId) {
    RLock lock = redissonClient.getLock("lock:" + goodsId);
    try {
        lock.lock();  // 默认启用看门狗
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}

公平锁(Fair Lock)

普通锁在竞争时是非公平的------锁释放后,所有等待线程同时被唤醒,谁先抢到算谁的。

公平锁保证 FIFO(先进先出) ------先请求锁的线程一定先获得锁,不会出现"插队"现象。

实现原理 :公平锁在 Redis 中维护一个等待队列(基于 Redis List 或 ZSet),按请求时间排序。锁释放后,只唤醒队列头部的线程。

代价:公平锁的性能略低于非公平锁,因为需要维护队列和按顺序唤醒。大多数场景用默认的可重入锁就够了。

读写锁(ReadWriteLock)

允许多个线程同时读 ,但只允许一个线程。适用于读多写少的场景。

实现原理 :读写锁在 Redis 中用一个 Hash 结构 存储,加锁时通过 mode 字段区分读锁还是写锁

text 复制代码
锁名: "anyRWLock"
+--------------------------------------------------+
| mode              | read                          |
| UUID:ThreadId     | 1(读锁重入次数)              |
| {anyRWLock}:UUID:ThreadId:rwlock_timeout:1 | 1  |
+--------------------------------------------------+
  • 读锁:多个线程可以同时持有,每加一次锁,重入计数 +1。
  • 写锁:与所有读锁、其他写锁互斥。
  • 锁升级/降级:Redisson 读写锁支持锁降级(写锁降为读锁),但不支持锁升级(读锁升为写锁),因为升级容易导致死锁。
java 复制代码
RReadWriteLock rwLock = redissonClient.getReadWriteLock("myRWLock");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();

// 读操作
readLock.lock();
try { /* 读取数据 */ } finally { readLock.unlock(); }

// 写操作
writeLock.lock();
try { /* 修改数据 */ } finally { writeLock.unlock(); }

联锁(MultiLock)

当需要同时锁定多个资源 时使用。联锁会把多个 RLock 组合成一个逻辑锁,只有所有锁都获取成功才算加锁成功。任意一个获取失败,已获取的锁会被释放。

java 复制代码
RLock lock1 = redissonClient.getLock("lock1");
RLock lock2 = redissonClient.getLock("lock2");
RLock lock3 = redissonClient.getLock("lock3");
RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);

multiLock.lock();
try { /* 业务逻辑 */ } finally { multiLock.unlock(); }

红锁(RedLock)------ 不推荐使用

RedLock 是 Redis 作者 Antirez 提出的算法,需要在多个独立的 Redis 节点上同时加锁,超过半数成功才算加锁成功。

为什么不推荐?

  1. 性能差:每次加锁需要访问 N 个节点,网络开销大。
  2. 运维复杂:需要维护多个独立 Redis 实例,不能使用主从或集群。
  3. 争议大:分布式系统专家 Martin Kleppmann 曾与 Antirez 就 RedLock 的安全性展开过激烈辩论,核心争议在于时钟漂移场景下 RedLock 无法保证安全性。
  4. 有替代方案:对于需要强一致性的场景,建议使用 ZooKeeper 或 etcd 的分布式锁。

建议 :除非有非常特殊的强一致性需求,否则不要使用 RedLock。Redisson 从 3.17.0 版本开始已经弃用 RedLock,官方推荐使用普通的 RLock 配合哨兵/集群模式。

锁类型对比总结

锁类型 可重入 公平/FIFO 互斥范围 典型场景
可重入锁 单资源 通用互斥
公平锁 单资源 需要严格 FIFO 顺序
读写锁 读共享/写互斥 读多写少
联锁 多资源同时锁定 跨资源原子操作
红锁 多节点 不推荐

第三站:分布式限流器 RRateLimiter

除了分布式锁,Redisson 还提供了开箱即用的分布式限流器。其底层基于令牌桶算法 实现

核心原理

  • 系统以固定速率向桶中添加令牌。
  • 请求需要获取令牌才能被处理,没有令牌则被拒绝。
  • 通过调整令牌生成速率和桶容量,可精确控制请求速率。
  • 令牌桶的状态存储在 Redis 中,多节点共享,确保分布式环境下的一致性。

使用示例

java 复制代码
// 获取限流器(每秒生成5个令牌,桶容量为5)
RRateLimiter rateLimiter = redissonClient.getRateLimiter("api:limiter");
rateLimiter.trySetRate(RateType.OVERALL, 5, 1, RateIntervalUnit.SECONDS);

// 非阻塞获取令牌
boolean acquired = rateLimiter.tryAcquire(1);
if (acquired) {
    // 处理请求
} else {
    // 拒绝请求(限流)
}

// 阻塞式获取令牌(最多等待5秒)
boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS);

两种限流模式

模式 说明 适用场景
OVERALL 全局限流,所有节点共享同一个令牌桶 全局限流(如全局限流 1000 QPS)
PER_CLIENT 客户端级限流,每个节点独立计数 单节点限流(如单节点限流 200 QPS)

第四站:延迟队列 RDelayedQueue

Redisson 的延迟队列用于实现"延迟消费"场景,比如订单 30 分钟未支付自动取消。

核心原理

RDelayedQueue 的设计很巧妙------它并非一个单一的 Redis 数据结构,而是结合了 ZSET(有序集合)LIST(列表,即 RBlockingQueue) 来实现的

三个队列的分工

队列 结构 作用
消息延时队列 ZSET(score=到期时间戳) 按到期时间排序,快速找到下一个到期的消息
消息顺序队列 LIST 按消息添加顺序存储,移除消息时按顺序删除
消息目标队列 LIST 存放到期的消息,供消费端阻塞获取

使用示例

java 复制代码
// 生产者端
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue("delay-queue");
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);

// 发送延迟5秒的消息
delayedQueue.offer("测试延迟消息", 5, TimeUnit.SECONDS);

// 消费者端
String msg = blockingQueue.take(); // 阻塞等待到期消息
// 处理业务逻辑

第五站:布隆过滤器 RBloomFilter

Redisson 封装了分布式布隆过滤器,用于高效判断元素是否存在

核心原理

布隆过滤器是一种概率型数据结构,基于哈希函数和位数组实现:

  • 添加元素:将元素通过多个哈希函数映射到位数组的多个位置,置为 1。

  • 判断存在

    • 若所有对应位全为 1 → 可能存在(有误判概率)。
    • 若存在任意一位为 0 → 一定不存在

Redisson 将位数组存储在 Redis 中,支持跨节点共享,误判率默认为 0.03(3%)

使用示例

java 复制代码
// 初始化布隆过滤器(预计100万个元素,误判率1%)
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("userBloomFilter");
bloomFilter.tryInit(1000000, 0.01);

// 添加元素
bloomFilter.add("user123");

// 判断元素是否存在
boolean mightExist = bloomFilter.contains("user123"); // 可能存在
boolean notExist = bloomFilter.contains("unknown");   // 一定不存在

第六站:最佳实践------避坑指南

看门狗的坑

说明 解决方案
指定了 leaseTime,看门狗不续期 调用 lock.lock(10, SECONDS) 时看门狗不启动 使用 lock() 无参方法
看门狗续期失败 Redis 连接断开导致续期失败 确保 Redis 高可用(哨兵/集群),监控续期失败日志
业务执行时间过长 即使续期,业务线程也可能被中断 设置合理业务超时,避免无限循环

锁使用的最佳实践

核心避坑要点

  1. 必须在 finally 块中释放锁------否则业务异常会导致锁无法释放,即使有看门狗也会造成死锁。
  2. 获取锁后,再发起加锁判断 ------Redisson 的 unlock() 会检查当前线程是否持有锁,未持有会抛异常。
  3. Redisson 锁是"咨询锁" ------它只协调通过相同锁对象加锁的客户端,不阻止直接操作 Redis 的客户端
  4. 主从切换场景下的锁安全 :Redisson 默认启用了从节点同步检查 ------加锁时会等待锁写入至少一个从节点才返回成功,降低主从切换导致锁丢失的风险

终章:Redisson 能力全景

问题 Redisson 方案 一句话
分布式锁怕超时 看门狗机制 30 秒超时,10 秒续期,业务没结束锁就一直续
多线程同时读 读写锁 读共享、写互斥,读多写少场景利器
需要 FIFO 顺序 公平锁 先到先得,不插队
同时锁多个资源 联锁 要么全锁,要么全不锁
限制请求速率 RRateLimiter 令牌桶算法,多节点共享限流
延迟消费 RDelayedQueue ZSET + LIST,到期自动转移
防缓存穿透 RBloomFilter 跨节点共享,高效去重判断

Redisson 的核心价值在于:它把分布式场景下的各种并发问题,都封装成了开箱即用的组件。 看门狗解决了锁续期的痛点,读写锁和公平锁提供了更细粒度的并发控制,延迟队列和布隆过滤器则覆盖了更多业务场景。理解了这些组件的底层原理,才能在实战中选对工具、避开陷阱。

相关推荐
淘源码d1 小时前
基于Java开发的上门家政系统源码,采用SpringBoot+MySQL+UniApp+Vue3技术栈,构建三端一体化平台
java·源码·上门家政·家政系统·同城家政·源码部署
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之多因子认证逻辑与实体类改造
java·开发语言·spring
ashiho1 小时前
【Spring】RequestParam/PathVariable/RequestBody详解
java·后端·spring·servlet·springmvc
Charlie_Byte1 小时前
Hugo 相对路径图片显示问题的分析与解决
后端
Escalating_xu1 小时前
【mmap 进程间通信】从共享内存到跨进程唤醒:用互斥锁、条件变量控制多个进程
java·linux·开发语言·jvm
Charlie_Byte1 小时前
用 conda 固定默认环境 normal
后端
泡海椒2 小时前
脚本包导入实战:JQuick-Java自定义包引入与别名配置教程
java·开发语言·python
隐退山林2 小时前
JavaEE进阶:SpringBoot统一功能处理
java·spring boot·后端