适用读者:准备中高级面试,被问"你的 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. 自测
- 消息不丢的三段加固分别是什么?
- 为什么"不重"不能靠 MQ 保证?给出两种幂等手段。
- 积压百万的排查顺序与四种解法?
- Kafka 快的四个原因?顺序写的收益从哪来?
答案
- 生产(acks=all+重试)、存储(刷盘+多副本 ISR)、消费(处理成功后手动提交)。
- 语义是 at-least-once;唯一键去重表、状态机/CAS。
- 先比生产/消费速率,再看单条耗时;扩消费者/扩分区/批量/降级/转存/优化下游。
- 顺序写、页缓存、零拷贝、批量压缩(+ 分区并行)。
9. 下一篇
S25-J12 · 分布式与微服务题库 —— 高可用的度量与手段、分布式锁的高可用、分布式事务四方案对比、服务隔离与熔断的阈值设计。
本篇属于系列《S25 · 求职与面试》。旧文来源:A-3489/3490/3491/3492/3493/3496、A-3570;扩展点 E-S25-034/035/036。技术细节以 duplicate_ref 指回 S17 主讲系列。