2047 字
约 6 分钟
7
S25-J11 · 消息队列与 Kafka 题库:不丢、不重、不乱序

适用读者:准备中高级面试,被问"你的 MQ 怎么保证可靠性"的人。
前置:S17(分布式与中间件)、S25-J10(缓存)、J07(三层骨架)。
覆盖旧文:3489/3490/3491/3492/3493/3496、3570(消息队列面试题)→ 7 篇 / 约 9 万字符。
扩展点:E-S25-034(消息三问:不丢/不重/不乱序)、E-S25-035(积压百万的四种解法与代价)、E-S25-036(Kafka 高性能的四个原因与延迟消息的三种实现)。
下一篇:S25-J12 · 分布式与微服务题库。


1. 一句话结论

MQ 的可靠性只有一条主线:生产 → 存储 → 消费 三段,每段都问"丢了会怎样、重复了会怎样、乱序了会怎样"。 答"开 confirm 就可靠了"会在第二个追问倒下——confirm 只保证到达 Broker,不保证落盘。

出处:A-3493(消息 100% 不丢)、A-3489(积压)、A-3490(延迟消息)、A-3492(Kafka 性能)、A-3496(架构作用)。


2. 为什么用 MQ:三个真实动机

动机 说明 代价
解耦 上游不必知道下游有多少消费者 链路变长,问题定位难
异步 非关键路径移出主流程,RT 下降 最终一致,用户可能看到"延迟生效"
削峰 洪峰入队,下游按能力消费 峰值时延增加,队列成为单点

反面:不是所有异步都值得。RT 敏感且必须强一致的链路(支付扣款)不适合纯异步,应走"同步扣款 + 异步通知"。


3. 消息三问(E-S25-034)

3.1 不丢:三段分别加固

段 丢失点 手段
生产 网络抖动、Broker 未落盘 同步发送 + 重试 + acks=all(Kafka)/ 事务消息(RocketMQ)
存储 页缓存未刷盘、副本未同步 刷盘策略(同步/异步)、min.insync.replicas>=2、多副本
消费 先提交位点后处理 处理成功后再提交(手动 ACK / 手动 commit)

完整的"100% 不丢"链条:

生产者 confirm(acks=all + retries)
  → Broker 落盘 + 多副本 ISR
  → 消费者手动提交位点(处理成功之后)
  → 失败进重试队列 / 死信队列 + 告警

判据:"不丢"的最后一环永远在消费端——先提交位点再处理,是最常见的隐性丢失。

3.2 不重:幂等是唯一出路

MQ 的语义是 at-least-once(至少一次),"恰好一次"要靠业务侧去重:

手段 说明 边界
唯一键去重表 msg_id 或业务唯一键建唯一索引 需清理历史,表会膨胀
状态机幂等 只允许 待支付 → 已支付,重复消息被状态拒绝 需要状态字段
Redis SETNX 标记 处理前占位,TTL 过期 宕机可能丢标记 → 仍需兜底
版本号/CAS UPDATE ... WHERE version = ? 天然幂等,适合计数类

判据:"MQ 保证不重"是伪命题,正确说法是"MQ 保证至少一次,业务保证幂等"。

3.3 不乱序:三个层次的顺序

层次 Kafka 做法 说明
分区内有序 同一 key 落同一 partition 全局有序需单分区(牺牲并行度)
消费有序 单消费者单线程消费该分区 多线程会打乱
业务有序 按业务主键分区 + 状态机校验 跨分区不可能有序

取舍:"全局有序"与"高吞吐"互斥。电商订单按 orderId 分区即可,不需要全局有序。


4. 积压百万怎么办(E-S25-035)

解法 做法 代价
扩消费者 加实例(不超过分区数) Kafka 分区数固定 → 超出无效
扩分区 增加 partition 并重平衡 重平衡期间不可用;顺序性受影响
批量消费 max.poll.records 调大 + 批量入库 单批失败重试代价大
降级丢弃 非核心消息丢弃/采样 业务损失,需评估
转存重放 落到对象存储,闲时回放 实现复杂,延迟高
提升单条处理速度 优化下游(慢 SQL/远程调用) 治本,但需要定位

排查顺序:先看消费速率 vs 生产速率,再看单条处理耗时(多数积压是下游慢,不是消费者少)。


5. Kafka 为什么快(E-S25-036)

原因 机制
顺序写 追加日志,避免随机 IO(磁盘顺序写接近内存带宽)
页缓存 读写走 OS page cache,不自己管理内存
零拷贝 sendfile 直接从页缓存到网卡,省两次拷贝
批量 + 压缩 生产者攒批(linger.ms、batch.size),整批压缩
分区并行 水平扩展吞吐

追问:Kafka 会丢消息吗? 会——acks=1 + 异步刷盘 + 副本未同步时;可靠性靠配置换,不是默认保证。

5.1 延迟消息的三种实现

方案 做法 适用
定时轮询 扫表找到期消息 量小、精度要求低
延时队列(RabbitMQ) TTL + 死信队列 中小规模、单机
时间轮(RocketMQ / Kafka 自建) 按 5.8 节的时间轮分层插入,到期投递 量大、精度秒级
Kafka 无原生支持 自建"延迟 topic + 定时转发"或外部调度 需自行实现

RocketMQ 5.x 的原生延迟消息支持任意时刻(deliverTimeMs),4.x 只支持 18 个固定级别——版本差异要主动说。


6. 自研一个 MQ:四个必答模块

① 存储:追加日志 + 索引(offset → 物理位置),顺序写
② 投递:拉模式(push 长轮询模拟),消费位点由客户端管理
③ 可靠性:副本同步(Leader/Follower + ISR)、刷盘策略
④ 有序与重试:分区键路由、重试队列 + 死信队列

面试加分点:说清"为什么消费端用拉而不是推"——拉模式让消费端按自己能力取,天然背压;推模式在慢消费者处会堆积或丢。


7. 反例:四种常见错答

说法 问题
"开了 confirm 就不会丢" 只保证到达 Broker;落盘/副本仍需配置
"MQ 保证 exactly-once" 语义是 at-least-once,恰好一次要靠幂等
"加机器就能解决积压" 受分区数限制;且多数积压是下游慢
"Kafka 全局有序" 只有分区内有序

8. 自测

  1. 消息不丢的三段加固分别是什么?
  2. 为什么"不重"不能靠 MQ 保证?给出两种幂等手段。
  3. 积压百万的排查顺序与四种解法?
  4. Kafka 快的四个原因?顺序写的收益从哪来?
答案
  1. 生产(acks=all+重试)、存储(刷盘+多副本 ISR)、消费(处理成功后手动提交)。
  2. 语义是 at-least-once;唯一键去重表、状态机/CAS。
  3. 先比生产/消费速率,再看单条耗时;扩消费者/扩分区/批量/降级/转存/优化下游。
  4. 顺序写、页缓存、零拷贝、批量压缩(+ 分区并行)。

9. 下一篇

S25-J12 · 分布式与微服务题库 —— 高可用的度量与手段、分布式锁的高可用、分布式事务四方案对比、服务隔离与熔断的阈值设计。


本篇属于系列《S25 · 求职与面试》。旧文来源:A-3489/3490/3491/3492/3493/3496、A-3570;扩展点 E-S25-034/035/036。技术细节以 duplicate_ref 指回 S17 主讲系列。

S25-J11 · 消息队列与 Kafka 题库:不丢、不重、不乱序
https://www.clxhxhhr.top/posts/5592/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。