Skip to content

异步与消息架构 ​

消息队列不是「为了用而用」。它解决的是三个具体问题:解耦、削峰、最终一致 —— 如果这三个都不沾,就别引入。 Kafka 的存储与副本细节见 Kafka 深入,这篇讲架构层面怎么用。

一、先判断:该不该异步 ​

场景该异步吗理由
发短信、发邮件、推送✅失败可重试,不影响主流程
下单后扣积分、写日志、更新报表✅主流程不需要它的结果
秒杀削峰✅用队列把瞬时流量摊平
查余额❌必须同步拿到结果
强一致的扣款❌异步只会引入不一致

判断标准一句话:调用方需不需要立刻用这个结果。不需要,就可以异步。

异步的三种代价 ​

  1. 复杂度:从「一次调用」变成「两个系统 + 一个中间件」,排查链路变长;
  2. 一致性:从「事务内」变成「最终一致」,必须设计补偿;
  3. 延迟:正常情况多几十毫秒,异常时可能积压几十分钟。

二、四种模型 ​

模型语义典型实现适用
点对点一条消息只被一个消费者处理RabbitMQ 队列、Kafka 分区任务分发
发布订阅一条消息广播给所有订阅者Kafka topic + 不同 group、Redis pub/sub事件通知
请求-应答异步请求带回复通道临时队列 + correlationId跨服务调用
延迟/定时到点才投递RocketMQ 延迟等级、RabbitMQ DLX+TTL订单超时取消

三、中间件选型(速查) ​

KafkaRocketMQRabbitMQPulsar
定位流平台、日志管道业务消息灵活路由、低延迟云原生、存算分离
吞吐极高高中高
延迟msmsμs~msms
顺序分区内队列内队列内分区内
事务消息有(复杂)原生不支持有
延迟消息需外部方案原生插件/DLX支持
运维中中简单复杂

一句话选型:日志/埋点/流处理 → Kafka;业务消息 + 事务 + 延迟 → RocketMQ;量不大、路由复杂 → RabbitMQ。

四、可靠性:还是那三件事 ​

不丢、不重、不乱序 —— 具体配置见 Kafka 深入 · 第 6 节,这里只列架构层面的兜底:

问题架构层兜底
消息丢失生产端本地消息表 + 定时补偿;消费端处理成功才 ack
重复消费消费端幂等(唯一键/去重表/状态机),永远不要指望 MQ 只投一次
顺序错乱同业务 key 打同一分区/队列;消费端串行处理

记住一句:MQ 只保证「至少一次」,业务必须自己幂等。

五、事务消息与本地消息表 ​

「本地事务成功,消息一定发出去」—— 这是最容易出问题的地方。

方案 A:本地消息表(最通用,不依赖 MQ 特性) ​

1. 业务事务里,同时往「消息表」插一条 status=待发送
2. 事务提交后,异步任务扫表发送 MQ
3. 发送成功 → 状态改「已发送」;失败 → 重试(指数退避)
4. 定时任务补偿卡在「待发送」超过 N 分钟的记录
5. 消费端幂等(用消息表主键去重)
1. 业务事务里,同时往「消息表」插一条 status=待发送
2. 事务提交后,异步任务扫表发送 MQ
3. 发送成功 → 状态改「已发送」;失败 → 重试(指数退避)
4. 定时任务补偿卡在「待发送」超过 N 分钟的记录
5. 消费端幂等(用消息表主键去重)

优点:任何 MQ 都能用、可靠、易排查。 缺点:多一张表、有延迟、扫表要控制频率。

方案 B:RocketMQ 事务消息 ​

1. 发 half message(对消费端不可见)
2. 执行本地事务
3. 提交/回滚 half message
4. 若第 3 步没到达,Broker 回查本地事务状态
1. 发 half message(对消费端不可见)
2. 执行本地事务
3. 提交/回滚 half message
4. 若第 3 步没到达,Broker 回查本地事务状态

优点:不用自己扫表。 缺点:绑定 RocketMQ,且回查逻辑要幂等。

方案 C:Outbox + CDC(大厂做法) ​

业务表 + outbox 表同库,用 Canal/Debezium 订阅 binlog 投递 MQ。业务代码零侵入,但有运维成本。

六、延迟消息怎么实现 ​

方案精度说明
RocketMQ 延迟等级秒级开箱即用,等级固定(1s/5s/10s…)
RabbitMQ DLX + TTL秒级队列级 TTL 会队头阻塞,不推荐大量使用
Redis ZSet 轮询秒级score=到期时间,定时任务捞 score <= now
时间轮(Netty/自研)毫秒级定时任务框架常用,见 定时任务调度
数据库扫表分钟级订单超时关单最常见的做法

订单超时取消的推荐组合:延迟消息(快)+ 定时扫表(兜底,防止消息丢了)。

七、积压怎么处理 ​

情况处理
消费慢(业务本身耗时)扩容消费者(≤ 分区数)、优化单条处理、批量消费
分区数不够扩分区(注意:Kafka 扩分区会打乱 key 的顺序)
突发流量临时把消息转到临时 topic,用更多消费者处理("搬运"方案)
消费逻辑阻塞加线程池并行处理(但会丢顺序,要按 key 哈希到固定线程)

先查三件事:消费者是不是挂了、是不是在反复重试同一条毒消息、下游依赖是不是慢了。

八、四个常见反模式 ​

  1. 用 MQ 做 RPC(请求-应答、同步等结果)→ 延迟高、失败难处理,直接 HTTP 更好。
  2. 消息体塞整个实体(几 MB)→ 只发 ID,消费方回查;大消息会拖垮 Broker。
  3. 没有死信队列 → 毒消息无限重试,把整个 topic 堵死。
  4. 消费端不幂等 + 生产端重试 → 重复数据,事后对账一地鸡毛。

九、速查 ​

问题答案
什么时候该用 MQ解耦、削峰、最终一致,三者不沾就别用
怎么保证不丢生产端本地消息表 + 消费端处理成功再 ack
怎么保证不重消费端幂等(唯一键/去重表),别指望 MQ
怎么保证顺序同 key 同分区 + 消费端串行
事务消息怎么选通用选本地消息表;绑 RocketMQ 可用事务消息
延迟消息怎么做RocketMQ 延迟等级 / Redis ZSet / 时间轮,配定时扫表兜底
积压怎么办先查是否卡在毒消息,再扩消费者或扩分区
最大的坑用 MQ 做同步 RPC、消费端不幂等