延时消息的几种实现方式及优缺点

方案①:定时任务轮询

核心原理

写一个定时任务(比如用 Spring Task),每隔固定时间(比如 1 分钟)去数据库里查一遍「到期 / 超时」的数据,查到后执行业务逻辑。

举个例子:订单关单场景,就是定时任务查 expire_time <= now() AND status = '待支付' 的订单,然后批量关单。

特点 说明
✅ 优点:「准」 数据存在数据库里,只要定时任务执行了,就一定会查到并处理,不会丢数据,可靠性拉满。
❌ 缺点:「时效差」 延迟精度由轮询间隔决定:比如 1 分钟轮询一次,订单最多会晚 1 分钟才被关闭,做不到秒级精准延时。

隐藏坑点

  1. 资源浪费:不管有没有到期数据,都要频繁查库,高并发场景下会给数据库带来不必要的压力。
  2. 重复执行:分布式部署多个定时任务实例时,会同时查库导致重复处理,必须加分布式锁(比如 Redis 锁)控制。
  3. 扩展性差:如果业务需要不同的延时时间(比如有的订单 10 分钟超时,有的 30 分钟),轮询间隔不好设置,设小了浪费,设大了不满足需求。

适用场景

对延时精度要求低(分钟级以上)、数据量不大的非核心场景,比如:

  • 每日凌晨对账、用户每周签到提醒
  • 非紧急的订单超时兜底(和其他方案搭配使用)

三、方案②:Redis Key 过期监听

核心原理

利用 Redis 的Key 过期事件通知机制 :给需要延时处理的业务数据(比如订单 ID)设置一个带过期时间的 Key,比如 setex order:123 1800 1(30 分钟过期)。

当 Key 过期时,Redis 会发布一个 expired 事件,客户端监听这个事件,收到后就去执行关单逻辑。

优缺点 & 对应你笔记里的点

特点 说明
✅ 优点:实现简单 不用额外组件,只要 Redis,配置一下就能用,延时精度比定时任务好(能到秒级)。
❌ 缺点:「丢失」 这是这个方案的致命问题!Redis 的过期事件通知是不可靠的发布订阅模式,如果消费者掉线、Redis 主从切换、网络波动,事件就会直接丢失,而且 Redis 不会补发。

隐藏坑点

  1. 配置门槛 :Redis 默认关闭过期事件通知,必须修改配置 notify-keyspace-events Ex 才能收到事件,很多新手会踩这个坑。
  2. 时间不准:Redis 的 Key 过期是「惰性删除 + 定期删除」,不是到点就删,可能会出现 Key 过期了但 Redis 没扫到,导致事件延迟触发。
  3. 无法持久化:如果 Redis 宕机,Key 的过期信息直接丢失,重启后也不会补发事件。
  4. 主从限制:主节点的过期事件不会同步到从节点,只能监听主节点,主从切换后事件会断流。

适用场景

只适合对可靠性要求极低、允许少量消息丢失的非核心场景,比如:

  • 用户登录态过期提醒、非核心缓存清理
  • 不建议用在订单关单、支付回调这类核心业务,丢事件会导致资损。

四、方案③:MQ 延时队列(RabbitMQ / RocketMQ)

一、什么是 MQ 延时队列

普通 MQ:

复制代码
生产者发送消息 -> Broker -> 消费者立刻消费

延时队列:

复制代码
生产者发送消息 -> Broker 暂存一段时间 -> 到期后投递给消费者

注意:延时消息一般不是"到时间必然执行成功",而是"到时间后触发一次检查"。业务上还要查数据库状态,不能只相信消息。


二、RabbitMQ 延时队列

RabbitMQ 本身原生没有特别完善的延时消息能力,常见有两种实现方式。

方式一:TTL + 死信队列

这是最经典的 RabbitMQ 延时队列方案。

核心组件:

复制代码
TTL:消息过期时间
DLX:Dead Letter Exchange,死信交换机
死信队列:接收过期消息的队列

流程:

复制代码
生产者发送消息到延时队列
消息在延时队列中等待 TTL 时间
消息过期后变成死信
RabbitMQ 把死信转发到死信交换机
死信交换机路由到真正的消费队列
消费者消费消息

订单超时示例:

复制代码
order.delay.queue:延时队列,设置 TTL = 30 分钟
order.close.exchange:死信交换机
order.close.queue:真正消费关闭订单消息的队列

流程:

复制代码
创建订单 -> 发消息到 order.delay.queue
30 分钟后消息过期 -> 进入 order.close.queue
消费者消费 -> 查询订单是否未支付 -> 关闭订单

优点:

复制代码
实现简单
不需要额外插件
适合固定延时时间

缺点:

复制代码
不适合大量不同延时时间
可能有队头阻塞问题
延迟精度一般
管理起来相对麻烦

队头阻塞是什么意思?

假设队列里第一条消息延迟 30 分钟,第二条消息延迟 5 分钟。

如果 RabbitMQ 按队列顺序检查过期,第二条消息可能被第一条挡住,不能准时投递。

所以 TTL + 死信队列更适合:

复制代码
固定延时,比如统一 30 分钟关闭订单
统一 10 分钟重试
统一 24 小时提醒

三、RocketMQ 延时队列

RocketMQ 对延时消息支持更直接。

早期 RocketMQ 版本主要支持 固定延时等级

例如:

复制代码
1s
5s
10s
30s
1m
2m
3m
4m
5m
6m
7m
8m
9m
10m
20m
30m
1h
2h

发送消息时指定延时等级:

复制代码
message.setDelayTimeLevel(16);

比如 level 16 可能代表 30 分钟,具体看 broker 配置。

流程:

复制代码
生产者发送延时消息
RocketMQ 根据延时等级暂存消息
到时间后投递到真实 Topic
消费者消费消息

订单超时关闭:

复制代码
创建订单
发送延时等级为 30 分钟的消息
30 分钟后消费者收到消息
查询订单状态
未支付则关闭订单

RocketMQ 的优点:

复制代码
原生支持延时消息
使用简单
适合订单超时、支付超时、定时触发类业务
吞吐能力较强

缺点:

复制代码
早期版本延时时间不够灵活,只能选固定等级
不适合特别精确的定时任务
大量长时间延时消息也要关注 Broker 存储压力

现在一些新版本 RocketMQ 对定时/延时消息支持更灵活,但面试时你可以先讲经典的延时等级机制。


四、RabbitMQ 和 RocketMQ 对比

复制代码
RabbitMQ:
更常见方案是 TTL + 死信队列,或者延时插件。
适合固定延时、业务量中等、已有 RabbitMQ 技术栈的系统。

RocketMQ:
原生支持延时等级消息。
适合订单、支付、交易类场景,尤其是电商、金融、履约系统。

简单对比:

复制代码
实现复杂度:
RocketMQ 更简单,RabbitMQ TTL + DLX 更绕。

延时灵活性:
RabbitMQ 插件比较灵活;RabbitMQ TTL + DLX 不太适合动态延时。
RocketMQ 早期固定等级,灵活性有限。

业务适配:
RocketMQ 更适合交易链路里的延时消息。
RabbitMQ 也能做,但要设计好死信队列和路由。

关键注意点

  1. 消费者必须查数据库状态

    不能消息到了就直接改状态。

  2. 消费要幂等

    MQ 可能重复投递。

  3. 要有失败重试

    消费失败不能直接丢。

  4. 最好有定时任务兜底

    延时队列负责实时触发,定时任务负责补偿。

  5. 延时精度不要要求太高

    MQ 延时队列适合"分钟级/秒级业务触发",不是高精度定时器。

  6. 大量延时消息会占 Broker 资源

    高并发场景要评估消息堆积和存储压力。

相关推荐
极客先躯1 小时前
高级java每日一道面试题-2026年02月08日-实战篇[Docker]-如何实现容器的快照和恢复?
java·运维·docker·容器·备份·持久化·恢复
布朗克1681 小时前
29 反射机制
java·开发语言·反射
San813_LDD1 小时前
[数据结构]共享栈与双端队列:算法思想分析及C语言实现
java·开发语言·数据结构
我是一颗柠檬1 小时前
【Java项目技术亮点】全链路分层限流:从网关到数据库的多层防护体系
java·开发语言·数据库
wuminyu1 小时前
Java锁膨胀机制之偏向锁到轻量级锁源码剖析
java·linux·c语言·jvm·c++
码不停蹄的玄黓1 小时前
SpringBoot 实现拦截器
java·spring boot·后端
狗凯之家源码网1 小时前
永夜大圣 H5 棋牌大厅源码效果实测与品质解析
java·开发语言
凡人叶枫1 小时前
Effective C++ 条款13:以对象管理资源(RAII)
java·linux·开发语言·c++·嵌入式开发
小马爱打代码1 小时前
Java开发:Spring Cloud Alibaba微服务之消息队列(RocketMQ、Kafka、RabbitMQ)
java·java-rocketmq·java-rabbitmq