方案对比
选延时队列方案时,最容易犯的错误是只问”支不支持延时”。真正决定方案的,是四个维度:原生支持(要不要自己造轮子)、精度(能接受多大的误差)、吞吐(能不能扛住突发)、运维成本(谁来维护这套延时机制)。中间还夹着两个隐性维度:可取消性和顺序性——前者决定”订单付款后能不能撤销取消任务”,后者决定”同一订单的消息是否还有序”。
下表是四种主流消息系统在这四个维度上的横向对比。
| 维度 | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|
| 延时支持 | 插件实现 | 需自实现 | 内置支持 |
| 延时精度 | 毫秒级 | 秒级 | 毫秒级 |
| 最大延时 | 有限制 | 无限制 | 18 天 |
| 吞吐量 | 中等 | 极高 | 高 |
| 可靠性 | 高 | 高 | 高 |
| 复杂度 | 中 | 高 | 中 |
| 适用场景 | 中小规模 | 大规模 | 大规模 |
关于表中的”18 天”
RocketMQ 的”18”指的是 18 个预设的延迟级别,不是 18 天。经典的 18 级是 1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h,单级最长 2 小时,超过 2 小时需要 4.x/5.x 的定时消息能力或自行改造。级别是离散的,级别之间靠档位取整,实际触发可能比预期晚一整档。
把 Pulsar 和四个维度一起放进来,是一张更完整的选型表:
| 维度 | RabbitMQ | Kafka | RocketMQ | Pulsar |
|---|---|---|---|---|
| 原生支持 | ❌ 依赖 rabbitmq_delayed_message 插件 | ❌ 无原生延时 | ✅ 内置延迟级别(5.x 支持定时消息) | ⚠️ 以消费端延迟投递为主,无生产端原生延迟级别 |
| 延时精度 | 毫秒级(插件按毫秒调度) | 秒级(取决于轮询间隔) | 毫秒级到秒级(级别离散,精度受档位影响) | 毫秒级(消费端 deliverAt 语义) |
| 最大延时 | 受插件实现限制,量级在数十天 | 理论无限制 | 传统级别最长 2 小时,定时消息可覆盖更长周期 | 取决于 broker 缓冲能力 |
| 吞吐量 | 中等(万级 TPS) | 极高(百万级 TPS) | 高(十万级 TPS) | 高(多租户、分层存储) |
| 可靠性 | 高(持久化队列 + ACK + 镜像) | 高(副本 + acks=all) | 高(主从 + DLedger 等) | 高(BookKeeper 多副本写入) |
| 运维成本 | 中(插件需匹配版本,升级要重装) | 高(分区/保留期/消费位点都要自己管) | 中(内置能力,运维相对简单) | 高(Broker + BookKeeper 两层,组件多) |
| 可取消性 | ⚠️ 插件队列里的消息无法直接删除 | ❌ 只能靠消费端跳过 | ⚠️ 同上,一般靠业务状态兜底 | ❌ 同上 |
| 顺序性 | 单队列内有序 | 分区内严格有序 | 分区内有序 | KeyShared 模式下有序 |
关于 Pulsar 的延迟投递
Pulsar 的延时能力主要落在消费端:消费者可以指定”这条消息在某个时间戳之后再投递给我”,由 broker 侧缓冲到期前的消息。这和 RocketMQ 那种”生产端就指定延时级别”不是一回事——语义位置不同,broker 侧的缓冲压力也不同。生产端任意延时通常仍要靠自建延迟 topic 加搬运服务。
除了 broker 自身的能力,延时队列还有三个通用能力必须在选型时问清楚:
| 通用能力 | 为什么重要 | 各家的现状 |
|---|---|---|
| 到期时刻可控 | 能不能精确到”某个时间点”而不是”再等 N 秒” | RocketMQ 5.x 定时消息、Pulsar deliverAt 支持;Kafka 需自建 |
| 可撤销 | 订单已支付,取消任务应失效 | 四家都不支持真正的删除,只能靠消费端查状态后跳过 |
| 积压可控 | 某个业务突增会不会拖垮其他业务 | 需要分区隔离或队列隔离,四家都要自己设计 |
最后一条是实践中最容易被忽略的。共用一个 topic 意味着共用一个延迟队列,一个大客户的批量任务会把其他客户的延时消息一起压住。隔离能力比延时精度更重要,因为精度差一点只是晚几秒,隔离没做好是整条业务链路停摆。
是否已有 MQ 基础设施?
├─ 是 → 使用已有 MQ
│ ├─ RabbitMQ → 使用延时插件
│ ├─ Kafka → 分区策略实现
│ └─ RocketMQ → 使用内置延时
│
└─ 否 → 专用方案
├─ 中小规模 → Redis ZSet
└─ 大规模 → 时间轮 + 持久化
这张决策树的方向是对的,但它隐含了一个前提:你已经接受”用 MQ 做延时队列”这件事本身。在实际选型时,更前置的问题应该是”这件事到底该不该用 MQ”。
先问三个前置问题。
- 延时能不能被业务状态覆盖? 如果订单付款后取消任务就失效,那么”精确到毫秒触发”其实没有价值——真正重要的是”最晚不要超过 N 分钟触发”。这种情况下,数据库轮询加一个合适的索引(见《索引优化》)往往比任何 MQ 都简单可靠。
- 任务量到底有多大? 每天几千个任务,Redis 单实例加一个 ZSet 就够了;每天上千万还要求毫秒精度,才值得引入 MQ 的复杂度。
- 可取消性重要吗? 重要的话,四家 MQ 都不擅长,任务事实应该落在数据库,中间件只做触发。
再按基础设施选型。
| 现状 | 推荐 | 理由 |
|---|---|---|
| 已有 RocketMQ | 直接用内置延迟级别 | 零开发成本,精度够用,注意级别离散 |
| 已有 RabbitMQ | 用 rabbitmq_delayed_message 插件 | 插件成熟,注意版本匹配和最大延时上限 |
| 已有 Kafka,且日志吞吐极高 | 分区策略或分级延迟 topic | 复用现有设施,但要自建搬运或调度逻辑 |
| 已有 Pulsar | 消费端延迟投递 | 消费端语义,注意 broker 缓冲压力 |
| 没有 MQ,任务量不大 | Redis ZSet | 实现简单,精度够用,见《Redis ZSet 方案》 |
| 没有 MQ,需要可靠审计 | 数据库轮询 + 定时任务 | 可查询、可取消、最不容易出错 |
最后是一条经验: 很多团队一开始用 MQ 做延时,一年后发现真正的瓶颈不是延时能力,而是”任务到底执行了没有”这个可观测性问题。中间件只保证”消息送到了”,不保证”业务做对了”。所以无论选哪家,都要配一套自己的任务状态表和幂等消费逻辑,这部分工作量不会因为选了 MQ 而减少。相关的可靠性设计见《可靠性》。
发现内容有问题?
告诉我们哪里需要改进。
