方案①:定时任务轮询
核心原理
写一个定时任务(比如用 Spring Task),每隔固定时间(比如 1 分钟)去数据库里查一遍「到期 / 超时」的数据,查到后执行业务逻辑。
举个例子:订单关单场景,就是定时任务查 expire_time <= now() AND status = '待支付' 的订单,然后批量关单。
| 特点 | 说明 |
|---|---|
| ✅ 优点:「准」 | 数据存在数据库里,只要定时任务执行了,就一定会查到并处理,不会丢数据,可靠性拉满。 |
| ❌ 缺点:「时效差」 | 延迟精度由轮询间隔决定:比如 1 分钟轮询一次,订单最多会晚 1 分钟才被关闭,做不到秒级精准延时。 |
隐藏坑点
- 资源浪费:不管有没有到期数据,都要频繁查库,高并发场景下会给数据库带来不必要的压力。
- 重复执行:分布式部署多个定时任务实例时,会同时查库导致重复处理,必须加分布式锁(比如 Redis 锁)控制。
- 扩展性差:如果业务需要不同的延时时间(比如有的订单 10 分钟超时,有的 30 分钟),轮询间隔不好设置,设小了浪费,设大了不满足需求。
适用场景
对延时精度要求低(分钟级以上)、数据量不大的非核心场景,比如:
- 每日凌晨对账、用户每周签到提醒
- 非紧急的订单超时兜底(和其他方案搭配使用)
三、方案②:Redis Key 过期监听
核心原理
利用 Redis 的Key 过期事件通知机制 :给需要延时处理的业务数据(比如订单 ID)设置一个带过期时间的 Key,比如 setex order:123 1800 1(30 分钟过期)。
当 Key 过期时,Redis 会发布一个 expired 事件,客户端监听这个事件,收到后就去执行关单逻辑。
优缺点 & 对应你笔记里的点
| 特点 | 说明 |
|---|---|
| ✅ 优点:实现简单 | 不用额外组件,只要 Redis,配置一下就能用,延时精度比定时任务好(能到秒级)。 |
| ❌ 缺点:「丢失」 | 这是这个方案的致命问题!Redis 的过期事件通知是不可靠的发布订阅模式,如果消费者掉线、Redis 主从切换、网络波动,事件就会直接丢失,而且 Redis 不会补发。 |
隐藏坑点
- 配置门槛 :Redis 默认关闭过期事件通知,必须修改配置
notify-keyspace-events Ex才能收到事件,很多新手会踩这个坑。 - 时间不准:Redis 的 Key 过期是「惰性删除 + 定期删除」,不是到点就删,可能会出现 Key 过期了但 Redis 没扫到,导致事件延迟触发。
- 无法持久化:如果 Redis 宕机,Key 的过期信息直接丢失,重启后也不会补发事件。
- 主从限制:主节点的过期事件不会同步到从节点,只能监听主节点,主从切换后事件会断流。
适用场景
只适合对可靠性要求极低、允许少量消息丢失的非核心场景,比如:
- 用户登录态过期提醒、非核心缓存清理
- 不建议用在订单关单、支付回调这类核心业务,丢事件会导致资损。
四、方案③: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 也能做,但要设计好死信队列和路由。
关键注意点
-
消费者必须查数据库状态
不能消息到了就直接改状态。
-
消费要幂等
MQ 可能重复投递。
-
要有失败重试
消费失败不能直接丢。
-
最好有定时任务兜底
延时队列负责实时触发,定时任务负责补偿。
-
延时精度不要要求太高
MQ 延时队列适合"分钟级/秒级业务触发",不是高精度定时器。
-
大量延时消息会占 Broker 资源
高并发场景要评估消息堆积和存储压力。