← 全部文章

Redisson 延迟队列

分布式系统中的延迟队列解决方案

更新于 2026.03.27

本文目录 7 节
Redisson 延迟队列封面

底层实现

先看整体结构——Redisson延迟队列底层由 Redis 中的两个数据结构配合实现。 Redisson延迟队列数据结构和流程图 底层其实就是 Redis 里的两个原生数据结构:一个 zset(有序集合)充当”等候室”,一个 list 充当真正的队列。

运行流程

第一步:offer() 投递消息

调用 delayedQueue.offer("START:123", 5000, MILLISECONDS) 时,Redisson 做的事情很简单:把消息写入 zset,score 设为 当前时间戳 + 5000ms,即”到期时间”。消息就静静躺在 zset 里,没有任何线程在等它。

第二步:内部定时任务轮询

Redisson 客户端启动时,会在后台开启一个定时任务(默认每 500ms 执行一次),执行一段 Lua 脚本,逻辑大致是:

-- 从 zset 中取出所有 score ≤ 当前时间戳的消息
local msgs = redis.call('zrangebyscore', zset_key, 0, now)
-- 把它们逐个 lpush 到 list 中
for _, msg in ipairs(msgs) do
    redis.call('lpush', list_key, msg)
    redis.call('zrem', zset_key, msg)
end

这一步是原子的(Lua 脚本在 Redis 里原子执行),所以不会出现”取出来但没放进去”的情况。

第三步:blockingQueue.take() 消费消息

消费者线程调用 blockingQueue.take(),底层是 Redis 的 BRPOP 命令——没消息时阻塞挂起,有消息时立刻返回。消费者拿到消息后,根据消息类型进行消费。 因此,在使用Redisson充当延迟队列时,常常需要两个Queue。分别是 RBlockingQueue 和 RDelayedQueue。 前者用来消费,后者用来投递,它们操作的是同一个 zset+list 组合。

生产与消费示例

// 1. 获取目标阻塞队列
RBlockingDeque<String> destinationQueue = redissonClient.getBlockingDeque("orderQueue");

// 2. 获取延迟队列(装饰目标队列)
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(destinationQueue);

// 3. 生产者:发送延迟消息
// 10秒后消息会进入 orderQueue
delayedQueue.offer("order_12345", 10, TimeUnit.SECONDS);

// 4. 消费者:在另一个线程或服务中
new Thread(() -> {
    while (true) {
        try {
            // 注意:消费时直接监听 destinationQueue
            String orderId = destinationQueue.take();
            System.out.println("收到过期订单: " + orderId);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

总结

Redisson 延迟队列的本质是用 Redis 原生数据结构模拟的定时投递机制,整个流程可以拆解为三个角色的协作:

RDelayedQueue(投递方):负责将消息写入 zset,score 为到期时间戳,消息在此躺着,什么也不干。 Redisson 内部定时任务(搬运工):每 500ms 扫描一次 zset,将到期消息原子地转移到 list,触发消费。 RBlockingQueue(消费方):底层通过 BRPOP 阻塞监听 list,消息一到立刻唤醒消费者线程处理。

相比 JDK 自带的 ScheduledExecutorService,这套机制最大的优势是调度状态外置于 Redis——消息不在进程内存中,服务重启后消息不丢失,多实例部署时也不会重复触发(配合分布式锁使用)。代价是引入了 500ms 左右的轮询延迟,以及对 Redis 可用性的依赖。 因此,它适合对触发精度要求在秒级、需要跨进程可靠调度的场景,

评论