用过 RocketMQ、RabbitMQ,也读过 Kafka 的设计。这篇先不谈具体产品,把所有 MQ 都要面对的问题整理出来 —— 这些才是选型和排障的真正依据。
一、MQ 的四大价值
二、三大 MQ 怎么选
| 维度 | RocketMQ | Kafka | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 十万级 TPS | 百万级 TPS(最高) | 万级 TPS |
| 延迟 | 毫秒级 | 毫秒级(批处理后略高) | 微秒–毫秒(最低) |
| 消息模型 | Topic + Tag + 队列 | Topic + Partition | Exchange + Queue(路由灵活) |
| 顺序消息 | 支持(分区顺序) | 支持(同 Partition) | 单队列顺序 |
| 事务消息 | 原生支持(半消息) | 支持(较弱) | 需自己实现 |
| 延迟消息 | 原生 18 个等级 | 需自己实现 | 死信 + TTL 或插件 |
| 消息回溯 | 支持(按时间) | 支持(按 offset) | 不支持(消费即删除) |
| 生态/运维 | Java 友好,控制台好用 | 大数据生态强 | Erlang,运维相对复杂 |
| 典型场景 | 业务消息、订单、交易 | 日志、埋点、流计算 | 企业内部业务、路由复杂 |
我的选择习惯:
- 业务链路(下单、支付、通知) → RocketMQ。事务消息和延迟消息是刚需,Java 栈也顺手。
- 日志/埋点/行为数据 → Kafka。吞吐和生态最强,接 Flink / 数仓方便。
- 内部系统间小规模解耦、路由规则复杂 → RabbitMQ。Exchange 的灵活路由很好用。
三、六个绕不开的问题
1. 消息丢失
三个环节都可能丢:
| 环节 | 风险 | 保障 |
|---|---|---|
| 生产端 | 发送失败、网络抖动 | 同步发送 + 失败重试;RocketMQ 用事务消息 |
| Broker | 刷盘前宕机 | 同步刷盘(flushDiskType=SYNC_FLUSH)或至少同步复制 |
| 消费端 | 自动 ack 后业务失败 | 业务处理成功再 ack(手动提交) |
最容易被忽略的是消费端:用了自动提交 offset,业务抛异常但消息已经算消费成功了 —— 消息等于丢了。
2. 重复消费
MQ 只保证至少一次(at least once),重复消费是必然发生的,不要指望靠 MQ 避免。
幂等方案(按推荐顺序):
- 唯一键 + 数据库约束:消息带业务唯一 ID,插入时冲突即忽略。最可靠。
- Redis
SETNX:set(msgId, 1, NX, EX 24h),已存在则跳过。 - 状态机:
if (order.status != PENDING) return;,天然幂等。 - 去重表:专门一张
msg_consumed表,带唯一索引。
3. 顺序性
只有局部顺序是可行的:把需要保序的消息(同一订单)发到同一个队列/分区,且消费者单线程消费。用 MessageQueueSelector 按订单 ID 取模分片。
全局顺序 = 一个分区 = 没有并发 = 没有吞吐,生产上不要追求。
4. 消息堆积
现象:消费速度跟不上生产速度,队列长度持续上涨。
排查顺序:
- 消费者是不是挂了 / 卡住了(看消费 TPS 曲线、看线程栈)。
- 消费逻辑是不是变慢了(下游接口慢、慢 SQL)。
- 消费端有没有报错导致反复重试。
应急扩容:增加消费者实例,但前提是队列数足够 —— RocketMQ 中消费者数量 > 队列数时,多出来的消费者分不到队列,等于白加。所以队列数一开始就得多配(比如 32/64)。
兜底:堆积严重时,把消息转存到「死信/备用 Topic」,先让主链路恢复,再慢慢消费积压数据。
5. 消费失败与重试
- 重试要有次数上限和退避,不要无限重试(会把 MQ 打满)。
- 重试 N 次仍失败 → 进死信队列 / 人工补偿表,并告警。
- 区分可重试异常和不可重试异常:参数错误重试一万次也没用,直接进死信;网络超时才值得重试。
6. 事务消息(本地事务 + 消息的一致性)
典型场景:下单成功要发一条消息,但「订单入库」和「发消息」是两个操作,不能保证同时成功。
RocketMQ 事务消息流程:
1. 发送半消息(对消费者不可见)
2. 执行本地事务(下单入库)
3. 本地事务成功 → Commit(消息可见);失败 → Rollback(消息丢弃)
4. 若第 3 步没响应,Broker 回查本地事务状态(需实现回查接口)
轻量替代方案(我们更常用的):本地消息表 —— 在业务库建一张 mq_message 表,和业务数据同一个本地事务写入,再由定时任务扫描发送,发送成功标记完成。简单可靠,缺点是有一定的延迟。
四、工程规范(团队约定)
- Topic 命名有规范:
业务域_事件_版本,如order_created_v1。 - 消息体必带字段:
msgId(唯一键,用于幂等)、traceId(链路追踪)、occurTime(业务发生时间)、bizKey(业务主键,用于排查)。 - 消息体要向后兼容:只增字段不删字段,用 JSON 且反序列化时忽略未知字段。
- 消费端必须可重放:业务逻辑写成幂等的,这样出问题时可以「重置 offset 重跑」。
- 监控必配四件套:生产 TPS、消费 TPS、堆积量、消费失败率。堆积量告警是生命线。
五、一次真实的堆积事故
脱敏自某营销中台的批量任务场景。
现象:营销活动批量发券,MQ 堆积从 0 涨到 300 万,活动效果延迟了 2 小时才生效。
原因:消费端在处理每条消息时都会调用一次用户服务的 HTTP 接口查画像,用户服务当时正慢(RT 2s),消费能力从 2000 TPS 掉到 50 TPS。
处理:
- 紧急扩容消费者(队列数 32 够用,扩到 32 个实例)。
- 更重要的一步:把每条消息的远程调用改成批量 —— 消费端攒批 500 条再一次性查画像,网络往返从 1 次/条降到 1 次/500 条。
- 长期:画像数据本地缓存 + 变更时推送,消费端不再依赖实时查询。
结论:消费端单条消息里的远程调用是吞吐杀手。优化 MQ 的时候别只盯着 MQ 参数,往往瓶颈在消费逻辑本身。
一起聊聊