Skip to content

模块导航: 总入口 · 总纲与方法 · 后端场景 · AI与算法工程 · 国企与金融 · 网络问答


第四章 消息队列(第79-96题) ​

本章导览(元问题与表面主题逐字取自 《核心考点与元理论》§六)

  • 本章真正考的那一个问题: 异步解耦之后,正确性责任转移到了谁身上?
  • 表面主题: 不丢/不重/有序、积压
  • 题号与题量: 第 79–96 题,共 18 题;刷题时不要按知识点记,要按上面那一个元问题记。

79. 架构评审:引入 MQ 的理由与代价(MQ 价值与代价) ​

【考察内容】MQ 核心价值与引入代价(解耦/异步/削峰)

【题目】架构评审会上,你提出要引入消息队列,有人反对说“多一个组件多一份故障”。请讲清楚:引入 MQ 到底解决什么痛点(削峰/解耦/异步),不引入会怎样,以及引入后新增了哪些风险与成本?

【参考答案】三大核心作用:

  1. 解耦:A 系统产生订单事件,需要通知 B/C/D 系统——同步调用会“新增一个下游就改一次代码”;通过 MQ,A 只发消息,下游自行订阅,新增下游不动 A 的代码;
  2. 异步:下单链路同步调用“发短信、送积分、更新推荐”导致接口 RT 长——MQ 异步化后主链路只做核心操作,RT 从 2s 降到 200ms;
  3. 削峰:秒杀瞬间 10 万请求,直接打到 DB 会挂——MQ 缓冲,消费端按 DB 可承受的速率慢慢消费(如每秒 1000),保护下游;
  4. 代价:引入新组件(运维成本)、消息可靠性问题(丢失/重复/乱序需要治理)、最终一致性(业务要接受异步延迟)。
  5. 容量估算(步进):大促峰值 10 万下单请求/10s ≈ 1 万 QPS 瞬时写入;DB 稳态只能扛约 1000–2000 TPS,需 MQ 削峰约 5–10 倍;积压缓冲要先判过载形态,两种算法差两个量级:持续过载(流量长期高于处理能力,如大促 30 分钟)→ 缓冲 ≈(到达率−处理率)×持续时长=(1 万−1500)×1800 ≈ 1530 万条;瞬时尖峰(总量有限、峰后即回落,如题干 10 万请求压在 10 秒内)→ 缓冲上限=突发总量−期间已处理量≈ 10 万−1500×10 ≈ 8.5 万条,约 60–100 秒即可排空。本题是尖峰型:误按 30 分钟持续算会把缓冲高估约 180 倍(1530 万÷8.5 万)、照此扩容就是浪费;工程上取 min((到达率−处理率)×持续时长, 突发总量),再按消息体折算磁盘与内存。异步化后主链路 RT:串行慢下游 2s → 只保留本地事务 200ms 量级;通知/积分类 SLA 常约定秒级,对账 T+0/T+1。
  6. 失败与降级:MQ 集群不可用时,下单主事务不依赖 MQ 成功与否——本地消息表/事务消息暂存,恢复后补发——但降级通道不能把尖峰原样压回同一套 DB:本题按订单写含索引维护与锁竞争取约 1500 TPS(低于单机 MySQL 常见的 2000–5000 TPS 上限,与第 5 条的缓冲算式同一口径),而 MQ 挂时原本要被异步吸收的瞬时写入可达 1 万 QPS,即约 7 倍超载;若你的库实测能到 2000–5000,超载倍数与限流阈值要同步下调,照此答等于用降级打垮 DB。降级三件套要同时给出:① 只留核心事件、② 本地表批量合并写、③ 入口按 DB 实测余量限流(与第 91 题同口径);非核心副作用(推荐、运营通知)直接关闭;生产端发送失败写本地重试表并告警,禁止静默丢弃;消费端失败进死信人工介入。引入前必须书面确认:哪些业务可接受最终一致、延迟窗口多长、降级开关负责人。

【原理溯源】

  • 为什么解耦必须靠“事件广播”而不是接口调用? 同步调用是编译期耦合:调用方必须知道下游地址、协议、签名,新增下游 = 改上游代码并重新发布。MQ 把“调用”变成“发布事实”:生产者只声明“发生了什么”,谁关心谁订阅。上游与下游在时间、空间、接口三维度解耦——时间上不必同时在线,空间上不必互相知道地址,接口上只约定消息 schema。

  • 为什么异步能显著压 RT? 同步链路的 RT = 核心操作 + 所有下游串行/并行耗时的最慢者。发短信(外部 SMS 网关 300ms)、积分(跨服务 50ms)、推荐更新(重计算 800ms)都挂在主链路上时,用户等的是整条链。异步化后主链路只保留“落库成功”这一正确性关键路径,其余变成可延迟的副作用——RT 由“最慢下游”决定变成由“本地事务”决定。

  • 削峰的本质是什么? 把突发写入速率与稳定处理速率解耦。DB 的稳态吞吐是相对固定的(连接池、索引、锁竞争),瞬时 10 倍流量不是“处理慢一点”而是直接雪崩。MQ 以磁盘/内存缓冲吸收峰值,消费端按自身容量拉取——代价是端到端延迟变成可变量,积压时延迟线性上涨。

  • 引入 MQ 新增了哪些不可回避的复杂度? ① 可靠性从“同步返回码”变成“分段确认”(生产/Broker/消费三段都要治理);② 一致性从 ACID 变成最终一致(业务要接受“支付成功但积分晚几秒”);③ 顺序性从“调用顺序天然保证”变成“需要按 key 路由 + 单线程消费”;④ 运维多一类有状态中间件(磁盘、副本、Rebalance、监控)。这些不是“配置一下就好”,而是要落到代码与预案里。

  • 什么场景不该上 MQ? 强一致且同步可完成的操作(扣款余额)、实时性要求毫秒级的查询、调用链只有 1 个下游且低频、需要同步拿到下游结果才能继续的分支逻辑——这些场景上 MQ 是为了异步而异步,增加故障面却不换来收益。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
要不要引入 MQ?
├─ 是否存在“生产快于消费”的瞬时峰值?
│   └─ 是(秒杀/推送/日志洪峰)→ 削峰价值成立
├─ 下游是否 ≥2 个且还会继续增加?
│   └─ 是 → 解耦价值成立
├─ 下游操作是否可延迟(秒级/分钟级可接受)?
│   └─ 是 → 异步价值成立
├─ 以上三条全否 → 不要上 MQ(本地调用更简单)
└─ 以上命中 ≥1 条
    ├─ 强一致 + 必须同步拿到结果 → 同步 RPC,MQ 只做旁路通知
    └─ 允许最终一致
        ├─ 需要事务消息/金融级可靠 → RocketMQ
        ├─ 超高吞吐日志/大数据 → Kafka
        └─ 中小流量复杂路由 → RabbitMQ

判断口诀:先问“峰值、多下游、可延迟”三问,全否就别上;上了就要预算可靠性治理成本。

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“这题核心是权衡:MQ 用最终一致和运维复杂度,换解耦、异步、削峰三类收益”
0:30–2:30三价值展开每条价值配一个“不引入会怎样”的反例:新增下游改代码、RT 被慢下游拖死、峰值打挂 DB
2:30–3:30代价清单主动列:可靠性三段治理、最终一致、顺序问题、运维成本——主动谈代价是加分项
3:30–4:30边界场景说明什么场景不该上:强一致同步操作、毫秒级实时、单下游低频
4:30–5:00收尾“一句话:MQ 不是银弹,是用‘异步 + 最终一致’换‘扩展性 + 抗峰值’的架构交易”

【关键数字】

参数经验值说明
同步 RT 优化幅度2s → 200ms 量级异步化砍掉慢下游后的典型落差
削峰消费比峰值:稳态 ≈ 5–10:1秒杀常见;消费端按 DB 容量定速
MQ 单机吞吐量级Kafka 十万级;RocketMQ 十万级;RabbitMQ 万级选型时的粗粒度锚点
异步延迟可接受窗口积分/通知 秒级;对账 T+0/T+1业务必须书面确认 SLA
引入后故障面多 1 个有状态组件需独立监控、备份、演练
峰值:稳态消费比5–10:1秒杀常见;MQ 缓冲 + 定速消费
削峰后消费速率按 DB/下游容量反推如 DB 1000 TPS → 消费端限流 1000
缓冲积压容量(先分过载形态,两式差两个量级)持续过载:(到达率−处理率)×持续时长;瞬时尖峰:突发总量−期间已处理量;工程取 min(两式)秒杀属尖峰型(≈8.5 万条),误按持续型×30 分钟会高估约 180 倍(1530 万÷8.5 万);磁盘/保留策略按算出的值装下

【追问链】(三层)

L1|“什么场景不适合 MQ?” → 三类:① 强一致且必须同步完成(扣余额);② 实时性要求极高(毫秒级查询/推送确认);③ 简单调用、单下游、低频——上 MQ 是过度设计。核心判断:下游结果是否阻塞主流程、延迟是否业务可接受。

L2|“异步后用户点了支付,怎么知道成功了?” → 主链路同步保证核心正确性(订单落库、支付回调处理成功)立即返回;副作用(积分、通知)异步。体验上返回“处理中/成功”,提供订单状态查询与推送;资金类以支付渠道回调+订单状态机为准。面试可补数字:若下游积分 SLA 为 5s,用户无感;若超 30s 未到账触发补偿对账。

L3|“MQ 整个集群挂了,下单还能做吗?” → 必须能。方案:① 核心链路与 MQ 解耦——下单主事务不依赖 MQ 是否可用;② 降级:本地消息表暂存、MQ 恢复后补发(见 91 题)——但本地表仍写同一套 DB,必须同时「只留核心事件+批量合并写+入口按 DB 实测余量限流」,否则 5–10 倍尖峰会把降级做成二次故障;③ 非核心异步功能直接关闭。原则:MQ 是增强可用性与扩展性的旁路,不能成为下单成功与否的同步依赖。

【评分标准】

档位答案特征
60 分能背出解耦/异步/削峰三个词,各给一句定义
80 分每条价值都有“不引入会怎样”的反例;能主动列出可靠性/最终一致/运维三类代价
95 分说清削峰=速率解耦、解耦=三维度解耦;给出“不该上 MQ”的边界;MQ 挂掉时的降级要说三件套(本地消息表+只留核心事件/批量合并写+入口按 DB 余量限流),只答「写本地消息表」等于把尖峰压回同一套 DB

【关联题】

【自测】

  1. 判断对错:只要 QPS 高,就该上 MQ 削峰。 参考答案: 错。若消费端能力与生产端匹配、无突发峰值、且需要同步结果,上 MQ 只增加复杂度。削峰只解决“峰值 ≫ 稳态”问题。
  2. 架构评审时,反对者说“多一个组件多一份故障”,你怎么回应? 参考答案: 承认代价,再算收益:MQ 把故障域从“全链路同步串联”变成“主链路短 + 异步可重试”;同时给出可靠性三段防护与降级预案,证明引入后整体可用性可更高,而不是简单多一个单点。
  3. 为什么说解耦是“发布事实”而不是“调用接口”? 参考答案: 发布事实只声明事件与 schema,不绑定下游地址与存在性;新增订阅方无需改生产者。接口调用则把下游身份硬编码进上游,形成编译期耦合。

80. 业务方投诉“消息丢了”,订单状态没推进(消息不丢失) ​

【考察内容】消息不丢失是 MQ 最高频题

【题目】业务方投诉:异步任务经常“消息丢了”,比如订单支付成功但积分没到账。消息在“生产者发送→Broker 存储→消费者消费”三个阶段都可能丢失。请给出每个阶段不丢的配置与机制(ack、持久化、重试、确认机制)。

【参考答案】逐环节防护:

  1. 生产端:
    • Kafka:acks=all(等所有 ISR 副本确认)+ 重试机制;同步发送并在失败时记录告警;
    • RabbitMQ:开启 publisher-confirm(Broker 返回 ack/nack,nack 重发);
    • RocketMQ:同步发送(syncSend),不丢才返回成功;
  2. Broker 端:
    • 持久化:Kafka topic 副本数≥2 + min.insync.replicas=2 + unclean.leader.election 关闭(缺 mir 时 acks=all 只需 leader 落盘即算成功,leader 崩仍丢已 ack 消息);RabbitMQ 队列持久化 + 消息 delivery_mode=2;RocketMQ 刷盘策略(同步刷盘更强);
    • 集群高可用(副本机制),防止单节点宕机丢数据;
  3. 消费端:
    • 手动 ack(而不是自动):业务处理成功后才提交 offset/ack,处理失败不 ack,消息重新投递;
    • 关闭“收到即确认”(RabbitMQ autoAck=false);
  4. 兜底:发送失败写本地重试表/MQ 自身重试;消费失败进死信队列人工处理;对账(业务侧核对消息与结果)。
  5. 容量估算(步进):日订单 100 万、每单平均 3 条异步消息 → 日消息约 300 万条,均值 ≈ 300万/86400 ≈ 35 msg/s,大促峰值 ×50 ≈ 1750 msg/s(约 0.18 万)。若单条消息 1KB、保留 3 天:存储 ≈ 300万×1KB ≈ 3GB/日,保留 3 天约 9GB,再乘副本×3 约 27GB(磁盘与保留策略要按这个量预留)。生产端 acks=all 同步确认时,单分区写入 RT 约 2–10ms,吞吐低于 acks=1,但换来不丢。
  6. 失败与降级:生产端 confirm 失败 → 重试 N 次后落本地重试表/告警,禁止业务假装成功;Broker 单副本故障 → 副本接管,若 ISR 为空宁可拒绝写也不丢已确认数据;消费失败 → 不 ack,重试后进死信;业务方仍报丢 → 启动对账(消息流水 vs 业务结果),差错走补偿任务。绝对不丢不可达,工程目标是“可发现 + 可补偿”。

【原理溯源】

  • 为什么“消息会丢”不是 bug 而是默认设计? MQ 默认走性能优先路径:生产者不等落盘确认(RabbitMQ publisher-confirm、RocketMQ 刷盘都是可选项而非默认,Kafka 自 3.0 起默认已是 acks=all+幂等生产者,不再算 fire-and-forget)、Broker 异步刷盘、消费者 auto-ack——任一环节“先确认再干活”或“不确认”都会在进程崩溃时丢数据。不丢是配置 + 代码约定换来的,不是开箱即用。

  • 生产端丢消息的根因是什么? 发送方把“写入本地缓冲/网络发出”当成成功。网络分区、Broker 未落盘、leader 切换时,消息只存在于发送方内存或已发出但未持久化。所以必须有同步确认:Kafka acks=all 等 ISR 全部写入;RabbitMQ publisher-confirm 等 Broker 回调;RocketMQ syncSend 等写入结果。失败要重试 + 告警,不能静默吞掉。

  • Broker 端为什么会丢? ① 未持久化:消息只在 page cache/内存,进程崩溃即丢;② 副本不足:leader 挂且 follower 数据落后;③ 非干净选举:允许落后副本成为 leader,丢已确认消息。对策是“写盘策略 + 副本数 + 禁止 unclean election”三件套。Kafka 关闭 unclean.leader.election.enable 后,ISR 空时宁可不可用也不选落后副本。

  • 消费端丢消息的根因是什么? auto-ack / 过早提交 offset。消费者收到消息就确认,但业务处理中崩溃——Broker 以为消费成功,消息不再投递。正确姿势:处理成功后再 ack/commit offset;失败不 ack,依赖重投。代价是可能重复(at-least-once),需幂等(见 81 题)。

  • 为什么理论上仍难做到“绝对不丢”? 任一确认信号都可能在“已持久化但 ack 丢失”或“已 ack 但下游业务失败”之间存在窗口。工程上用事务消息/本地消息表消掉生产侧窗口(92/93 题),用对账消掉消费侧与业务侧不一致——所以答案永远是“三段防护 + 对账兜底”,而不是“配一个参数就完事”。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
用户报“消息丢了”
├─ 先定位丢失段
│   ├─ 生产日志无发送失败、Broker 无消息 → 生产端未开 confirm/acks,或异步发送失败被吞
│   ├─ Broker 有消息、消费者无拉取记录 → 消费未启动/订阅错误/Rebalance 异常
│   └─ 消费者拉到了但业务未生效 → auto-ack 过早 / 处理中崩溃 / 业务异常未回滚
├─ 配置核对清单
│   ├─ 生产:Kafka acks=all + 重试;Rabbit confirm;RocketMQ sync
│   ├─ Broker:持久化 + 副本≥2 + 同步刷盘(金融) + 禁 unclean 选举
│   └─ 消费:手动 ack + 处理成功再提交 + 死信兜底
└─ 最终兜底:对账任务(消息表 vs 业务结果)发现静默丢失

口诀:三段都要确认;确认越晚越安全,但重复越多——不丢与不重必须成对设计。

【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“消息丢失必须按生产、Broker、消费三段拆,每段有独立的确认机制”
0:30–2:30三段展开每段:怎么丢的因果 + 对应 MQ 的关键配置(acks/confirm/持久化/手动 ack)
2:30–3:30因果闭环强调:越安全的确认越慢、越容易重复,所以必须接幂等
3:30–4:30兜底死信、重试表、对账——承认“极端仍可能丢”,工程靠对账发现
4:30–5:00收尾“不丢 = 三段确认 + 兜底;不丢与不重是一对 trade-off”

【关键数字】

参数经验值说明
Kafka acks0 / 1 / allall 最安全;1 在 leader 故障时可能丢
Kafka 副本数≥2,金融 ≥3ISR 同步副本
RabbitMQ delivery_mode2(persistent)配合队列持久化
RocketMQ 刷盘同步刷盘 > 异步刷盘同步更安全,吞吐更低
消费确认时机业务成功后auto-ack 仅适合可容忍丢失的日志类
对账周期T+0 准实时或 T+1资金/积分建议 T+0
日消息量估算日订单×每单消息数100万单×3 ≈ 300万条/日
峰值 msg/s均值×峰值系数均值35 → 大促×50 ≈ 1750(约0.18万)
存量存储日量×消息体×保留天×副本注意磁盘水位与保留策略

【追问链】(三层)

L1|“开了 acks=all 是不是就绝对不丢?” → 生产端接近不丢,但 Broker 宕机、消费 auto-ack、业务失败仍可能丢。acks=all 只覆盖“生产者 → ISR 副本”这一段,不是端到端保证。

L2|“消费者处理成功了,但提交 offset 前挂了,消息会怎样?” → offset 未提交,重启后从上次提交点重投 → 重复消费,不会丢。这正是 at-least-once:宁可重复不可丢失,所以消费必须幂等。可补:Kafka 提交 offset 默认 5s 一批,崩溃窗口内未提交的消息都会重投;用 enable.auto.commit=false + 业务成功后手动 commit,可把重复面缩到“业务成功但 commit 前挂掉”这一极窄窗口。

L3|“如果业务方坚持要‘恰好一次’呢?” → 严格 exactly-once 在分布式中昂贵。实践拆成:生产端幂等生产者(Kafka enable.idempotence,3.0 起默认开启)+ 事务(跨分区原子写)+ 消费端幂等。对多数业务,at-least-once + 消费幂等已达到业务上的“效果恰好一次”,成本远低于端到端 exactly-once。

【评分标准】

档位答案特征
60 分知道要开 ack,但分不清生产 ack 与消费 ack
80 分能按生产/Broker/消费三段给出配置与机制,点名 acks=all、confirm、手动 ack
95 分说清每段“为什么会丢”的因果;主动提对账兜底;指出不丢必然带来重复、需幂等;能对比三家 MQ 的实现差异

【关联题】

【自测】

  1. Kafka 的 acks=1 在什么情况下会丢消息? 参考答案: leader 写入成功并 ack,但 follower 尚未同步时 leader 宕机且发生非干净选举,已 ack 消息丢失。acks=all 可避免(等 ISR 全部写入)。
  2. 为什么消费端要“处理成功再 ack”? 参考答案: 若收到即 ack,处理中崩溃则消息被标记成功不再投递 → 丢失。处理后再 ack,崩溃时未 ack 会重投 → 最多重复,不丢。
  3. 判断对错:只要 Broker 做了副本,消息就一定不丢。 参考答案: 错。还需生产端确认、消费端正确 ack、禁止不干净选举,否则任一段仍可能丢。

81. 网络抖动导致同一条消息被消费两次(重复消费) ​

【考察内容】消息重复消费是 MQ 必考题

【题目】网络抖动或消费者宕机重平衡,导致同一条消息被投递了两次,业务被重复处理(比如重复发奖)。怎么保证“消息只被处理一次”?消费端的幂等设计怎么做?

【参考答案】

  1. 认知:重复消费在至少一次(at-least-once)投递语义下必然存在(ack 丢失、重试、Rebalance、进程崩溃),只能靠“消费幂等”解决;
  2. 幂等方案:
    • 业务唯一键 + 数据库唯一索引:处理前先插入唯一键(如 order_id),重复插入报错则跳过——最可靠;
    • 状态机幂等:消息处理推进状态机(如订单:待支付→已支付),重复消息发现状态已推进则忽略;
    • Redis SETNX 去重:以消息 ID/业务 ID 为 key,SETNX 成功才处理(配合过期时间),Redis 挂了由 DB 唯一索引兜底;
    • 消息表去重:本地记录已处理消息 ID 表,查重后处理;
  3. 关键:幂等要基于业务唯一键(订单号、支付流水号)而非消息本身 ID(同业务可能产生多条消息);
  4. 设计原则:消费逻辑本身要“可重入”(同参数执行多次结果一致)。
  5. 容量/一致性窗口估算:幂等存储(DB 唯一索引/Redis SETNX)要按峰值消费 TPS 设计——若峰值 5000 msg/s,Redis SETNX 需能扛同量级写(单实例通常 10 万+ ops,足够);DB 唯一索引插入在热点业务键上可能成瓶颈,建议按业务键分表或走 Redis 预检 + DB 最终落库。去重 key 的 TTL:至少覆盖最大重试窗口 + 时钟偏差 + 业务最长处理时间,常见 24h~7 天;过短会在延迟投递时失效导致重复。
  6. 失败与降级:Redis 去重层宕机 → 降级只靠 DB 唯一索引(可扛较低 QPS)或本地缓存布隆预筛;唯一键冲突 → 视为“已处理”直接 ack,不再进死信(否则死信被打爆);状态机发现已推进 → 同样静默成功。原则:幂等失败要“安全成功”而不是“安全失败”,否则重复流量会拖垮核心库。

【原理溯源】

  • 为什么 at-least-once 必然重复? 确认信号与业务副作用不在同一个原子单元里:消费者先做业务再 ack,若 ack 丢失/超时/进程在 ack 前崩溃,Broker 认为未成功会重投。反之若先 ack 再做业务,崩溃就丢消息。分布式里没有“既确认又保证副作用恰好一次且不丢”的免费午餐——要么可能丢,要么可能重。at-least-once 选择了“宁重不丢”。

  • 哪些操作会触发重复? ① 消费者 ack 丢失后重投;② 生产者超时重发(发送成功但 ack 未收到);③ Kafka Rebalance:分区再分配后新消费者从上次提交 offset 重放;④ 消费者处理成功但提交 offset 前崩溃;⑤ 网络分区导致的重复投递。这些都不是“偶发 bug”,而是协议允许的正常路径。

  • 为什么幂等键要用业务唯一键而不是消息 ID? 同一业务事件可能被重发成多条消息(不同 msgId),用 msgId 去重会漏;同一 msgId 也可能对应需要多次处理的语义边界不清。业务唯一键(订单号 + 业务动作)才能表达“这件事只应发生一次”。

  • 为什么唯一索引是最可靠的幂等手段? 数据库唯一约束是与业务写入同一个本地事务的原子检查:插入成功 = 第一次处理;冲突 = 已处理过。不存在 Redis 那种“检查与执行不原子”的窗口。Redis SETNX 只能做前置过滤减少冲突,最终正确性靠 DB。

  • 状态机为什么天然幂等? 状态迁移是“从状态 A 到 B”,若已是 B,重复消息判定为 no-op。前提是迁移条件明确、单向,且并发下用乐观锁/唯一约束保护(WHERE status='A' 更新)。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
如何做消费幂等?
├─ 消息是否可映射到稳定业务唯一键(订单号/流水号)?
│   ├─ 是 → DB 唯一索引 / 状态机(正确性优先)
│   └─ 否 → 先补齐业务键,或退化为消息表按 msgId 去重(有漏重风险)
├─ 吞吐要求?
│   ├─ 高 → Redis SETNX 前置过滤 + DB 唯一约束兜底
│   └─ 中低 → 直接 DB 唯一索引
└─ 业务是否本身就是状态推进?
    └─ 是 → 状态机 + 乐观锁(天然幂等)

口诀:唯一约束保正确,Redis 只挡流量,状态机管推进。

【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“重复消费在 at-least-once 下必然存在,解法不是消灭重复,而是幂等”
0:30–2:00根因讲清 ack 与副作用非原子 → 要么丢要么重;列 Rebalance/重试等触发场景
2:00–3:30方案分层唯一索引(正确性)→ 状态机(业务语义)→ Redis(性能)→ 消息表(兜底)
3:30–4:30关键细节强调业务唯一键 > msgId;Redis 必须设 TTL;冲突异常要能识别为“已处理”
4:30–5:00收尾“幂等是 MQ 可靠性闭环的最后一块拼图,与不丢、有序成套设计”

【关键数字】

参数经验值说明
Redis 去重 TTL覆盖「最大重试窗口+时钟偏差+业务最长处理时间」(常见 24h~7 天)只对齐重试窗口会在延迟投递时漏防;过长占内存
重复率网络抖动时 0.1%–1% 不罕见必须工程化处理,不能当小概率
唯一索引冲突耗时毫秒级高并发下需 Redis 前置
SETNX + DB 双层Redis 挡 90%+ 重复DB 仍可能被穿透
峰值幂等 QPS= 峰值消费 msg/sRedis 轻松;DB 需关注热点键
唯一键选择业务键优先于消息 ID同业务多条消息时消息 ID 无效

【追问链】(三层)

L1|“加个消息 ID 去重不就行了?” → 不够。同一业务可能产生多条 msgId(生产端重发),会漏;且 msgId 与业务正确性无关。应基于订单号/流水号等业务唯一键。

L2|“Redis SETNX 和 DB 唯一索引都用,先查哪个?” → 先 Redis 后 DB。Redis SET key NX EX 是 O(1)、单实例能扛 10 万+ ops,先过滤掉绝大多数重复,避免每条消息都去打 DB 唯一索引(热点业务键上会形成索引/行锁争用,正是本参考答案第 5 条点名的瓶颈)。但 Redis 不能当唯一真相——重启、主从切换、key 过期都会让去重记录消失,所以最终兜底是 DB 唯一索引:插入冲突即判定重复并放弃业务。顺序反过来等于把去重压力全压到 DB。

L3|“消费逻辑里调用了下游接口,怎么保证重试时不重复扣款?” → 下游接口本身要提供幂等(幂等号=业务唯一键)。消费端重试时传同一幂等号;下游用唯一约束/状态机防重。若下游无幂等能力,则消费端需本地记账“已调用成功”后再跳过,或改为“先落地意图再异步调用”并接受对账补偿。

【评分标准】

档位答案特征
60 分知道要“去重”,方案只有 Redis set
80 分承认 at-least-once 必然重复;给出唯一索引/状态机/Redis 分层;强调业务唯一键
95 分讲清 ack 与副作用非原子的根因;说明 Redis 只做前置、DB 保正确;能处理下游接口幂等与 Rebalance 场景

【关联题】

【自测】

  1. 为什么说重复消费“无法避免,只能幂等”? 参考答案: 因为 ack 与业务副作用不在同一原子单元;at-least-once 协议下,崩溃/网络问题会导致“业务已做但未确认”从而重投。
  2. 幂等键为什么不用 msgId? 参考答案: 同一业务事件可能对应多条 msgId(生产端重发),用 msgId 会漏重;业务唯一键才能表达“只应发生一次”。
  3. 判断对错:Redis SETNX 成功就可以不落库唯一索引。 参考答案: 错。Redis 非持久、可能丢、与业务非原子,只能前置过滤;正确性必须靠 DB 唯一约束或状态机。

82. 订单的“创建→支付→发货”消息乱序了(消息顺序性) ​

【考察内容】消息顺序是 MQ 三大可靠性问题之一

【题目】订单系统的状态流转依赖消息顺序:必须先消费“创建”、再“支付”、再“发货”,一旦乱序订单状态就错。消息队列怎么保证这种顺序性?Kafka/RocketMQ 分别怎么实现?代价是什么?

【参考答案】

  1. 认知:全局有序成本极高(只能单分区单消费者,吞吐骤降),生产上只做局部有序(同一业务 key 的消息有序);
  2. 方案:
    • Kafka:同一业务 key(如 order_id)作为消息 key,hash 到同一个分区(同一分区内消息有序);消费者组内一个分区只能被一个消费者线程消费,单线程按序处理;
    • RocketMQ:顺序消息(MessageQueueSelector 按业务 id 选队列)+ 单消费者;
    • RabbitMQ:同一条队列 + 单消费者(队列天然 FIFO,但要避免多消费者并发消费同一队列);
  3. 代价:同一 key 的消息串行处理,吞吐受限——拆 key 粒度(用户级/店铺级)平衡;热点 key 可在允许乱序的业务维度上拆(如按用户/店铺分队列);不能按「订单子流程」拆——那会把同一个 order_id 的创建/支付/发货打散到不同队列,直接破坏题干要求的同单有序;真要提速只能改成状态机版本号+条件更新来容忍乱序(另见原理溯源的旁路 topic 方案,同样必须配版本裁决);
  4. 失败处理:消息处理失败不能跳过继续(会乱序),需重试阻塞或进死信并暂停后续(业务补偿);
  5. 兜底:对账/重放机制纠正极端乱序。
  6. 容量与并行度估算:订单状态消息峰值假设 1 万 msg/s,若 order_id 哈希到 N 个分区,则有序消费最大并行度 = N。要保证同一订单串行:同 order_id 固定分区;不同订单可并行。若单消费者处理 500 msg/s,则需要至少 20 个分区 + 20 个消费者实例才能扛 1 万/s。代价:热点商户/热点订单单分区可能过热,需在业务键中加入合理分散因子(如 seller_id+order_id)——前提是同一订单的创建/支付/发货三条消息上该因子恒等(同一卖家、同一订单),否则同一 order_id 会因字段取值不同落到不同分区,顺序照样破;所以分散因子只能用下单时就固定、与事件类型无关的稳定字段(如订单号自带的分片号),绝不能用「消息类型」「当前状态」这类会变的属性。也不能破坏“必须有序的最小键”。
  7. 失败与降级:单分区内消息失败 → 该分区阻塞(保序必然牺牲局部可用);处理策略:① 可跳过的非关键乱序消息进死信;② 关键状态走状态机幂等,旧状态消息直接忽略;③ 极端时对该分区做“人工跳过 + 对账修复”。禁止为提速把同 key 打散到多分区——那会重新引入乱序。Rebalance 期间可能短暂重复/乱序,消费端必须幂等。

【原理溯源】

  • 为什么全局有序不可行? 全局 FIFO 要求所有消息进入同一队列/分区,并由单线程消费——并行度降为 1,吞吐塌到单线程处理能力。订单场景真正需要的是同一订单内有序,订单之间无顺序依赖。用全局有序去满足局部需求,是用 1% 的需求杀掉 99% 的吞吐。

  • 为什么“同 key 同分区”能保证局部有序? Kafka 分区内消息以 append-only 日志存储,offset 单调递增,分区内天然 FIFO。key hash 决定分区 → 同一订单的创建/支付/发货落入同一分区 → 按 offset 消费即按序。分区是顺序的边界。

  • 为什么一个分区不能被组内多个消费者并行消费? 两个原因:① 并发处理会打乱“先创建后支付”的处理顺序;② offset 以“组 + 分区”为单位提交,多消费者提交会互相覆盖,进度错乱。所以组内是分区独占分配。

  • 重试为什么会破坏顺序? 失败消息若被转发到重试 topic 与后续消息并行,或跳过失败继续消费后续——创建失败被跳过、支付先被处理,状态机直接错乱。顺序场景下失败必须阻塞当前 key 的后续消息(或暂停该分区),直到成功或进死信并人工介入。

  • 代价从哪来? 同 key 串行 = 单 key 吞吐上限是单线程处理速度。热点 key(秒杀单品、大 V 直播)会把该分区打满。缓解:拆 key 粒度(用户/店铺)、把可乱序的旁路事件另建 topic(创建/支付/发货这类同单主链路禁止拆子流程——会把同 order_id 打散,与本条第 3 项的约束相反),或业务层允许最终一致 + 状态机防乱序(乱序到达时用版本号/状态前置条件拒绝过期消息)。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
需要什么级别的顺序?
├─ 业务上真需要全局 FIFO?
│   └─ 几乎从不 → 不要选全局有序
├─ 是否只要求同一实体(订单/用户)内有序?
│   └─ 是 → 局部有序:同 key 同分区 + 分区独占消费
├─ 是否存在热点 key?
│   ├─ 否 → 直接 hash 分区即可
│   └─ 是 → 拆 key 粒度 / 旁路 topic 分流(主链路禁拆子流程)/ 状态机版本号防乱序
└─ 消费失败怎么办?
    ├─ 阻塞重试(保序)
    └─ 进死信 + 暂停该 key 后续(人工/补偿)

口诀:顺序以分区为界;同 key 同分区;失败不能跳。

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“全局有序吞吐不可接受,生产只做同 key 局部有序”
0:30–2:30机制Kafka:key hash → 同分区 → 分区内 FIFO + 分区独占;RocketMQ MessageQueueSelector;Rabbit 单消费者
2:30–3:30代价与热点同 key 串行;热点 key 拆分策略
3:30–4:30失败处理不能跳过;阻塞重试或死信+暂停;对账兜底
4:30–5:00收尾“顺序、吞吐、失败处理三者要一起设计”

【关键数字】

参数经验值说明
全局有序吞吐降为单线程消费能力通常不可接受
局部有序并行度= 分区数(非热点 key 时)分区数决定上限
热点 key 判定单分区 lag 持续上涨需拆 key
顺序失败策略阻塞 ≤ 数次重试后进死信无限阻塞会堵分区
有序并行度上限= 分区数(同 key 单分区内串行)1万 msg/s ÷ 单消费者500 = 约20分区
单分区消费能力单线程/单实例慢逻辑会成为整 key 热点
热点 key 策略业务键粒度取舍不能为分散而破坏最小有序键

【追问链】(三层)

L1|“Kafka 不是能保证顺序吗?” → 只保证单分区内有序。多分区时跨分区无序。要业务顺序必须同 key 同分区,并保证组内分区独占。

L2|“消费者多线程消费同一个分区行不行?” → 组内不行。一个分区在同一消费组里只能分给一个消费者;两个线程共享同一分区就得共享同一份消费进度,offset 由谁提交、提交到哪条都说不清,Rebalance 时还会互相覆盖,直接造成重复或丢失。要提并行度只有两条正路:① 加分区(保持同 key 同分区,顺序不破);② 消费者实例数 ≤ 分区数,多出来的只会空闲。若坚持在单进程内多线程处理同一分区,只能自己拉下来再按 key 派发到工作线程,这会破坏分区内有序(完成顺序不定),且 offset 必须等最慢的一条才能提交。

L3|“消息已经乱序到达了,消费端还能救吗?” → 能。用状态机前置条件 + 版本号:处理“支付”时校验当前状态必须是“已创建”;版本号更小的旧消息直接丢弃或补偿。这把“传输层顺序保证”降级为“业务层状态一致性”,对最终一致系统更稳健。

【评分标准】

档位答案特征
60 分说“Kafka 保证顺序”
80 分知道局部有序 vs 全局有序;说清同 key 同分区 + 单消费者
95 分解释分区是顺序边界;讲清失败阻塞与热点 key;能提状态机兜底乱序

【关联题】

【自测】

  1. 为什么生产环境几乎不用全局有序? 参考答案: 全局有序要求单分区单消费者,并行度为 1,吞吐无法支撑业务;实际只需同业务实体内有序。
  2. 同一订单的三条消息 hash 到不同分区会怎样? 参考答案: 跨分区无顺序保证,可能先消费支付再消费创建,状态错乱。必须保证同 order_id 同分区。
  3. 判断对错:顺序消息消费失败应该跳过,先处理后面的。 参考答案: 错。跳过会破坏顺序,应阻塞重试或进死信并暂停该 key 后续消息。

83. 一觉醒来 MQ 积压了几百万条消息(消息积压) ​

【考察内容】消息积压是生产事故高频题

【题目】凌晨上线的一个 bug 让消费者全挂了,早上发现 MQ 积压了几百万条消息,业务链路大面积延迟。先恢复消费还是先修 bug?积压的消息怎么快速消化(扩容、临时消费者、跳过)?会不会挤垮下游?

【参考答案】

  1. 先定位原因:
    • 消费能力不足(消费者实例少、消费逻辑慢);
    • 下游故障(DB/依赖服务挂了,消费一直失败重试);
    • 生产端突发流量(大促)超过消费速率;
  2. 处理流程:
    • 修复下游故障:如果是下游问题,先恢复下游(否则扩容也没用);
    • 第 0 步:先回滚或热修(题干根因就是「凌晨上线的 bug 让消费者全挂」):扩容、临时 topic、转发都跑在同一份有 bug 的代码上,消费照样失败,重试还会把下游再压一遍;所以下列手段都以「消费代码已能正常处理」为前提;
    • 临时扩容消费者:增加消费者实例(注意:Kafka 单分区只能一个消费者消费,分区数=最大并行度——可临时增加分区并重新分布,或用“多线程消费”);更快的办法:把积压消息转发到新建的临时 topic(分区数扩大),用更多消费者并行消费,消费完再处理结果;
    • 降级策略:非核心消息(如日志、统计)在积压严重时可跳过/丢弃(业务可接受);核心消息不能丢;
    • 限流生产端:暂停/减缓非关键生产,先清积压;
  3. 根治:优化消费逻辑(批量消费、异步化)、评估分区数与消费者数匹配、监控堆积量告警(堆积超过阈值自动告警/扩容);
  4. 关键防坑:不要盲目重置位点、或用新消费组从 auto.offset.reset=latest 起(会把积压直接跳过,等效丢消息);单纯重启不会丢已提交 offset——位点存在 Broker 上;扩容要保证消费幂等(重复消费仍安全)。
  5. 容量估算(步进):积压 L=500 万条,下游恢复后安全消费速率 C=500 msg/s,则清空时间 ≈ 500万/500 = 10000s ≈ 2.8 小时(未计生产新增)。若可扩到 5000 msg/s(10 实例×500,且分区≥10),约 17 分钟(500万÷5000=1000s)。但这个数与本题的限流口径必须二选一:本条后半与第 6 条又按「DB 只能扛 1000 TPS」限流,按 1000/s 算是 500万÷1000=5000s≈1.4 小时,恢复期 20%→50%→100% 的爬坡也不能越过该阈值——答题要先声明用的是哪个口径,否则「17 分钟」与「限流 1000」互相抵消。生产端若仍以 P=2000 msg/s 写入、消费 C=500,则积压还在以 1500 msg/s 上涨——必须同步限流生产。磁盘:500 万×1KB×副本3 ≈ 15GB 量级,一般可承受;但保留策略与磁盘水位要盯住。下游 DB:清积压时写入冲到峰值,若 DB 只能扛 1000 TPS,消费限流就不得超过 1000。
  6. 失败与降级:下游未恢复前禁止盲目扩容;非核心 topic(日志/统计/埋点)可丢弃或采样;核心消息不可丢,但可“只处理最近 N 小时 + 更早走对账补偿”;消费端必须幂等(临时 topic 转发会带来重复);恢复期并发按 20%→50%→100% 爬坡,观察下游错误率。监控:lag、消费 RT、下游错误率、磁盘水位四条曲线一起看。

【原理溯源】

  • 积压的数学本质是什么? 积压量 L 的变化:dL/dt = 生产速率 P − 消费速率 C。当 P > C 持续,L 线性上涨。清积压只有三条路:提高 C、降低 P、或对部分消息“取消消费”(丢弃/跳过非核心)。

  • 为什么“下游挂了”时扩容消费者是白扩? 消费瓶颈不在消费者线程,而在下游。扩容只会让更多线程同时打挂掉的下游 → 更多重试 → 更大压力 → 可能把下游彻底打死。先修下游,再谈扩容。

  • Kafka 为什么“加机器不加速”? 组内并行度 = 分区数。消费者数 > 分区数时多余消费者空闲。所以扩容前先看分区数;不够就扩分区或用临时 topic 转发。临时 topic 方案避免了直接改原 topic 分区带来的 Rebalance 与迁移风险。

  • 为什么“直接改线上分区数”要谨慎? 增分区会改变 key→partition 映射,同 key 消息可能进入新分区,破坏局部有序;并触发大规模 Rebalance,短暂影响可用性。事故中更稳妥的是新 topic 转发,原 topic 保持不动。

  • 清积压会不会挤垮下游? 会。积压解除时消费速率会冲到峰值,若下游刚恢复仍脆弱,可能二次打挂。正确做法:恢复下游后限流爬坡(逐步放开消费并发),配合批量与背压,而不是全速冲。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
发现积压
├─ 根因?
│   ├─ 上线 bug/代码异常 → 第 0 步回滚或热修(扩容跑在同一份坏代码上是白扩)
│   ├─ 下游故障 → 先恢复下游(禁盲目扩容)
│   ├─ 消费逻辑慢 → 优化逻辑/批量/异步;再扩分区与实例
│   └─ 生产突增 → 限流非关键生产 + 扩容消费
├─ 如何加速?
│   ├─ 分区数充足 → 加消费者实例
│   ├─ 分区数不足 → 新建临时大分区 topic 转发
│   └─ 非核心消息 → 丢弃/跳过(业务确认)
└─ 恢复期
    ├─ 限流爬坡,防止二次打挂下游
    └─ 全程幂等 + 监控 lag 回落

口诀:先根因后扩容;分区=并行度;恢复要限速。

【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“积压是 P>C 的流量问题,先分清是下游挂、消费慢还是生产突增”
0:30–2:30处置顺序先回滚/热修(题干根因是凌晨上线的 bug) → 恢复下游 → 扩容/临时 topic → 非核心丢弃 → 限流生产
2:30–3:30Kafka 特殊点分区=最大并行度;临时 topic 而不是直接改分区
3:30–4:30防二次事故恢复限流爬坡;幂等;不乱动 offset
4:30–5:00收尾“积压处置 = 根因 + 并行度 + 下游保护”

【关键数字】

参数经验值说明
消费并行度上限= Kafka 分区数超出的消费者空闲
积压告警阈值绝对量(如 10 万)或 lag 持续上涨时长(5–10min)按业务 SLA 定
临时 topic 分区原分区数 × 2–10视下游容量
恢复爬坡并发 20% → 50% → 100%每档观察下游 RT/错误率
非核心可丢日志/统计/部分埋点需业务事先确认
清空时间积压量 ÷ 安全消费速率500万÷500/s≈2.8h;÷5000/s≈17min;若下游只能 1000/s 则≈1.4h——17min 与「限流 1000」二选一,答题要先声明口径
下游保护限流≤ 下游稳态容量如 DB 1000 TPS → 消费限 1000
积压磁盘条数×消息体×副本500万×1KB×3≈15GB

【追问链】(三层)

L1|“积压了先加消费者行不行?” → 看根因。下游挂了加消费者是白扩甚至有害;代码有 bug 时扩容照样失败,还会用重试把下游再压一遍,必须先回滚/热修;只有「消费能力不足且分区够」时才有效。先看错误率与下游健康。

L2|“能不能直接把分区数从 12 调到 60?” → 技术上可以,但:① 触发全量 Rebalance;② key 映射变化可能破坏同 key 顺序;③ 迁移期间可用性抖动。事故中更稳:新建分区更多的临时 topic,把积压消息转发过去并行消费,原 topic 不动。若业务要求严格有序,扩分区前必须确认状态机/对账能否吸收短期乱序,否则只扩“可乱序”的旁路 topic。

L3|“几百万条积压,下游只恢复到每秒 500,要跑很久,业务等不了怎么办?” → 分层处理:① 非核心丢弃;② 核心中可合并的做批量聚合(如同订单多条合并);③ 与业务确认“只处理最近 N 小时”的降级窗口,更早的走对账补偿。目标从“全部实时消费”改为“核心正确 + 可接受延迟”。

【评分标准】

档位答案特征
60 分说“加消费者扩容”
80 分先分根因;知道 Kafka 分区=并行度;会用临时 topic
95 分先答「根因是发布引入的 bug 就先回滚/热修」,再谈修下游与扩容;恢复限流爬坡、非核心丢弃策略、幂等与 offset 防坑

【关联题】

【自测】

  1. 下游 DB 挂了导致积压,扩容消费者有用吗? 参考答案: 没用且有害。瓶颈在下游,应先恢复下游,再限流爬坡恢复消费。
  2. 为什么 Kafka 加消费者实例有时速度不变? 参考答案: 消费者数超过分区数后,多余实例分不到分区,空闲。并行度上限=分区数。
  3. 判断对错:清积压时应全速消费尽快追平。 参考答案: 错。全速可能二次打挂刚恢复的下游,应限流爬坡。

84. 选型会:Kafka/RabbitMQ/RocketMQ 用哪个(MQ 选型) ​

【考察内容】MQ 选型是高频综合题

【题目】公司要做消息中间件选型,候选 Kafka、RabbitMQ、RocketMQ。按吞吐量、可靠性、顺序性、事务消息、延迟、运维成本几个维度对比,并给出你的业务场景(比如日志收集 vs 交易订单)该怎么选?

【参考答案】

  1. Kafka(顺序性:key→分区内原生局部有序,跨分区无序;组内分区独占消费,无原生死信/延迟):优点——超高吞吐(顺序写+零拷贝+批量)、天然分布式分区、消息可回溯(按 offset 重放)、生态好(大数据/日志);缺点——功能简单(无死信/延迟等高级特性)、消息可能重复(at-least-once)、管理较复杂。适合:日志、埋点、大数据、削峰场景;
  2. RabbitMQ(顺序性:三家最弱——只有单队列+单消费者才有序,多消费者或调大 prefetch 即乱序):优点——功能丰富(死信队列、优先级、灵活路由;延迟队列靠 TTL+DLX 组合或官方 delayed-message-exchange 插件实现)、Erlang 稳定可靠、社区成熟;缺点——吞吐中等(万级)、水平扩展不如 Kafka(旧的高可用方案「镜像队列」3.9 起废弃、4.0 起已移除,现行要用 Quorum 队列)。适合:业务解耦、复杂路由、中小流量;
  3. RocketMQ(顺序性:官方顺序消息 API=MessageQueueSelector 队列选择器+失败阻塞重试,最省事):优点——阿里出品、吞吐高(十万级)、事务消息/延迟消息/死信内置、Java 生态友好;缺点——社区相对小、功能多但运维复杂。适合:电商交易、金融业务消息(需要事务消息和可靠性的场景);
  4. 选型结论:日志/大数据→Kafka;交易/事务/延迟消息→RocketMQ;轻量业务/复杂路由→RabbitMQ;技术栈统一优先考虑团队熟悉度。
  5. 容量锚点(选型用):日志/埋点类常见日增 TB 级、峰值 数十万~百万 msg/s → Kafka;交易订单消息峰值 1万~10万 msg/s、单条 KB 级、需事务/延迟 → RocketMQ;企业内业务解耦 数百~数千 msg/s、复杂路由 → RabbitMQ。容量粗算:峰值 msg/s × 消息体 × 86400 × 副本 × 保留天 = 磁盘(别漏掉「×86400 秒/天」这一步);例:5万 msg/s×1KB ≈ 50 MB/s ≈ 4.32 TB/日,×3副本×3天 ≈ 约 39 TB。消费端能力要 ≥ 生产峰值,否则必须削峰策略。选型时同时估:集群规模、运维人力、机房/跨城复制带宽。
  6. 失败与降级:任何 MQ 都要做生产 confirm、Broker 持久化副本、消费手动 ack+死信;集群故障时业务侧本地消息表/降级开关;多 MQ 并存时要定义“哪条链路依赖谁”以及故障时的隔离边界。Kafka 无原生延迟消息时用“时间轮/延迟表+二次投递”兜底;RocketMQ 半消息回查超时要有本地事务状态表,避免悬挂。

【原理溯源】

  • 为什么 Kafka 吞吐最高? 架构目标是“高吞吐日志流”:顺序写磁盘、页缓存、零拷贝 sendfile、生产/消费批量与压缩、分区并行。它用功能做减法换吞吐——没有内置事务消息(早期)、延迟队列、复杂路由。选型时要明白:Kafka 的强项来自“把 MQ 当日志存储系统”。

  • 为什么 RabbitMQ 适合复杂路由但吞吐有限? 每条消息经过 exchange 路由到队列,Broker 做更多逻辑;复制本身也贵——旧镜像队列已被 Quorum 队列(Raft)取代,镜像队列 4.0 起移除。它强在协议与路由模型(topic/fanout/direct、死信、优先级、TTL),弱在水平扩展。中小流量业务解耦非常合适。

  • 为什么交易场景偏 RocketMQ? 电商/金融需要:事务消息(本地事务与发消息原子)、延迟消息(关单)、死信、顺序消息 API 这些 RocketMQ 内置,Kafka 要自研;但「按业务 key 局部有序」Kafka 原生就有(key→分区 + 分区内 FIFO,见第 82 题),RocketMQ 多出的是队列选择器与失败阻塞重试。对交易团队,“开箱即用的事务消息”价值远大于“再多 5 倍吞吐”。

  • “可靠性”三家差在哪? 都能通过配置做到接近不丢(副本、刷盘、confirm)。差异在语义完备性:RocketMQ 事务消息提供生产端与本地事务的原子性;Kafka 提供幂等生产者 + 事务(跨分区原子,但不等于业务事务消息);RabbitMQ 有 confirm + 事务(channel tx,性能差,少用)。

  • 运维成本如何估? Kafka/RocketMQ 集群组件多(Kafka 4.0 起只支持 KRaft,ZooKeeper 模式已移除;RocketMQ 是 NameServer),需要专人;RabbitMQ 简单但扩展天花板低。还要看团队语言栈:Java 团队 RocketMQ 顺手;大数据生态 Kafka 必选。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
场景是什么?
├─ 日志 / 埋点 / 大数据管道 / 超高吞吐削峰 → Kafka
├─ 交易 / 支付 / 需要事务消息或延迟消息 → RocketMQ
├─ 中小流量业务解耦 / 复杂路由 / 优先级 / 快速上手 → RabbitMQ
└─ 多场景并存
    ├─ 允许多 MQ → 按场景分用(常见:Kafka 做日志,RocketMQ 做交易)
    └─ 必须统一 → 交易优先则 RocketMQ;数据平台优先则 Kafka + 自研事务层

附加检查:
- 团队是否熟悉运维?
- 是否需要跨机房 / 多租户?
- 生态绑定(Flink/Spark → Kafka)

口诀:吞吐与生态选 Kafka,交易特性选 RocketMQ,路由与轻量选 RabbitMQ。

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“选型不是比参数表,是按场景匹配能力边界”
0:30–2:30三家对比每家:架构取向 + 优势 + 硬伤 + 典型场景,各 30–40 秒
2:30–3:30落地结论日志→Kafka,交易→RocketMQ,轻量路由→RabbitMQ
3:30–4:30工程因素运维、团队栈、生态、是否允许多 MQ 并存
4:30–5:00收尾“没有最好,只有最匹配;交易特性与吞吐不可兼得时按业务分级”

【关键数字】

参数经验值说明
Kafka 吞吐单机数十万–百万级 msg/s批量 + 压缩时
RocketMQ 吞吐十万级 msg/s交易特性更全
RabbitMQ 吞吐万级 msg/sQuorum 队列再低一档
顺序性Kafka:key→分区内原生局部有序,跨分区无序;RocketMQ:队列选择器+阻塞重试,官方 API 最直接;RabbitMQ:单队列+单消费者才有序,最弱题目给出的这几个维度都要比到,不能只比吞吐/功能/生态
事务消息RocketMQ 原生;Kafka 事务≠业务事务消息选型分水岭
副本数≥2,生产常 3可靠性
端到端延迟RabbitMQ 推模式毫秒级最低;Kafka 受 batch/linger.ms 影响常在 10ms~百 ms 级;RocketMQ 毫秒~十毫秒级题干的「延迟」指这个,与「有没有延迟消息功能」是两回事
Kafka 吞吐锚点单机数十万–百万 msg/s批量+压缩时
RocketMQ 吞吐锚点十万级 msg/s交易特性更全
RabbitMQ 吞吐锚点万级 msg/sQuorum 队列再低一档
磁盘粗算峰值msg/s×体×86400×副本×天5万/s×1KB≈4.32TB/日,×3×3≈39TB

【追问链】(三层)

L1|“Kafka 能保证不重复吗?” → 默认仍是 at-least-once,可能重复;生产端重复由幂等生产者去重——Kafka 3.0 起 enable.idempotence 与 acks=all 已是默认值(3.0 之前要显式开);消费端仍要幂等。跨分区 exactly-once 用事务 API,但业务上的“恰好一次”仍推荐消费幂等。

L2|“RabbitMQ 高吞吐场景行不行?” → 万级 QPS 内可以;更高吞吐、要水平扩展、或已绑定大数据生态时换 Kafka/RocketMQ。队列复制开销大(现行是 Quorum 队列,镜像队列已移除),日志洪峰不适合。补充判断:若峰值稳定在 3k–5k msg/s 且路由复杂,RabbitMQ 仍可胜任;一旦出现“大促 10 倍尖峰 + 需要回放”,应提前按 Kafka/RocketMQ 规划分区与容量,而不是上线后再换。

L3|“已经用了 Kafka,但业务要事务消息,怎么办?” → 三条路:① 业务侧本地消息表(93 题)——最通用;② 换/并用 RocketMQ 做交易通道;③ Kafka 事务 + 消费端幂等做“写多分区原子”,但解决不了“本地 DB 事务与发消息”的原子,仍需消息表或对账。不要误以为 Kafka 事务 = RocketMQ 事务消息。

【评分标准】

档位答案特征
60 分能说出三家名字和大概用途
80 分按吞吐/可靠性/顺序性/事务消息/延迟/运维成本六个维度对比,给出场景结论
95 分解释架构取向(Kafka 日志流、RocketMQ 交易特性、Rabbit 路由);指出 Kafka 事务≠事务消息;考虑运维与多 MQ 并存策略

【关联题】

【自测】

  1. 日志收集与订单状态通知,分别选什么?为什么? 参考答案: 日志选 Kafka(高吞吐、回放、大数据生态);订单通知选 RocketMQ(可靠、事务/延迟/死信内置,Java 交易栈)。
  2. 为什么不能说“Kafka 有事务所以等价于 RocketMQ 事务消息”? 参考答案: Kafka 事务保证跨分区读写的原子性与幂等生产,不解决“本地数据库事务与发消息”的两阶段问题;后者需要 RocketMQ 半消息或本地消息表。
  3. 判断对错:功能越多的 MQ 越好,应优先选功能全的。 参考答案: 错。功能多常伴随吞吐/复杂度代价。应按场景选:日志不需要事务消息,交易不需要百万 TPS 日志管道。

85. 自研消息队列技术预研,核心能力有哪些(MQ 设计) ​

【考察内容】“设计一个 MQ”是考察系统设计深度的经典题

【题目】团队做技术预研:想自研一个消息队列。从零设计,需要哪些核心能力?存储模型(日志/队列)、生产消费模型、ack 与重试、顺序、堆积、集群高可用分别怎么设计?

【参考答案】按“最小可用→逐步补齐”的顺序:

  1. 基础模型:生产者 → Broker(队列/分区)→ 消费者,消息的发送、存储、消费基本流程;
  2. 持久化:消息落磁盘(顺序写),解决重启丢消息——顺序写文件+批量刷盘;
  3. 高可用:副本机制(leader/follower),主故障自动选举,解决单点;
  4. 可靠性:生产端 ack、Broker 持久化、消费端 offset/ack 机制(处理成功才提交);
  5. 顺序性:分区/队列模型 + 同一 key 路由同分区 + 单消费者消费;
  6. 负载均衡:消费者组内分区分配(一个分区一个消费者)、Rebalance;
  7. 高级能力:延迟消息(时间轮/分级)、重试设计(独立重试队列/Topic+指数或分级退避+最大重试次数+不可重试异常直投死信;重试按延迟等级分档存放,避免定时器轮询全量消息;顺序 topic 的重试必须阻塞该 key 而不是并发重投,否则重新引入乱序)、死信队列(重试耗尽转人工,DLQ 要可查询、可回放)、事务消息(半消息+确认)、消息回溯(按 offset 重放)、削峰(消费限流);
  8. 其他:监控(积压量、消费 lag)、权限、压缩、多租户。
  9. 容量估算(自研 MQ 预研目标可写进设计文档):目标单机写入 5万–10万 msg/s(1KB 消息、3 副本、异步刷盘);同步刷盘场景降到 1万–3万 msg/s 量级。分区数规划:生产峰值 / 单分区消费能力;例峰值 5 万、单消费者 500/s → 至少 100 分区(再留 50% 余量)。存储:日增消息条数×体×副本×保留天;堆外内存/页缓存要能覆盖热数据。延迟消息:时间轮精度 vs 扫描频率的 trade-off(秒级精度可接受则定时扫描更简单)。
  10. 失败与降级:副本同步失败/ISR 不足时的写入策略(拒绝写 vs 允许落后副本);Broker 宕机选主期间生产端重试与本地缓存;消费 Rebalance 期间的重复与暂停;磁盘打满时的拒绝策略与告警;自研组件必须有旁路降级——业务可切本地消息表或第三方 MQ。没有监控(lag、刷盘耗时、副本延迟、拒绝写次数)的自研 MQ 不允许上生产。

【原理溯源】

  • 为什么存储模型选“追加日志”而不是“逐条队列”? 追加写(append-only)把随机 IO 变成顺序 IO,磁盘顺序写接近内存速度;同时天然支持“按 offset 读取/回放”。传统“消息出队即删”的队列模型难以高吞吐持久化,也不好做多订阅回放。Kafka/RocketMQ CommitLog 都走这条路。

  • 为什么必须先有持久化再谈高可用? 副本复制的是“已持久化的日志段”。若消息只在内存,副本同步窗口内宕机即丢。顺序写 + 批量刷盘是在“延迟、吞吐、安全”之间的经典折中(异步刷盘丢最后几毫秒,同步刷盘更安全)。

  • ack 体系为什么必须两端都有? 生产端 ack 解决“发出是否被持久化”;消费端 offset/ack 解决“是否被业务处理”。只做一端就会在另一端丢或重。设计时要把 at-least-once 当默认语义,并把幂等责任明确交给消费方。

  • 分区模型为什么同时解决并行与顺序? 分区是并行单元(可多线程/多实例),又是顺序边界(分区内 FIFO)。同 key 路由同分区得到局部有序;分区数决定消费者组最大并行度。这是“吞吐与顺序”的平衡点,而不是二选一。

  • Rebalance 为什么难? 消费者增减时要把分区重新分配,期间可能:分区暂停消费(延迟)、重复消费(offset 交接)、顺序抖动。设计要尽量缩短 rebalance 时间(增量协作协议)、并要求消费幂等。自研时这是最容易出事故的模块之一。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】(自研能力优先级)

text
自研 MQ 能力路线图
├─ P0 最小闭环
│   ├─ 追加日志存储 + 持久化
│   ├─ 生产 ack / 消费 offset
│   └─ 分区 + 单消费者
├─ P1 生产可用
│   ├─ 副本与选主
│   ├─ Rebalance
│   └─ 死信 + 重试
├─ P2 业务增强
│   ├─ 延迟消息
│   ├─ 事务消息
│   └─ 回放 / 压缩 / 监控
└─ 先问:为什么不直接用 Kafka/RocketMQ?
    除非有强定制(合规、特殊存储、深度嵌入平台),否则自研 ROI 极低。

口诀:先日志后副本,先 ack 后高级特性;没有强理由不要自研。

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“设计 MQ 的核心是追加日志 + 分区并行 + 分段确认”
0:30–2:30P0/P1存储、持久化、ack/offset、分区顺序、副本
2:30–3:30难点深挖选 1–2 个展开:顺序写为什么快、Rebalance 代价、事务消息半消息
3:30–4:30工程完整度死信、延迟、监控、权限、多租户
4:30–5:00收尾 + 反问“自研前先论证为何不用现成组件——多数时候答案是不该自研”

【关键数字】

参数经验值说明
顺序写吞吐数百 MB/s 级vs 随机写差 1–2 个数量级
批量刷盘攒批数十–数百条或数 ms平衡延迟与 IO
副本数2–3ISR 同步
默认语义at-least-once消费幂等必须
延迟精度4.x 离散档位(1s–2h 共 18 级)vs 5.x 定时消息可任意到点档位实现简单,最细到 1s
Rebalance 目标秒级完成越长积压越久
单机写入目标5–10万 msg/s(异步刷盘)/ 1–3万(同步)1KB、3副本经验值
分区数≥ 峰值 / 单消费者能力 × 1.5例:5万/500×1.5=150
存储日量×体×副本×天提前估磁盘与保留策略

【追问链】(三层)

L1|“为什么顺序写比随机写快?” → 机械盘避免寻道;SSD 减少写放大与擦除。顺序写可近乎满带宽,随机写受 IOPS 限制。日志型存储把“写路径”全部变成 append。

L2|“offset 存在哪?” → 分两层。消费进度(committed offset)存在 Broker 内部的压缩型 topic __consumer_offsets(键为 group + topic + partition),老版本曾放 ZooKeeper,现在客户端与 broker 之间的 OffsetCommit/OffsetFetch 也走 broker;ZK 只留 broker/consumer 的注册与再均衡协调(新版也已搬到 broker)。消费者进程本地只持有下一次要拉的 nextFetch,真正「谁消费到哪」以 broker 上的提交记录为准。所以 auto.commit.interval.ms 决定写盘频率,崩溃时未提交的那段会被重放——这就是 at-least-once 的重复来源(与第 80 题同一因果链)。

L3|“事务消息为什么要半消息?” → 先发完整消息,本地事务失败就产生脏消息;先提交本地事务再发,宕机则丢消息。半消息对消费者不可见,等本地事务结果再 commit/rollback,未知状态靠回查兜底——用“暂存 + 二次确认”换原子性。

【评分标准】

档位答案特征
60 分能列生产-Broker-消费、持久化、ack
80 分有优先级路线图;讲清分区与顺序、副本
95 分深挖顺序写/offset/重试设计(独立重试队列+退避+次数上限+不可重试直投死信,顺序 topic 的重试须阻塞该 key)/半消息/Rebalance 之一;主动质疑“该不该自研”;覆盖监控与死信

【关联题】

  • 同簇: 第 86 题(Kafka 高吞吐)、第 80/81/82 题(可靠性)、第 92/93 题(事务)、第 87 题(消费模型)
  • 方法论: 第 99 题(分布式事务选型,含本地消息表)

【自测】

  1. 为什么 MQ 存储多用追加日志? 参考答案: 顺序写高吞吐、实现简单、支持按 offset 回放;出队即删的队列模型难以高吞吐持久化与多订阅。
  2. 消费端 offset 提交的正确时机? 参考答案: 业务处理成功之后。过早提交会丢,过晚会重复(可接受)。
  3. 判断对错:自研 MQ 应优先实现延迟消息和事务消息。 参考答案: 错。应先完成持久化、ack、分区、副本的最小闭环;高级特性建立在可靠核心之上。

86. 单机几十万 QPS 写入,Kafka 凭什么(Kafka 高吞吐原理) ​

【考察内容】Kafka 高吞吐原理是大数据/后端双高频题

【题目】压测显示 Kafka 单机写入能到几十万 QPS,而传统 MQ 只有几万。Kafka 靠什么做到?请从磁盘顺序写、页缓存、零拷贝、批量、分区并行几个角度解释。

【参考答案】多维度设计:

  1. 顺序写磁盘:消息 append-only 追加到分区文件末尾,顺序写接近内存速度(600MB/s 级),避开随机 IO;
  2. 零拷贝:消费时用 sendfile(内核态直接发到网卡),避免用户态拷贝;生产时页缓存(page cache)直接写;
  3. 批量与压缩:生产者批量发送(batch)、Broker 批量写、消费批量拉取;支持压缩(lz4/zstd)减少网络与磁盘;
  4. 分区并行:topic 分成多个分区,分区是并行单元,多磁盘/多线程并行处理;
  5. 页缓存而非应用缓存:利用 OS page cache,读写都命中缓存;数据已落盘,进程重启后 page cache 通常仍可命中;但 page cache 在机器重启/断电后必然丢失,此时需回读磁盘;
  6. 稀疏索引:每个 segment 只索引少量偏移,查找快,索引内存占用小;
  7. 常数级开销:O(1) 磁盘读写(append 与读 offset 附近数据),不受数据总量影响。
  8. 容量估算:Kafka 单机“几十万 QPS”通常指 小消息、批量、压缩、异步发送 条件。粗算:单 broker 顺序写磁盘可达数百 MB/s~GB/s 级;若消息 200B、批量后压缩比 5:1,有效写入放大后仍可支撑数十万条/s。集群规划:broker 数 ≥ 目标峰值 TPS ÷ 单 broker 有效写入 TPS,再按「峰值 TPS × 副本数」的总写流量校验单机磁盘与网络余量,不足就上浮机器数;例目标 30 万 msg/s、单机有效 10 万、3 副本 → 先按写入吞吐算 broker 数:30万 ÷ 10万 = 3 台;再乘副本得到的是 9 个副本位(leader+follower 共 9 份数据/9 段写流量),这 9 个副本位要摊到更多机器上才不会被复制写放大拖垮——3 台是下限、按副本写流量预留到 5–9 台才是可落地的起步规模(先说清"单机有效 TPS"是否已含 follower 复制开销,两者会差 3 倍)。分区数:生产峰值 / 单分区吞吐,并保证 ≥ 消费者实例数。
  9. 失败与降级:开启高吞吐配置(acks=1、异步刷盘、大 batch)会提高丢消息风险——金融/交易链路必须回到 acks=all + 足够 ISR;磁盘/页缓存不足时顺序写优势消失,表现为吞吐骤降;生产端缓冲打满开始阻塞/丢弃,必须监控 buffer 与 request timeout。高吞吐场景的降级:允许日志类消息采样丢弃,交易类不可降级为“丢了也行”。

【原理溯源】

  • 为什么顺序写能接近内存速度? 避免了磁盘最贵的操作——寻道/随机写。HDD 顺序写可达数百 MB/s,SSD 更高;随机写受 IOPS 限制差 1–2 个数量级。Kafka 把所有写变成 segment 文件 append,把“磁盘很慢”的直觉改写成“磁盘顺序写很快”。

  • 零拷贝省了哪几次拷贝? 传统:磁盘 → 内核 page cache → 用户态缓冲 → Socket 缓冲 → 网卡。sendfile/transferTo 让内核直接从 page cache 送到网卡,跳过用户态往返,CPU 与延迟都下降。这是消费路径吞吐的关键。

  • 批量化为什么放大吞吐? 网络 RTT、系统调用、刷盘固定成本被摊到多条消息上。批量越大,单条消息成本越低(延迟上升)。压缩在批量之上进一步减少字节数——CPU 换带宽,通常划算。

  • 为什么用 OS page cache 而不是 JVM 堆内缓存? ① 堆内缓存受 GC 影响,大堆更痛;② page cache 进程重启后仍在(除非机器重启);③ 与 sendfile 无缝配合。代价:机器冷启动后要回盘,所以持久化与顺序写仍不可省。

  • 稀疏索引为什么够用? 消费是顺序近似顺序读,只需定位到大致 offset 附近再扫描少量数据。每 4KB 左右建一个索引点即可,索引可全部装入内存,查找 O(1) 级。

  • 与“传统 MQ 几万 QPS”差在哪? 传统 Broker 逐条路由、可能随机写、每消息一次确认、无批量/零拷贝深度优化。Kafka 把自己设计成分布式提交日志,用日志模型换吞吐,而不是功能丰富的逐条队列。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

text
要更高写入吞吐
├─ 生产端
│   ├─ 开 batch.size / linger.ms
│   ├─ 压缩 lz4/zstd
│   └─ 幂等+适当 acks(all 会略降吞吐)
├─ Broker
│   ├─ 分区与磁盘分散
│   ├─ 页缓存充足(内存)
│   └─ 段文件与刷盘参数
└─ 消费端
    ├─ 批量拉取
    └─ 零拷贝(默认行为,勿破坏)

口诀:批量摊固定成本,顺序写打满磁盘,零拷贝省 CPU,分区换并行。

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“Kafka 把自己做成分布式提交日志,用日志模型换吞吐”
0:30–2:30四件套顺序写、零拷贝、批量压缩、分区并行——每项一句机制一句收益
2:30–3:30页缓存与索引page cache vs 应用缓存;稀疏索引
3:30–4:30数据路径从生产者到消费者的路径串起来
4:30–5:00收尾“高吞吐来自放弃部分功能复杂度,这是架构取舍”

【关键数字】

参数经验值说明
顺序写带宽数百 MB/s 级远高于随机写 IOPS
单机写入数十万 msg/s(批量+压缩)与消息大小强相关
索引间隔约 4KB 一条索引项稀疏索引
批量linger.ms 5–20ms 常见用延迟换吞吐
压缩lz4 / zstd网络与磁盘双赢
segment默认 1GB滚动删除与索引
单机吞吐前提小消息+批量+压缩+顺序写参数一变数字差一个数量级
集群规模粗算峰值÷单机有效TPS×副本30万/10万=3 台下限,×3 副本=9 副本位(按复制写放大预留 5–9 台)
分区数≥ 消费者数,且匹配峰值分区=并行度上限

【追问链】(三层)

L1|“页缓存和直接写磁盘有什么区别?” → 写 page cache 立即返回,由内核异步刷盘;读常命中缓存。比每条消息 fsync 快得多。代价:掉电可能丢最后未刷盘数据(可配置刷盘策略)。

L2|“零拷贝对生产端有用吗?” → 基本不沾边,零拷贝吃的是读路径的红利。消费者(以及 follower 副本同步)从磁盘取数据时,Kafka 用 sendfile 让数据从 page cache 直接到网卡,省掉「内核→用户态→内核」两次拷贝和一次上下文切换;而生产端走的是写路径,收益来自顺序追加写 + page cache + 批量与压缩,与零拷贝无关。所以给生产端提吞吐要调 batch.size/linger.ms/压缩算法,而不是指望零拷贝;只有当 broker 需要把消息原样转发出去(消费、副本复制)时零拷贝才起作用——这也正是 Kafka「写一遍日志、多方各自读」的架构前提。

L3|“acks=all 会不会把吞吐打回原形?” → 会降低,因为要等 ISR 副本网络往返。但配合批量、幂等、足够 ISR,仍可保持很高吞吐。金融/核心业务值得用 all;日志类可 acks=1 换吞吐——这是可靠性与吞吐的显式 trade-off。

【评分标准】

档位答案特征
60 分背出顺序写、零拷贝、分区
80 分能解释批量化摊薄成本、页缓存角色
95 分画清生产→Broker→消费路径;说清 sendfile 省了什么;指出 page cache 掉电丢失与持久化关系;能谈 acks 对吞吐影响

【关联题】

【自测】

  1. Kafka 零拷贝主要优化的是哪条路径? 参考答案: 消费路径。Broker 用 sendfile 将 page cache 数据直接送网卡,避免用户态拷贝。
  2. 为什么批量化能提高吞吐? 参考答案: 网络 RTT、系统调用、刷盘等固定成本被多条消息分摊,单条成本下降。
  3. 判断对错:用了页缓存就不用落盘了。 参考答案: 错。page cache 机器重启/断电会丢;仍需顺序写落盘保证持久性。

持续学习,持续积累。