消息中间件全景:选型、能力与落地
订单支付成功后,用户需要及时看到支付结果,邮件和搜索索引可以稍后完成。消息中间件(MQ)保存待处理的事件,让这些下游各自消费和重试;支付主链路与邮件服务的快慢由此分开。
选型要先回答:任务是否需要回放、失败能否重试、同一订单是否必须保序,以及多久必须处理完。下图先说明可靠事件链路,再对比 Kafka、RabbitMQ、RocketMQ 与 Pulsar 的模型和适用边界。
MQ 在系统中的位置
核心变化是:主链路只完成必要的业务提交,耗时或可异步的动作交给 MQ 后面的消费者处理。
例如支付请求只需要确认支付流水与订单状态已经提交,发券、邮件与搜索索引可异步完成。不能先提交数据库、再随手发一条消息就宣称可靠:进程可能恰好在两步之间退出。需要同一数据库事务写业务记录与 Outbox,再由中继重试发布;中继可能重复发送,消费端仍要幂等。
视频学习入口
| 系列 | 链接 | 搭配阅读 |
|---|---|---|
| Kafka 核心原理科普 | 从第 01 集开始 | 看完后阅读 Kafka,重点补 Topic、Partition、Consumer Group、事务和容量规划 |
| RocketMQ 核心原理科普 | RocketMQ 01 | 看完后阅读 RocketMQ,重点补事务消息、延迟消息、顺序消息和存储结构 |
| RabbitMQ | 暂无配套视频 | 先阅读 RabbitMQ,重点理解 Exchange、Queue、Binding、Confirm、Ack、TTL 与 DLX |
MQ 解决的典型问题
| 问题 | 不用 MQ 的表现 | 使用 MQ 后 |
|---|---|---|
| 应用解耦 | A 服务直接调用 B/C/D,任一下游失败影响主链路 | A 只发布事件,下游各自订阅 |
| 削峰填谷 | 高峰流量直接打爆数据库或第三方接口 | 消息先进入队列,消费者按能力处理 |
| 异步任务 | 用户请求等待慢操作完成 | 请求快速返回,后台异步执行 |
| 广播通知 | 逐个调用订阅方 | 多消费者订阅同一事件 |
| 事件回放 | 历史事件丢失,无法重建状态 | 保留消息后可重新消费 |
| 最终一致性 | 分布式事务复杂 | 通过事件、重试、幂等、补偿实现 |
主流组件能力地图
| 组件 | 核心模型 | 最强能力 | 典型场景 | 选型关键词 |
|---|---|---|---|---|
| RabbitMQ | Exchange + Queue | 灵活路由、任务队列、协议生态 | 业务异步、任务分发、微服务解耦 | 好上手、路由强 |
| Kafka | Topic + Partition Log | 高吞吐、可回放、流处理生态 | 日志、埋点、CDC、实时数据管道 | 事件流、数据平台 |
| RocketMQ | Topic + Queue | 事务消息、顺序消息、延时消息 | 订单、支付、库存、交易链路 | 业务语义强 |
| Pulsar | Topic + Subscription + BookKeeper | 多租户、存算分离、多订阅模型 | 云原生统一消息平台、跨地域 | 平台化、多租户 |
选型判断
这些能力可以重叠,图中给出评估入口,不是互斥判定。已经有 Kafka 的团队,为订单发券再引入一套 MQ 未必划算;先确认延迟精度、恢复目标、协议和运维能力,再用实际消息大小、峰值和故障恢复场景做验证。
消息投递的基本语义
| 语义 | 含义 | 风险 | 落地建议 |
|---|---|---|---|
| 至多一次 | 最多投递一次 | 可能丢消息 | 只适合日志、指标等可丢场景 |
| 至少一次 | 至少成功投递一次 | 可能重复 | 最常见,消费者必须幂等 |
| 精确一次 | 指定处理边界内,结果提交一次 | 越界副作用不自动覆盖 | 明确事务覆盖范围,再设计外部系统幂等 |
现实工程里不要幻想“MQ 帮我保证业务绝对只执行一次”。更可靠的做法是:MQ 至少一次 + 消费者幂等 + 业务状态机约束。
“至少一次”也有前提:生产成功被确认、消息未过期或被保留策略删除、消费者在可恢复期限内持续重试。MQ 不会在磁盘耗尽、保留期已过或死信无人处理时承诺业务最终完成。
一个标准消息处理链路
数据库提交后、消费确认前进程退出,是重复投递的典型窗口。发券记录和处理记录应在同一个本地事务提交,以业务唯一键限制重复。外部发券接口则需要支持幂等请求键或查询对账,仅在本地先写“已处理”会丢任务,先调用后写记录又可能重复调用。Kafka 的重试主题和死信需要应用或框架设计,并非 Broker 在“不提交 offset”后自动帮业务跳过失败消息。
落地前必须设计的 8 件事
- 消息契约:字段、版本、兼容策略、事件命名。
- Topic/Queue 规划:按业务域拆分,不要所有消息共用一个主题。
- 消息键设计:用于分区、顺序、幂等和排查。
- 投递语义:至少一次、至多一次,还是需要事务协同。
- 幂等策略:唯一键、防重表、状态机、乐观锁。
- 失败策略:重试次数、退避间隔、死信、人工补偿。
- 消息生命周期:TTL、保留时间、归档、清理。
- 监控告警:堆积、延迟、失败率、重试量、死信量。
统一案例:订单超时关闭 + 支付成功发券
这套案例会在每篇组件文档里展开:
核心业务规则:
- 下单 30 分钟未支付,自动关闭订单。
- 支付成功后异步发券。
- 超时关闭和支付成功存在竞态,订单状态机必须兜底。
- 发券必须幂等,不能重复发。
不同组件实现方式:
| 组件 | 推荐实现 |
|---|---|
| RabbitMQ | 固定延时队列 TTL + DLX 触发检查,核对死信转发可靠性 |
| Kafka | 事件流 + 调度服务,适合可回放和审计 |
| RocketMQ | 延时消息 + 事务消息,最贴近交易业务语义 |
| Pulsar | Delayed Delivery + Shared/Key_Shared,超时与支付竞态由数据库仲裁 |
收到超时消息后执行“查询未支付,再无条件关闭”仍有竞争窗口:查询与更新之间,支付回调可能先提交。订单服务应使用类似下面的条件更新,并检查影响行数;支付流程也只能从允许的状态迁移。
sql
UPDATE orders SET status = 'CLOSED'
WHERE id = :order_id
AND status = 'PENDING'
AND expires_at <= CURRENT_TIMESTAMP;这里是带命名参数的 SQL 逻辑片段,参数由数据库访问层绑定。影响 0 行表示没有取得关闭资格,需要检查是已支付、已关闭还是未到期;已经到账但订单已关闭的支付,要进入退款或人工对账策略。验收时至少覆盖消息重复、消费后确认前退出、超时和支付同时到达、下游停机后恢复四种情况。
