笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 通用后台基础(跨域)

    • 并发模型 · 进程 / 线程 / 协程
    • 操作系统核心与零拷贝
    • Go 语言基础与常见陷阱
    • C++11 语言基础与常见陷阱
    • C++20 语言基础与常见陷阱
    • Rust 语言基础与常见陷阱
    • 设计模型 · Actor / CSP / Reactor / 同步异步
    • GC 与 STW · Go / JVM
    • 可观测性
    • 时序异常检测(EWMA / ARIMA / 滑动窗口)
    • 数据库范式与存储引擎:从关系型到向量库
    • MySQL InnoDB 索引与事务
    • Redis 版本演进 & 分布式
    • 消息队列 · 可靠投递与选型
    • 分布式事务 · 2PC / TCC / Saga / 最终一致性
    • HTTP / HTTPS / TLS 与 RPC
    • 加密基础:对称 / 非对称 / 哈希与组合模式
    • 叙事主骨架轴选择方法论 SOP

消息队列 · 可靠投递与选型

削峰/解耦/异步 · 可靠投递(三处不丢)· 重复消费与幂等 · 顺序消费 · 消息积压 · 事务消息 · 死信/延迟队列 · Kafka vs RabbitMQ vs Pulsar 选型

一句话抓手

消息队列的所有面试题都绕着三个主线:"三处不丢"——生产端 ack、Broker 持久化+副本、消费端手动 ack 后再提交位点;"至少一次"是默认交付语义,所以消费端必须幂等;顺序、不重、不丢、高吞吐这四个目标互相矛盾,选型就是选你愿意牺牲哪个。抓住这三条,MQ 的题基本能自圆其说。

场景问题

打个比方:消息队列就像餐厅后厨的点餐传菜口。前厅服务员(生产者)把订单小票往夹子上一挂就转身接待下一桌,根本不用站着等后厨炒完(解耦 + 异步);哪怕高峰期订单雪片般涌来,也只是先在夹子上排队,后厨(消费者)按自己的节奏一张张取着做(削峰)。类比失效边界:这个夹子的规矩是"小票要等后厨确认做好、亲手撤下才算完"(消费端 ack 后才删消息)——于是就有了"至少一次"的坑:后厨把菜做好了、正要撤票时手一抖崩溃了,这张票还挂在夹子上,重启后会被再取一次,同一道菜可能做两遍。所以 MQ 天生不保证"恰好一次",去重只能靠消费端幂等(同一订单号无论被取几次,都只认一次)来兜底。

后台面试与系统设计高频题,本质考对"交付语义 + 可靠性代价"的理解:

题目考点直觉答案往往错在
消息会丢吗,哪里丢生产/Broker/消费三段三处都可能丢,各有独立机制,缺一段就丢
保证不重复消费交付语义MQ 只能做到"至少一次",不重靠消费端幂等
怎么保证顺序分区 + 单线程消费全局有序几乎不可能,只能做分区内有序
消息积压了几百万怎么办消费能力加消费者受限于分区数;先扩分区或临时转存
先写库还是先发 MQ分布式一致性两个都可能失败,需事务消息/本地消息表
exactly-once 存在吗端到端语义严格 EOS 要 Broker+消费端事务协同,代价高
延迟消息怎么实现时间轮/分级RabbitMQ 靠 TTL+死信,Kafka 需外部调度

实现方案

三大用途:削峰、解耦、异步

  • 削峰填谷:突发流量写入队列,消费端按自己节奏消费,把"瞬时高并发"摊平成"持续中等负载"(秒杀下单先入队)
  • 系统解耦:生产者不需知道谁消费、有几个消费者;下游增减不影响上游
  • 异步化:非核心链路(发短信、加积分、写日志)异步化,缩短主链路 RT

代价:引入 MQ = 多一个必须高可用的组件 + 最终一致性(不再是同步强一致)+ 排查链路变长。

可靠投递:三处不丢

三个环节各自独立会丢,缺一段兜底就漏:① 生产端确认、② Broker 持久化+副本、③ 消费端手动 ack。

消息在三个环节都会丢,要分别兜底

1. 生产端 → Broker

  • Kafka:acks=all(等所有 ISR 副本落盘)+ retries 重试;acks=1 只等 Leader,Leader 挂了未同步的丢;acks=0 发了不管,最快最不可靠
  • RabbitMQ:开启 publisher confirm(Broker 落盘后回 ack),配合 mandatory 处理路由不到队列的消息

2. Broker 自身

  • 必须持久化(Kafka 顺序写磁盘 + Page Cache;RabbitMQ 队列/消息都要 durable + persistent)
  • 必须多副本(Kafka replication-factor ≥ 3、min.insync.replicas=2;RabbitMQ 镜像队列 / Quorum 队列)。单副本 Broker 宕机磁盘坏就永久丢

3. Broker → 消费端

  • 关闭自动 ack,改手动 ack:业务处理成功后再提交(Kafka commit offset / RabbitMQ basic.ack)
  • 顺序必须是"先处理,后提交位点"。反过来先提交再处理,处理途中崩溃 → 消息已确认但没处理完 → 丢

重复消费与幂等(至少一次的必然代价)

主流 MQ 默认 at-least-once:消费成功但 ack 丢失/超时 → Broker 重投 → 重复。不重复不能靠 MQ,只能靠消费端幂等:

  • 业务唯一键 + 唯一索引(插入重复直接冲突丢弃)
  • 前置查询去重表 / Redis SETNX(msgId) 标记已处理
  • 状态机:只允许 待支付 → 已支付 单向流转,重复的支付回调被状态判断挡掉

详见 业务幂等性设计。

顺序消费

全局有序需要"单分区 + 单消费者",等于放弃并行,吞吐极低。实际只做分区内有序:

  • 把需要保序的消息用同一个 key 路由到同一分区(如同一订单号 hash 到同一 partition)
  • 该分区单线程消费(消费端不能对同分区消息并发处理)

顺序的隐藏坑

  • Kafka 生产端 max.in.flight.requests > 1 且开重试时,重试会打乱顺序 → 要保序需 max.in.flight=1 或开启幂等 producer(enable.idempotence=true,5 以内也能保序)
  • 消费端拉一批后用线程池并发处理会打乱顺序 → 保序场景只能同分区串行

消息积压

积压 = 生产速率持续 > 消费速率。处理:

  • 加消费者——但 Kafka 消费并行度受分区数上限约束(一个分区同组内只能被一个消费者消费)。分区不够先扩分区
  • 临时扩容 + 转存:紧急时用一批消费者只做"搬运",把积压消息快速转到扩容后的新 topic(更多分区),再慢慢消费
  • 定位根因:下游 DB 慢、消费逻辑重、有毒消息反复重试卡住(→ 死信队列隔离)

事务消息 / 本地消息表(先写库还是先发 MQ)

"更新本地 DB + 发 MQ 通知"要原子成功,否则出现"库改了 MQ 没发"或"MQ 发了库没改"。方案:

  • RocketMQ 事务消息:发半消息(对消费者不可见)→ 执行本地事务 → 提交/回滚半消息;Broker 定时回查本地事务状态兜底
  • 本地消息表:本地事务里把"待发消息"写进同库的消息表(与业务同一事务原子提交)→ 独立线程轮询消息表投递 MQ → 投递成功删除。用 DB 事务保证"库改了消息一定记下来"
  • Kafka 事务:transactional.id + 生产端事务,配合消费端 read_committed 做 Kafka 内部的 EOS,但跨越到外部 DB 仍需上面两种

详见 分布式事务。

死信队列 / 延迟队列

  • 死信队列(DLQ):消费失败超过重试上限的消息转入 DLQ,隔离"有毒消息"避免阻塞正常消费,后续人工/补偿处理
  • 延迟队列:RabbitMQ 用 TTL + 死信交换机(消息过期变死信路由到目标队列)或 rabbitmq-delayed-message 插件;Kafka 原生不支持,需分级 topic 或外部调度触发

为什么这么做

  • 为什么默认 at-least-once 而非 exactly-once:网络不可靠,ack 可能丢失,Broker 无法区分"消费者没收到"和"收到了但 ack 丢了",只能重投。严格 EOS 需要 Broker 与消费端两阶段事务协调,吞吐和复杂度代价大,多数业务用"至少一次 + 消费幂等"更划算。
  • 为什么顺序只做分区内:全局顺序要串行化所有消息,与"高吞吐靠并行"根本冲突。按 key 分区把"需要保序的一组"收敛到一个分区,既保序又保留分区间并行——用局部有序换整体吞吐。
  • 为什么要多副本 + ISR:单副本 Broker 是单点,磁盘损坏即永久丢数据。多副本 + min.insync.replicas 保证"至少 N 个副本确认才算写成功",是持久性与可用性的平衡点。

为什么别的选择不行

  • 为什么不用 DB 表当队列:DB 轮询有延迟、高频轮询压垮 DB、行锁竞争严重、无原生的消费组/分区/回溯/削峰能力。低频简单场景可以,高吞吐必须专用 MQ。
  • 为什么不 acks=0 图快:发了不确认,Leader 崩溃或网络抖动直接丢消息且无感知。只有能容忍丢失的场景(如可重算的指标上报)才用。
  • 为什么消费端不用自动 ack:自动 ack 在"收到即确认",业务处理崩溃就丢消息。手动 ack「处理成功后确认」才是可靠消费的地基。

沉淀结论

面试速答清单:

  • 三处不丢:生产 acks=all+重试、Broker 持久化+多副本(ISR≥2)、消费手动 ack 且"先处理后提交"
  • 默认至少一次 → 必然重复 → 消费端幂等(唯一索引/去重表/状态机)兜底
  • 顺序只做分区内:同 key 同分区 + 单线程消费;注意 producer 重试与消费端线程池打乱顺序
  • 积压:扩分区 → 加消费者(受分区数限制)→ 紧急转存到更多分区的新 topic
  • 库+MQ 原子:RocketMQ 事务消息 / 本地消息表 / Kafka 事务
  • 死信队列隔离有毒消息;延迟消息 RabbitMQ 靠 TTL+死信,Kafka 需外部方案

Kafka vs RabbitMQ vs Pulsar 选型

维度KafkaRabbitMQPulsar
模型分区日志(拉)队列 + 交换机(推)分层:计算(Broker)+存储(BookKeeper)
吞吐极高(顺序写+零拷贝)中高
延迟毫秒~十毫秒微秒~毫秒(低延迟强)毫秒
顺序分区内有序队列内有序分区内有序
路由/灵活性弱(topic+分区)强(direct/topic/fanout/header)中
回溯消费强(保留期内任意 offset)弱(消费即删)强
典型场景日志/流处理/大数据管道/削峰业务解耦/复杂路由/低延迟任务云原生/多租户/存算分离/长期存储

一句话选型:要吞吐和流处理 → Kafka;要灵活路由和低延迟业务消息 → RabbitMQ;要云原生存算分离和多租户 → Pulsar。

记忆口诀

  • 三处不丢:生产 acks=all+重试 / Broker 持久化+多副本(ISR≥2) / 消费手动 ack 先处理后提交
  • 交付语义:默认至少一次 / 必然重复 / 消费端幂等兜底(唯一索引·去重表·状态机)
  • 顺序积压:同 key 同分区+单线程保序 / 积压先扩分区再加消费者 / 紧急转存新 topic
  • 选型一句:吞吐流处理选 Kafka / 灵活路由低延迟选 RabbitMQ / 存算分离多租户选 Pulsar

内容来源

关键点整理自 Kafka 官方文档、RabbitMQ 文档、Apache Pulsar 文档与《Designing Data-Intensive Applications》(Martin Kleppmann,第 11 章 Stream Processing)重写为五段式。请以各 MQ 官方文档为准。

自测:合上资料能说清楚吗?

  1. 一条消息从生产到消费,可能在哪几个环节丢失?每个环节各靠什么机制兜底?
参考答案

三处会丢:生产→Broker 靠 acks=all+重试/publisher confirm;Broker 自身 靠持久化+多副本(ISR≥2);Broker→消费 靠手动 ack 且"先处理后提交位点"。缺一段即漏。

  1. 既然 MQ 保证不了不重复,那"不重复消费"到底靠什么实现?举两种手段。
参考答案

默认 at-least-once,ack 丢失就重投,不重只能靠消费端幂等:业务唯一键+唯一索引冲突丢弃;或 Redis SETNX/去重表标记 msgId;或状态机单向流转挡掉重复回调。

  1. 为什么顺序消费只能做"分区内有序"而非全局有序?怎么实现分区内有序?
参考答案

全局有序要串行化所有消息,与高吞吐靠并行根本冲突。做法:同 key hash 到同一分区+该分区单线程消费;注意 producer 重试(max.in.flight>1)和消费端线程池会打乱顺序。

  1. 消息积压几百万,为什么单纯加消费者不一定管用?正确处理顺序是什么?
参考答案

Kafka 同组内一个分区只能被一个消费者消费,并行度受分区数上限约束。顺序:先扩分区→再加消费者→紧急时用一批消费者只做搬运,转存到更多分区的新 topic 再慢慢消费。

  1. 对比 Kafka 与 RabbitMQ:模型、吞吐、路由灵活性、回溯能力有何差异?各自典型场景?
参考答案

Kafka 分区日志(拉)、吞吐极高、路由弱、回溯强,适合日志/流处理/削峰;RabbitMQ 队列+交换机(推)、吞吐中、路由灵活(direct/topic/fanout)、低延迟但消费即删无回溯,适合业务解耦/复杂路由。

最近更新: 2026/9/10 11:38
Prev
Redis 版本演进 & 分布式
Next
分布式事务 · 2PC / TCC / Saga / 最终一致性