消息队列常见问题

返回

消息队列常见问题


1. 消息队列的可靠性怎么保证?

通用思路

可靠性本质上要同时保证三段:

  1. 生产者可靠发送:消息真的发到了 MQ,而不是业务刚提交消息就丢了
  2. Broker 可靠存储:消息真的被持久化了,不会因为机器故障直接消失
  3. 消费者可靠处理:消息真的被业务处理成功后,才确认消费完成

常见做法:

  • 生产端要有发送确认、失败重试,关键场景配合 Outbox / 本地消息表
  • Broker 要持久化、多副本、正确的主从更新策略
  • 消费端要业务成功后再 ACK / Commit
  • 失败消息要能重试、进死信队列、人工补偿
  • 全链路要有监控、报警、对账

一句话概括:可靠性不是某一个配置保证的,而是生产、存储、消费、补偿四段一起做。

Kafka

Kafka 里通常这样保证可靠性:

  • 生产端
    • 保证 ISR 中所有副本都同步才是写入:acks=all
    • 开启幂等生产者:enable.idempotence=true
    • 配置合理重试,避免网络抖动导致消息直接失败
  • Broker 端
    • 所有副本数量:replication.factor >= 3
    • ISR副本数量:min.insync.replicas >= 2
    • 不允许非ISR副本选主:关闭 unclean leader election
  • 消费端
    • 关闭自动提交 offset
    • 业务处理成功后再手动提交 offset
    • 失败消息走重试或单独的死信 Topic

Kafka 的核心思路是:靠副本机制保住数据,靠手动提交 offset 保住消费结果。

RocketMQ

RocketMQ 里通常这样保证可靠性:

  • 生产端
    • 关键消息优先用同步发送,拿到明确发送结果
    • 发送失败重试
    • 对特别关键的业务可配合本地消息表
  • Broker 端
    • 开启持久化
    • 关键场景选择更强的刷盘和主从复制策略
    • Broker 高可用部署,避免单点
  • 消费端
    • 消费成功才返回成功状态
    • 可恢复异常走重试
    • 多次失败进入死信队列

RocketMQ 的核心思路是:靠发送结果、持久化和重试/死信机制兜住可靠性。


2. 消息丢失怎么解决?

通用思路

消息丢失通常发生在三个阶段:

  1. 生产时丢 业务以为发成功了,其实没发到 Broker
  2. Broker 存储时丢 刚写进去机器就宕机,副本还没同步完
  3. 消费时丢 消费者还没处理成功,就提前确认了消息

所以通用解法也是三段处理:

  • 生产端要有发送确认和失败重试
  • 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. 消息重复消费怎么解决?

通用思路

重复消费的根本原因是:业务处理成功消息确认成功 不是一个原子操作。

比如:

  1. 消费者处理完业务
  2. 还没来得及 ACK / Commit,进程就挂了
  3. MQ 认为这条消息没处理完,于是再次投递

所以绝大多数 MQ 只能做到“至少一次”,也就是:

  • 尽量不丢
  • 但可能重复

通用解决思路有两层:

  1. 消费确认时机要对 业务成功后再确认
  2. 消费者要幂等 即使重复消费,也不能把结果做错

Kafka

Kafka 里常见做法:

  • 关闭自动提交 offset
  • 消费者处理成功后,再手动提交 offset
  • 业务侧用唯一键、去重表、状态机保证幂等

Kafka 要特别注意:

  • 手动提交 offset 只能降低重复概率
  • 不能从根上消灭重复消费
  • 真正兜底还是业务幂等

RocketMQ

RocketMQ 里常见做法:

  • 消费失败时返回稍后重试,而不是返回成功
  • 消费成功才返回成功状态
  • 消费端基于业务 ID 做去重
  • 多次失败进入死信队列

RocketMQ 的重试机制比较直接,但它也一样不能保证绝对不重复,最终还是要靠消费端幂等。


4. 如何保证幂等性?

通用思路

幂等的意思是:同一条消息处理多次,结果和处理一次一样。

最常见的做法有四类:

  • 唯一键去重
    • orderIdpaymentIdeventId 做唯一约束
  • 去重表
    • 单独记录消息 ID 是否处理过
  • 状态机控制
    • 比如订单只能从 INIT -> PAID,不能重复支付
  • 业务天然幂等
    • 例如把用户状态更新为“已实名”,重复设置结果一样

其中最稳的是:业务唯一键 + 数据库唯一约束 + 状态机

Kafka

Kafka 场景下常见幂等方案:

  • 消息体中放业务唯一 ID
  • 消费时先查去重表或业务表
  • 写库时加唯一索引
  • 处理成功后再提交 offset

如果是“消费 Kafka -> 处理 -> 再写回 Kafka”的链路,还可以结合 Kafka 的幂等生产者和事务能力,减少重复写入下游 Topic 的问题。

但要注意:

  • Kafka 事务更擅长解决 Kafka 内部链路的一致性
  • 对“数据库 + Kafka”双写,仍然常常需要 Outbox 或业务补偿

RocketMQ

RocketMQ 场景下幂等方案和 Kafka 本质一样:

  • 消息中带业务唯一键
  • 消费前先查重
  • 写库加唯一约束
  • 结合状态机防止重复流转

RocketMQ 更常见在订单、支付、物流这些业务流里,所以“状态机幂等”尤其常见。


5. 消息队列的顺序性如何保证?

通用思路

顺序性的本质是两个约束:

  1. 同一业务键的消息必须进入同一个队列/分区
  2. 这个队列/分区里的消息要按顺序消费

这里要注意一个很重要的点:

  • 大多数 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 的事务消息更贴近“本地事务 + 发消息”这个经典问题。

它的典型流程是:

  1. 生产者先发送一条半消息
  2. Broker 先把半消息存下来,但暂时不给消费者看
  3. 生产者执行本地事务
  4. 本地事务成功就提交消息,失败就回滚消息
  5. 如果 Broker 长时间没收到结果,就回查生产者本地事务状态

RocketMQ 的事务消息非常适合:

  • 订单创建成功后再通知下游
  • 支付成功后驱动发券、积分、库存
  • 交易链路中对最终一致性要求高的场景

但要注意:

  • 事务消息不等于全局事务
  • 消费端仍然要做幂等
  • 失败后仍然需要补偿和对账

8. 什么是本地消息表、Outbox?

通用思路

本地消息表和 Outbox,本质上都是解决一个经典问题:

本地业务操作成功了,但 MQ 消息没发出去;或者 MQ 消息发出去了,但本地业务操作失败了。

比如:订单表已经写成功了,但“订单已创建”消息没发出去

它们的核心思想都是:

先把“业务数据”和“待发送消息”放在同一个本地事务里一起提交,后面再异步把消息投递到 MQ。

也就是说,不是业务事务提交完立刻直接发 MQ,而是先把待发送事件保存到数据库里,保证“这条消息不会凭空丢掉”,再由后台任务或专门的投递程序把它发出去。

可以这样理解:

  • 本地消息表:更偏国内项目里的常见叫法
  • Outbox:更偏事件驱动架构里的标准叫法

本质上两者是同一个思路,只是命名和工程落地方式略有区别。

一个典型流程是:

  1. 开启本地事务
  2. 写业务表,比如订单表、支付表
  3. 同时写消息表 / Outbox 表,记录待发送事件
  4. 本地事务一起提交
  5. 后台投递程序扫描未发送事件
  6. 把事件投递到 MQ
  7. 投递成功后把消息状态更新为“已发送”

它的优点是:

  • 避免数据库和 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 除了本地消息表,还经常直接讲事务消息。