消息队列常见问题
返回消息队列常见问题
1. 消息队列的可靠性怎么保证?
通用思路
可靠性本质上要同时保证三段:
- 生产者可靠发送:消息真的发到了 MQ,而不是业务刚提交消息就丢了
- Broker 可靠存储:消息真的被持久化了,不会因为机器故障直接消失
- 消费者可靠处理:消息真的被业务处理成功后,才确认消费完成
常见做法:
- 生产端要有发送确认、失败重试,关键场景配合
Outbox/ 本地消息表 - Broker 要持久化、多副本、正确的主从更新策略
- 消费端要业务成功后再 ACK / Commit
- 失败消息要能重试、进死信队列、人工补偿
- 全链路要有监控、报警、对账
一句话概括:可靠性不是某一个配置保证的,而是生产、存储、消费、补偿四段一起做。
Kafka
Kafka 里通常这样保证可靠性:
- 生产端
- 保证 ISR 中所有副本都同步才是写入:
acks=all - 开启幂等生产者:
enable.idempotence=true - 配置合理重试,避免网络抖动导致消息直接失败
- 保证 ISR 中所有副本都同步才是写入:
- Broker 端
- 所有副本数量:
replication.factor >= 3 - ISR副本数量:
min.insync.replicas >= 2 - 不允许非ISR副本选主:关闭
unclean leader election
- 所有副本数量:
- 消费端
- 关闭自动提交 offset
- 业务处理成功后再手动提交 offset
- 失败消息走重试或单独的死信 Topic
Kafka 的核心思路是:靠副本机制保住数据,靠手动提交 offset 保住消费结果。
RocketMQ
RocketMQ 里通常这样保证可靠性:
- 生产端
- 关键消息优先用同步发送,拿到明确发送结果
- 发送失败重试
- 对特别关键的业务可配合本地消息表
- Broker 端
- 开启持久化
- 关键场景选择更强的刷盘和主从复制策略
- Broker 高可用部署,避免单点
- 消费端
- 消费成功才返回成功状态
- 可恢复异常走重试
- 多次失败进入死信队列
RocketMQ 的核心思路是:靠发送结果、持久化和重试/死信机制兜住可靠性。
2. 消息丢失怎么解决?
通用思路
消息丢失通常发生在三个阶段:
- 生产时丢 业务以为发成功了,其实没发到 Broker
- Broker 存储时丢 刚写进去机器就宕机,副本还没同步完
- 消费时丢 消费者还没处理成功,就提前确认了消息
所以通用解法也是三段处理:
- 生产端要有发送确认和失败重试
- Broker 要有持久化和副本
- 消费端要“先执行业务,再确认消费”
对特别关键的场景,还要加:
- 本地消息表 / Outbox
- 对账补偿
- 死信队列
Kafka
Kafka 防丢消息,重点看这几件事:
- Producer 设置
acks=all - 开启
enable.idempotence=true - Topic 配置多副本,常见是 3 副本
min.insync.replicas不要太低- 关闭不干净选主,避免落后副本被选为 Leader
- Consumer 关闭自动提交,业务成功后再提交 offset
Kafka 最常见的坑是:
acks配得太低,Leader 还没同步就算成功- 自动提交 offset,业务失败但消息已经被认为消费完成
RocketMQ
RocketMQ 防丢消息,重点是:
- 关键消息用同步发送,检查发送结果
- Broker 做持久化和高可用部署
- 消费失败返回重试,而不是误返回成功
- 多次失败进死信队列
RocketMQ 最常见的坑是:
- 生产端只管发,不校验发送结果
- 消费异常被代码吃掉,最终返回“消费成功”
- 只做 MQ 重试,不做业务补偿
3. 消息重复消费怎么解决?
通用思路
重复消费的根本原因是:业务处理成功 和 消息确认成功 不是一个原子操作。
比如:
- 消费者处理完业务
- 还没来得及 ACK / Commit,进程就挂了
- MQ 认为这条消息没处理完,于是再次投递
所以绝大多数 MQ 只能做到“至少一次”,也就是:
- 尽量不丢
- 但可能重复
通用解决思路有两层:
- 消费确认时机要对 业务成功后再确认
- 消费者要幂等 即使重复消费,也不能把结果做错
Kafka
Kafka 里常见做法:
- 关闭自动提交 offset
- 消费者处理成功后,再手动提交 offset
- 业务侧用唯一键、去重表、状态机保证幂等
Kafka 要特别注意:
- 手动提交 offset 只能降低重复概率
- 不能从根上消灭重复消费
- 真正兜底还是业务幂等
RocketMQ
RocketMQ 里常见做法:
- 消费失败时返回稍后重试,而不是返回成功
- 消费成功才返回成功状态
- 消费端基于业务 ID 做去重
- 多次失败进入死信队列
RocketMQ 的重试机制比较直接,但它也一样不能保证绝对不重复,最终还是要靠消费端幂等。
4. 如何保证幂等性?
通用思路
幂等的意思是:同一条消息处理多次,结果和处理一次一样。
最常见的做法有四类:
- 唯一键去重
- 用
orderId、paymentId、eventId做唯一约束
- 用
- 去重表
- 单独记录消息 ID 是否处理过
- 状态机控制
- 比如订单只能从
INIT -> PAID,不能重复支付
- 比如订单只能从
- 业务天然幂等
- 例如把用户状态更新为“已实名”,重复设置结果一样
其中最稳的是:业务唯一键 + 数据库唯一约束 + 状态机。
Kafka
Kafka 场景下常见幂等方案:
- 消息体中放业务唯一 ID
- 消费时先查去重表或业务表
- 写库时加唯一索引
- 处理成功后再提交 offset
如果是“消费 Kafka -> 处理 -> 再写回 Kafka”的链路,还可以结合 Kafka 的幂等生产者和事务能力,减少重复写入下游 Topic 的问题。
但要注意:
- Kafka 事务更擅长解决 Kafka 内部链路的一致性
- 对“数据库 + Kafka”双写,仍然常常需要 Outbox 或业务补偿
RocketMQ
RocketMQ 场景下幂等方案和 Kafka 本质一样:
- 消息中带业务唯一键
- 消费前先查重
- 写库加唯一约束
- 结合状态机防止重复流转
RocketMQ 更常见在订单、支付、物流这些业务流里,所以“状态机幂等”尤其常见。
5. 消息队列的顺序性如何保证?
通用思路
顺序性的本质是两个约束:
- 同一业务键的消息必须进入同一个队列/分区
- 这个队列/分区里的消息要按顺序消费
这里要注意一个很重要的点:
- 大多数 MQ 只能保证局部有序
- 很难保证全局有序
因为一旦追求全局有序,就会严重影响并发和吞吐。
所以生产里常见的做法是:
- 只保证“同一个订单”“同一个账户”“同一个用户”的消息有序
- 不追求所有消息全局有序
Kafka
Kafka 只能保证单个 Partition 内有序。
所以要保证顺序,通常要这样做:
- 发送消息时指定同一个业务 key
- 让同一个 key 总是进入同一个 Partition
- 一个消费者组内,一个 Partition 同一时刻只会被一个消费者消费
Kafka 的限制也很明显:
- 如果强行把所有消息打到一个 Partition,顺序有了,但吞吐会掉很多
- 所以一般只做“按业务键局部有序”
RocketMQ
RocketMQ 的顺序消息思路类似:
- 同一个业务 key 路由到同一个 MessageQueue
- 再由顺序消费保证这一条链路有序
RocketMQ 在业务顺序场景里更常见,比如订单状态流转、支付状态流转、物流轨迹更新。
但代价也一样:
- 顺序性越强,并发能力越差
- 如果某个 key 特别热,还可能造成热点队列
6. 如何处理消息队列的消息积压问题?
通用思路
消息积压的本质就是:生产速度持续大于消费速度。
分为两类,一类是突发流量,比如双十一,秒杀场景;另一类是长期消息积压。
处理积压时,不要一上来就加机器。
- 先止血
- 降低生产速度
- 降低生产速度,必要时限流
- 降级非核心业务
- 延后批量任务、定时任务
- 扩充消费者
- 增加消费者实例数
- 增加消费线程数
- 增加分区 / 分片数
- 降低生产速度
- 再定位原因
- 消费能力不足
- 消费逻辑过重
- 消费实例少
- 分区数小于消费者数,扩容无效
- 消费能力不足
- 恢复积压
- 先修复原 consumer 的问题,避免继续恶化
- 临时创建更大的并行通道(更多 partition / queue)
- 把积压消息快速转发到这些临时队列里
- 用更多机器并发消费临时队列
- 等积压清完,再恢复原架构
- 最后监控
- 生产能力:每秒生产消息数
- 消费能力:消费速度、处理耗时、成功率、失败率。
- 队列积压:队列积压设置阈值,超过阈值告警
- 基础指标:CPU、内存、磁盘、网络
积压原因通常就三类:
- 消费者异常
- 单条消费太慢
- 队列/分区设计不合理
7. 如何保证数据一致性,事务消息如何实现?
通用思路
这里要解决的是:本地事务成功了,但消息没发出去;或者消息发出去了,但本地事务失败了。
本质上这是“数据库更新”和“发送 MQ 消息”的双写一致性问题。
常见解法有三类:
- 本地消息表 / Outbox
- 业务数据和待发送消息放在同一个本地事务里
- 后续异步投递 MQ
- 事务消息
- 先发半消息,再提交本地事务,再确认消息是否可投递
- 定时对账 + 补偿
- 出问题后兜底修复
在分布式系统里,通常追求的是最终一致性,而不是所有系统实时强一致。
Kafka
Kafka 有事务能力,但要分清它擅长解决什么问题。
Kafka 更适合解决的是:
- 多个 Topic / Partition 的原子写入
- “消费 Kafka -> 处理 -> 生产到 Kafka” 这条链路的一致性
- 配合
read_committed避免消费者读到未提交事务消息
Kafka 不擅长直接解决的是:
- “本地数据库事务 + 发 Kafka 消息” 这类跨数据库和 MQ 的双写原子性
所以生产里更常见的做法是:
- 用
Outbox保证数据库和待发送事件同事务提交 - 再由后台程序把 Outbox 事件投递到 Kafka
- 消费端继续做幂等和补偿
一句话说:Kafka 事务很强,但主要强在 Kafka 体系内部;跨 DB 的最终一致性通常还是靠 Outbox。
RocketMQ
RocketMQ 的事务消息更贴近“本地事务 + 发消息”这个经典问题。
它的典型流程是:
- 生产者先发送一条半消息
- Broker 先把半消息存下来,但暂时不给消费者看
- 生产者执行本地事务
- 本地事务成功就提交消息,失败就回滚消息
- 如果 Broker 长时间没收到结果,就回查生产者本地事务状态
RocketMQ 的事务消息非常适合:
- 订单创建成功后再通知下游
- 支付成功后驱动发券、积分、库存
- 交易链路中对最终一致性要求高的场景
但要注意:
- 事务消息不等于全局事务
- 消费端仍然要做幂等
- 失败后仍然需要补偿和对账
8. 什么是本地消息表、Outbox?
通用思路
本地消息表和 Outbox,本质上都是解决一个经典问题:
本地业务操作成功了,但 MQ 消息没发出去;或者 MQ 消息发出去了,但本地业务操作失败了。
比如:订单表已经写成功了,但“订单已创建”消息没发出去
它们的核心思想都是:
先把“业务数据”和“待发送消息”放在同一个本地事务里一起提交,后面再异步把消息投递到 MQ。
也就是说,不是业务事务提交完立刻直接发 MQ,而是先把待发送事件保存到数据库里,保证“这条消息不会凭空丢掉”,再由后台任务或专门的投递程序把它发出去。
可以这样理解:
- 本地消息表:更偏国内项目里的常见叫法
- Outbox:更偏事件驱动架构里的标准叫法
本质上两者是同一个思路,只是命名和工程落地方式略有区别。
一个典型流程是:
- 开启本地事务
- 写业务表,比如订单表、支付表
- 同时写消息表 / Outbox 表,记录待发送事件
- 本地事务一起提交
- 后台投递程序扫描未发送事件
- 把事件投递到 MQ
- 投递成功后把消息状态更新为“已发送”
它的优点是:
- 避免数据库和 MQ 双写不一致
- 即使 MQ 短暂故障,消息也还在数据库里
- 可以依赖重试、补偿把消息最终发出去
它的代价是:
- 系统链路更长了
- 需要额外维护消息表、扫描任务、重试逻辑
- 一般是最终一致性,不是实时强一致
Kafka
Kafka 场景里,Outbox 特别常见。
原因是 Kafka 虽然有事务能力,但它更擅长解决:
- Kafka 内部多分区、多 Topic 的原子写入
- “消费 Kafka -> 处理 -> 再发 Kafka” 这种链路一致性
但对于:
- “数据库事务 + 发 Kafka 消息”
这种跨数据库和 MQ 的双写问题,生产里更常用的还是 Outbox。
Kafka 里常见做法是:
- 业务服务在本地事务里同时写业务表和 Outbox 表
- 后台任务扫描 Outbox 表
- 把事件投递到 Kafka Topic
- 投递成功后把 Outbox 状态改成已发送
- 下游消费者继续做幂等
如果公司用 CDC 工具,也可能直接监听 Outbox 表变更,再同步到 Kafka,这也是很常见的做法。
RocketMQ
RocketMQ 场景里,本地消息表也能用,但一般要分情况看:
- 如果只是想解决“数据库 + MQ 双写一致性”,本地消息表完全可以用
- 如果业务特别适合事务消息,也可以直接用 RocketMQ 事务消息
所以 RocketMQ 常见有两条路:
- 方案一:本地消息表
- 和 Kafka 一样,先落库,再异步投递 RocketMQ
- 方案二:事务消息
- 先发半消息,再执行本地事务,最后提交或回滚消息
怎么选,主要看场景:
- 想要实现简单、容易掌控、方便排查,很多团队会选本地消息表
- 想更直接处理“本地事务 + 发消息”的一致性,可以选 RocketMQ 事务消息
所以可以这样记:
Kafka 更常讲 Outbox;RocketMQ 除了本地消息表,还经常直接讲事务消息。