vault backup: 2026-08-09 19:06:40

This commit is contained in:
2026-08-09 19:06:40 +08:00
parent e2975eb86b
commit 9d664545c9
46 changed files with 7287 additions and 0 deletions
@@ -0,0 +1,153 @@
---
tags: [test/review, mq, rabbitmq, manual-ack, dead-letter-exchange, dlq, retry-pattern, prefetch]
create time: 2026-08-09 12:00
---
# ACK 确认与死信队列_测试题
## 概述
本测试覆盖 RabbitMQ 消费者 ACK 确认机制(Auto vs Manual)、unacked 消息重放、死信交换(DLX)以及 TTL + DLX 重试队列模式。共 10 道题:6 道选择题、3 道填空题、1 道简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
AMQP 协议中 Auto ACK 的本质隐患是什么?
A. Auto ACK 的性能比 Manual ACK 低很多
B. Basic.ConsumeOk 本身就是"投递动作",Auto ACK 把投递等同于处理成功,消费者崩溃后未处理消息丢失
C. Auto ACK 不支持多消费者负载均衡
D. Auto ACK 需要在消费者代码中手动调用确认方法
### Q2(基础)— 考察行为判断
当消费者处理完业务逻辑后应发送哪种 ACK 命令来永久标记消息为已消费并从队列移除?
A. basic.nack(requeue=true)
B. basic.ack
C. basic.reject
D. basic.get
### Q3(进阶)— 考察核心原理
Dead Letter Exchange(DLX)在什么情况下会捕获并重新路由消息?
A. 消费者主动调用 basic.ack 后
B. 消息 TTL 到期、被 nack(requeue=false)、或队列达到最大长度时
C. 消费者调用 basic.get 拉取消息后
D. 消息被发送到 Fanout Exchange 后
### Q4(进阶)— 考察对比辨析
以下哪种 nack 策略会一次性拒绝当前通道中所有 unacked 消息?
A. nack(tag, multiple=false, requeue=false)
B. nack(tag, multiple=true, ...)
C. ack(tag, multiple=true)
D. reject(tag, requeue=true)
### Q5(深入)— 考察场景推理
某电商系统使用三层重试队列 + DLX 方案处理第三方支付回调。第一层重试间隔 5 秒、第二层 30 秒、第三层 2 分钟。三次重试均失败后消息进入死信队列。如果希望在死信消息进入人工处理队列的同时也触发告警通知运维人员,应该如何设计?
A. 死信队列直接连接到运维人员的邮件系统
B. 死信队列配置 x-dead-letter-exchange 指向一个 monitoring-exchange,用于告警通知
C. 在消费者代码中检测死信数量然后发送邮件
D. 使用 Monitor API 轮询死信队列长度
### Q6(深入)— 考察源码级别细节
在指数退避重试的 Go 代码示例中,当处理失败时,消息被重新发布到重试 Exchange 并对原消息发送 ACK。这种设计的目的是什么?
A. 让 broker 自动决定下次投递时间
B. 避免 nack 导致的重复拉取,同时让下次投递有时间间隔
C. 将重试计数器重置为 0
D. 跳过当前消息直接处理下一条
---
## 二、填空题(3道)
### F1 — 填空
DLX 中的 `x-dead-letter-routing-key` 属性可以指定死信消息转发到 DLX 时使用的 routing key。如果未显式指定该属性,默认使用消息原来进入该队列时的 ______ 。
> **提示**: DLX 本质上是重新发布消息到指定的交换器。
### F2 — 填空
在三层重试队列模式中,每条消息只需要携带一个简单的 ______ header 而不需要维护计数器。当 TTL 到期后消息自然流入下一层队列。
> **提示**: 这个 header 控制了消息在队列中的等待时间。
### F3 — 填空
实践中常见的坑:如果消费者处理耗时过长(比如几分钟),unacked 的消息会持续占用 ______ 配额,导致其他消费者无法获得新消息。
> **提示**: 这与 QoS 预取参数相关。
---
## 三、简答题(1道)
### S1
你正在设计一个重要的订单状态更新系统,第三方平台会异步推送订单状态变更到你们的 MQ。系统具有以下特征:
- 消息处理可能有各种失败类型(网络超时、下游服务不可用、数据校验失败)
- 某些失败是暂时的(几秒后可恢复),有些是永久的(数据有问题需要人工处理)
- 消息处理必须在数据库事务完成后才确认消费
- 高峰期消费者处理速度可能跟不上生产者速度
请设计一个综合的消息处理流水线,回答以下问题:
1. Auto ACK 还是 Manual ACK?为什么?
2. nack 的 requeue 策略如何选择(临时故障 vs 不可恢复错误)?
3. 重试队列层级如何设计?
4. 如何处理背压(消费者速度 < 生产者速度)?
> **答题框架提示**:
1. ACK 模式选择及理由
2. 不同失败类型的处理策略
3. DLX + TTL 重试设计
4. prefetch 和队列长度控制
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | AMQP 协议层面,Basic.GetOk / Basic.ConsumeOk 本身就是"投递动作"。Auto ACK 把"投递"等同于"处理成功"。如果消费者在收到消息后尚未执行业务逻辑就宕机了,这条消息已经被认为消费完了——这就是隐患。 |
| Q2 | B | basic.ack 永久标记为已消费并从队列中移除。basic.nack(requeue=false) 走 DLX;basic.nack(requeue=true) 放回队列头;basic.reject 类似 nack 但不支持 multiple 参数。 |
| Q3 | B | DLX 在三种情况下捕获消息:basic.nack + requeue=false(被拒绝且不重入队)、TTL 到期(x-message-ttl 过期)、队列达到最大长度(x-max-length 溢出)。这些都是消息不再适合留在原队列的情况。 |
| Q4 | B | nack(tag, multiple=true, ...) 一次性拒绝当前通道中所有 unacked 消息。谨慎使用,可能造成大面积消息堆积。通常只用于通道异常关闭等特殊情况。 |
| Q5 | B | 在死信队列的配置中添加 x-dead-letter-exchange 指向 monitoring-exchange,这样最终进死信的消息会自动流转到告警队列。这利用了 DLX 的链式转发能力,是最优雅的解耦方案。A/C/D 都增加了额外的耦合或延迟。 |
| Q6 | B | 先 publish 到重试 Exchange 再 ack 原消息,这样既避免了 nack 导致的同一消息立即被同一消费者重新拉取(无限循环),又通过指数退避 delay header 让下次投递有时间间隔。这是比单纯 nack(requeue=true) 更精细的控制。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | 原始 routing key | 如果没有显式指定 x-dead-letter-routing-key,DLX 会使用消息原先进入该队列时的 routing key 进行转发。这样可以保持消息的路由意图不变,简化配置。 |
| F2 | TTL(x-message-ttl) | N 层队列模式的优势在于每条消息只需携带一个简单的 TTL header 而不需要维护计数器。消息在每层队列等待对应时长后自然流入下一层,形成递增的时间链。 |
| F3 | prefetch | unacked 消息会持续占用 prefetch 配额(即 Qos 预取的槽位),导致其他消费者无法获得新消息。解决方案:适当放大 prefetch 数值、或在处理过程中定期发送 heartbeat ack。 |
### 简答题参考答案
S1:**参考答案要点**:
1. 选择 Manual ACK。原因:消息需要在数据库事务提交后才确认消费,Auto ACK 在消费者 crash 时会导致未处理消息丢失。Manual ACK 把控制权交给消费者,可以在事务提交后才发送 ack。
2. 临时故障(网络超时、下游短暂不可用)→ nack(tag, false, true) 重回队列头部或直接 re-publish 到重试 Exchange;不可恢复错误(数据校验失败、业务逻辑错误)→ nack(tag, false, false) 走 DLX 进入人工处理流程。
3. 重试设计:使用多层 TTL 队列接力。retry-queue-1(TTL 5s,DLX→retry-queue-2)→ retry-queue-2(TTL 30s,DLX→retry-queue-3)→ retry-queue-3(TTL 120s,DLX→dead-letter-queue)。也可在消息 header 中嵌入 retryCount 字段配合指数退避。
4. 背压处理:设置合理的 prefetch_count(通用场景 10~50),启用队列最大长度限制(x-max-length)防止无限堆积。当队列满时新消息被拒并走 DLX。监控 unacked 数量和 queue depth 发现趋势性增长立即扩容消费者。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖 ACK 选择、nack 策略、重试设计和背压处理四个维度。
## 关联笔记
- [[04.MQ/rabbitmq/消息持久化与可靠性投递]]
- [[04.MQ/rabbitmq/Exchange 路由机制]]
- [[04.MQ/rabbitmq/推拉结合消费模式]]
@@ -0,0 +1,153 @@
---
tags: [test/review, mq, rabbitmq, exchange-routing, direct-exchange, fanout-exchange, topic-exchange, headers-exchange, delay-exchange]
create time: 2026-08-09 12:00
---
# Exchange 路由机制_测试题
## 概述
本测试覆盖 RabbitMQ 四种内置 Exchange 类型(Direct、Fanout、Topic、Headers)以及延迟交换插件和死信交换机的特性。共 10 道题:6 道选择题、3 道填空题、1 道简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
以下哪种 Exchange 类型完全忽略 routing key,将消息投递到所有绑定到该 Exchange 的 Queue?
A. Direct Exchange
B. Topic Exchange
C. Fanout Exchange
D. Headers Exchange
### Q2(基础)— 考察行为判断
在 Topic Exchange 中,binding key 为 `*.error.*`,以下哪个 routing key 能够匹配?
A. `order.create`
B. `system.error.disk.full`
C. `app.error`
D. `error.log`
### Q3(进阶)— 考察核心原理
关于 Topic Exchange 的通配符规则,以下哪项描述是正确的?
A. `*` 匹配零个或多个词,`#` 匹配恰好一个词
B. `*` 和 `#` 都匹配任意数量的词
C. `*` 匹配恰好一个词(以点号分隔),`#` 匹配零个或多个词
D. `*` 只能用于 binding key 的前缀位置
### Q4(进阶)— 考察对比辨析
以下哪种 Exchange 类型的性能最差且官方明确标注"不推荐在生产中使用"?
A. Direct Exchange
B. Fanout Exchange
C. Topic Exchange
D. Headers Exchange
### Q5(深入)— 考察场景推理
某系统需要实现"超时未支付自动取消订单"的功能,要求在订单创建后精确等待指定时长(如 30 分钟)再发送取消消息。以下哪种方案最合适?
A. 使用 Direct Exchange + 定时任务轮询检查
B. 安装 x-delayed-message 插件,声明延迟 Exchange 并在消息头设置 x-delay
C. 使用 Redis ZSet 定时弹出功能
D. 在消费者端 sleep 30 分钟后再处理
### Q6(深入)— 考察源码级别细节
在使用 x-delayed-message 插件时,声明延迟 Exchange 的一个必要参数是 `x-delayed-type`,它的作用是什么?
A. 指定 Exchange 的名称
B. 指定延迟到期后内部转发消息所使用的 Exchange 类型
C. 指定消息的最大延迟时间
D. 指定 Exchange 是否持久化
---
## 二、填空题(3道)
### F1 — 填空
Direct Exchange 的路由规则是完全匹配:routing key 必须与 Queue 绑定到 Exchange 时的 binding key ______ ,消息才会被投递。
> **提示**: 一个字概括这个关系。
### F2 — 填空
Topic Exchange 中,binding key 为 `#` 时可以匹配 ______ 路由(即所有消息)。
> **提示**: 井号匹配的语义是什么?
### F3 — 填空
DLX(Dead Letter Exchange)本质上也是一个普通 Exchange,只是通过队列属性间接触发。当队列中的消息 TTL 过期或被 nack 且不重新入队或队列长度限制已满时,会被重新路由到 DLX。DLX 可以配置自己的 binding key(即 x-dead-letter-______)来控制死信消息的去向。
> **提示**: 这是 DLX 路由时使用的 key 属性名。
---
## 三、简答题(1道)
### S1
你正在为一个大型电商平台设计消息路由系统,有以下业务需求:
1. 用户下单成功后,需要通知库存系统扣减库存、物流系统生成运单、优惠券系统退还优惠券
2. 这些下游服务可能随时增减,不能硬编码 routing key
3. 日志系统需要接收所有级别的日志消息(info/warn/error),但运维人员只想订阅 error 和 warn
4. 部分非关键通知(如用户注册邮件)可以在 10 分钟后发送
5. 失败的消息需要有归档通道
请设计一套 Exchange 和 routing key 的命名规范及架构方案,回答以下问题:
1. 每种业务场景选择什么 Exchange 类型?
2. routing key 的命名规范建议是什么?
3. 如何满足"服务动态增减"的需求?
> **答题框架提示**:
1. 逐一对应需求分析最优 Exchange 选型
2. 提出层级化的 routing key 命名策略
3. 说明如何通过 Topic Exchange 的通配符能力实现解耦
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | C | Fanout Exchange 忽略 routing key,将消息广播投递到所有绑定到该 Exchange 的 Queue。典型场景:事件广播、配置刷新、缓存失效通知。 |
| Q2 | B | `*.error.*` 通配符匹配规则:`*` 匹配恰好一个词。system.error.disk.full 有三段中间一段是 error,符合 *.error.*(第一段任何、第二段 error、第三段任何)。A 只有一段;C 只有两段缺第三段;D 结构完全不对。 |
| Q3 | C | `*`(星号)匹配恰好一个词(以点号分隔),`#`(井号)匹配零个或多个词。例如 order.create 匹配 # 也匹配 *.create,但不匹配 *.pay.*。这是面试常考点。 |
| Q4 | D | Headers Exchange 通过检查消息的 headers 属性(key-value 对)来决定路由,性能较差且灵活性不如 Topic Exchange,官方文档明确标注"不推荐在生产中使用"。应优先选择 Topic Exchange。 |
| Q5 | B | x-delayed-message 插件是最直接可靠的延迟消息方案。RabbitMQ 原生不支持延迟消息,常见替代方案有定时任务轮询(复杂)、Redis ZSet(增加外部依赖)等,但在高可靠性场景中都不如插件方案简洁可靠。 |
| Q6 | B | `x-delayed-type` 指定延迟到期后内部转发消息所使用的 Exchange 类型(通常为 direct)。这是声明延迟 Exchange 的必要参数,告诉插件消息延期后应该用什么路由规则进行转发。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | 一致(或相同) | Direct Exchange 使用完全匹配原则:routing key 必须与 binding key 完全一致。支持一对一或一对多投递(多个 Queue 绑定了相同的 binding key)。这是最常用也最可预测的路由方式。 |
| F2 | 全部(或所有) | `#` 匹配零个或多个词,因此 `#` 作为 binding key 可以匹配所有的 routing key。在 Topic Exchange 中常用于"全量订阅"的场景。 |
| F3 | routing-key | DLX 可以配置 x-dead-letter-routing-key 属性来控制死信消息转发到 DLX 时使用的 routing key。如果未显式指定则默认使用消息原先进入队列时的 routing key。 |
### 简答题参考答案
S1:**参考答案要点**:
1. 下单通知:使用 Topic Exchange。routing key 格式 `order.{event_type}`(如 order.create、order.pay)。库存、物流、优惠券分别 binding 不同的 key 前缀。这样新增下游服务只需声明 binding 即可,无需修改生产者代码——完美支持动态增减。
2. 日志分级收集:同样使用 Topic Exchange,routing key 格式 `{service}.log.{level}`(如 user-service.log.error)。运维绑定 binding key `*.log.error` 和 `*.log.warn`,其他组件绑定更具体的 level。
3. 延迟通知:安装 x-delayed-message 插件,声明一个 delayed.exchange(x-delayed-message 类型),所有非关键通知统一发到这里并设置 x-delay header(单位毫秒)。
4. 死信归档:每个关键业务的 Queue 都配置 x-dead-letter-exchange 指向统一的 dead-letter-exchange(Direct 类型),死信 routing key 格式 `{original_queue}.dead`,方便定位和归档。
5. routing key 命名规范建议采用层级化命名:`{domain}.{entity}.{action}` 如 `order.create.success`、`user.register.email`。Top-level 表示业务域,便于按域分组路由。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖 Exchange 选型、命名规范、动态扩展方案和死信设计四个维度。
## 关联笔记
- [[04.MQ/rabbitmq/消息持久化与可靠性投递]]
- [[04.MQ/rabbitmq/ACK 确认与死信队列]]
- [[04.MQ/rabbitmq/推拉结合消费模式]]
@@ -0,0 +1,153 @@
---
tags: [test/review, mq, rabbitmq, pull-model, qos, prefetch, backpressure, consumer-balance]
create time: 2026-08-09 12:00
---
# 推拉结合消费模式_测试题
## 概述
本测试覆盖 RabbitMQ 消费模型的本质(Push vs Pull 混合理解)、QoS 预取策略、背压处理和消费者负载均衡。共 10 道题:6 道选择题、3 道填空题、1 道简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
RabbitMQ Consumer API 名为 `basic.consume`(拉取)但实际运行模型是 Server Push(服务端推送)。以下说法正确的是:
A. `basic.consume` 和 `basic.get` 的行为完全相同
B. `basic.consume` 建立订阅后 Broker 持续主动投递,`basic.get` 每次调用阻塞获取单条消息
C. `basic.consume` 只能获取一次就断开连接
D. `basic.get` 是默认的生产者发送方法
### Q2(基础)— 考察行为判断
设置 `prefetch_count = 1` 可以实现公平分发(Fair Dispatch),其核心原因是:
A. Broker 会随机选择消费者分配消息
B. A 每处理完一条并发 ack 后才收到下一条,自然形成速度匹配
C. prefetch=1 时所有消息排队等候直到第一个消费者空闲
D. prefetch=1 强制只有一个消费者活跃
### Q3(进阶)— 考察核心原理
`global=true` 是 Prefetch 设置中的一个遗留参数,以下哪一项描述了在多 Channel 场景下使用 global=true 可能导致的意外行为?
A. prefetch 只对单个 Channel 生效,不会跨 Channel 共享配额
B. prefetch 对整个连接(而非单个 Channel)生效,导致一个 Channel 消耗了所有配额时其他 Channel 无法获得新消息
C. global=true 会使 prefetch_count 失效
D. global=true 只在 Fanout Exchange 中有效
### Q4(进阶)— 考察对比辨析
以下哪种预取值范围最适合通用生产环境?
A. prefetch=1
B. prefetch=0
C. prefetch=10~50
D. prefetch=10000+
### Q5(深入)— 考察场景推理
一个日终跑批系统需要从数据库全量读取商品信息并通过 MQ 推送到缓存集群预热。数据量大但每条消息处理简单。同时另一个即时订单状态推送系统要求低延迟。这两个场景分别适合的 prefetch 值是:
A. 两者都用 prefetch=1
B. 跑批用 prefetch=200,订单推送用 prefetch=3
C. 跑批用 prefetch=3,订单推送用 prefetch=200
D. 两者都用 prefetch=0(无限)
### Q6(深入)— 考察源码级别细节
在 `basic.consume` 建立的流控窗口机制中,Window 耗尽时 Broker 会发生什么?
A. Broker 立即关闭 TCP 连接
B. Broker 暂停向该 Channel 投递直到窗口回收(ack 释放槽位)
C. Broker 将多余消息转移到其他 Channel
D. Broker 丢弃超出的消息
---
## 二、填空题(3道)
### F1 — 填空
设 prefetch = N 时,Broker 可以在没有收到 ack 的情况下最多向该 Channel 发送 ______ 条消息累积在未确认状态。当 ack 一条消息后 unacked 数减一,Broker 再补发一条。
> **提示**: 这就是 prefetch 值本身的含义。
### F2 — 填空
同一个 Queue 可以有多个消费者构成 Consumer Group,RabbitMQ 以 ______ 方式将消息均匀分配给各消费者。但在 prefetch=1 时可能出现只有一个消费者活跃而另一个空等的情况。
> **提示**: 这种分发方式的英文术语是什么?
### F3 — 填空
根据调优黄金法则:先设 prefetch=1 观察各消费者的处理耗时分布,再根据 p99 耗时最高的那个消费者来估算合适的 batch size,初始设为预估值的 ______ 倍即可。
> **提示**: 回顾文档中的调优经验。
---
## 三、简答题(1道)
### S1
你正在为一家在线教育公司设计课程点播系统的消息处理 pipeline。系统有以下特征:
- 用户观看视频后会触发埋点事件(播放进度、点赞、评论),每秒约 5000 条
- 埋点数据处理流程:去重 → 聚合 → 写入时序数据库
- 去重操作涉及 Redis 分布式锁,耗时约 50ms
- 聚合操作相对较快(约 5ms)
- 写入时序数据库是 IO 密集型操作(约 20ms)
- 高峰期总处理耗时约 75ms/条消息
- 当前部署了 5 个消费者实例
请设计 QoS 配置方案并回答:
1. 整体 prefetch 值应该设置多少?为什么?
2. 是否需要对不同阶段的处理进行拆分(多队列串联)?
3. prefetch 过大或过小各自会带来什么问题?
> **答题框架提示**:
1. 基于单条消息处理耗时计算合适的 prefetch
2. 考虑是否需要将耗时长的阶段拆出独立队列
3. 大 prefetch 和小 prefetch 的权衡分析
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | `basic.consume` 建立订阅后 Broker 持续主动投递(Push),适用于大多数场景。`basic.get` 每次调用阻塞获取单条消息(Pull),适用于管理界面和健康检查等非实时场景。API 名为 consume 但实质是 Push,源于 AMQP 协议早期约定。 |
| Q2 | B | prefetch=1 实现 Fair Dispatch 的核心机制:A 每处理完一条、发一个 ack,才会收到下一条,自然形成速度匹配。如果 prefetch=N(大数值),Broker 可能在短时间内把 N 条消息全发给 A 而 B 空闲等待。 |
| Q3 | B | global=true 是对整个连接(而非单个 Channel)生效,在多 Channel 场景下会导致意外行为。现代客户端中一律设为 false,作用于单个 Channel。这是遗留参数,不建议使用。 |
| Q4 | C | prefetch=10~50 是通用生产环境默认值。prefetch=1 吞吐量较低适合处理耗时差异大的场景;prefetch=100~500 吞吐最高但内存压力大;prefetch=0(不设置)为无限值严禁在生产中使用。 |
| Q5 | B | 跑批场景:数据量大但处理简单,适合高吞吐配置 prefetch=200 配合批量更新 Redis pipeline。订单推送:latency 优先级高于 throughput,设置 prefetch=3 让消费者尽快处理完再拿下一条。 |
| Q6 | B | 流控窗口耗尽时,Broker 暂停向该 Channel 投递直到窗口回收。这是内建的背压机制——消费者发送 basic.ack 后窗口恢复,或者 window refill 后继续投递。不会丢消息也不会断连。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | N | prefetch_count 定义了单个 Channel 上允许的最大未确认消息数。这 N 条消息累积在 unacked 状态,ack 一条后 Broker 再补发一条。prefetch 越大意味着降低网络 RTT 开销但也增加了内存压力和崩溃损失量。 |
| F2 | Round-Robin(轮询) | RabbitMQ 以轮询方式将消息平均分配给同一 Queue 的各消费者。但 Round-Robin 不一定是公平的——处理快的消费者实际上承担更多工作量,这正是 prefetch=1 要解决的问题。 |
| F3 | 2~3 | 调优黄金法则:先设 prefetch=1 观察各消费者的处理耗时分布,再根据 p99 耗时最高的消费者估算合适的 batch size,初始设为预估值的 2~3 倍。之后通过监控 unacked 数量和 queue depth 微调。 |
### 简答题参考答案
S1:**参考答案要点**:
1. 单条消息总处理耗时约 75ms,5 个消费者实例理论上可支撑 5 / 75ms ≈ 66 QPS。但实际需求为 5000 QPS,远超出单节点处理能力。prefetch 建议设置为 10~20:太小无法满足吞吐需求,太大会导致消息积压在消费者内存中。初始可按 prefetched messages ≤ 处理耗时 × prefetch_count 来估算。
2. 需要拆分!去重步骤涉及 Redis 分布式锁是串行瓶颈(50ms),应将去重作为独立的第一层消费者队列(快速筛选有效请求),聚合和写库放到第二层队列做批处理。这样可以大幅缓解单机瓶颈。
3. prefetch 过大的问题:Broker 可以积压更多消息到消费者内存,降低每次投递的往返开销但也增加了内存压力、崩溃恢复时的损失量以及处理时间过长导致 unacked 消息占满 prefetch 配额的风险。prefetch 过小的问题:串行节奏限制吞吐量,网络 RTT 开销占比增大。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖 prefetch 配置、队列拆分策略和大/小 prefetch 权衡三个维度。
## 关联笔记
- [[04.MQ/rabbitmq/消息持久化与可靠性投递]]
- [[04.MQ/rabbitmq/ACK 确认与死信队列]]
- [[04.MQ/rabbitmq/Exchange 路由机制]]
@@ -0,0 +1,163 @@
---
tags: [test/review, mq, rabbitmq, publisher-confirm, message-persistence, rabbitmq-transaction, return-callback]
create time: 2026-08-09 12:00
---
# 消息持久化与可靠性投递_测试题
## 概述
本测试覆盖 RabbitMQ 消息从生产者到消费者的完整链路中保证可靠性的三大支柱:Exchange/Queue 持久化、消息 persistent 标记和 Publisher Confirm 机制。共 10 道题:6 道选择题、3 道填空题、1 道简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
RabbitMQ 消息持久化的三个环节依次是:
A. 发送端加密 → 网络传输加密 → 存储端加密
B. Exchange durable 声明 → Queue durable 声明 → 消息 DeliveryMode=2
C. 消息签名 → Exchange 验证 → Queue 存储
D. 生产者确认 → Broker fsync → 消费者 ACK
### Q2(基础)— 考察行为判断
仅设置 Queue durable=true 但消息的 DeliveryMode=1(transient),服务器重启后这些消息会怎样?
A. 消息会保留在磁盘中因为队列本身是持久的
B. 消息会被丢弃因为瞬态消息不会写入磁盘
C. 消息会被转移到另一个临时队列
D. 消息会以压缩格式保存在内存中
### Q3(进阶)— 考察核心原理
Publisher Confirm 批量确认模式相比单条确认模式的主要优势是什么?
A. 能保证"恰好一次"语义而单条不能
B. 吞吐量接近未开启 Confirm 的水平,性能差距数倍甚至十倍以上
C. 不需要建立 Channel 连接即可工作
D. 支持事务回滚语义
### Q4(进阶)— 考察对比辨析
关于 RabbitMQ 的 Confirm vs Transaction 两种可靠投递方式,以下哪项对比是错误的?
A. Confirm 采用异步批量确认,Transaction 采用同步提交
B. Transaction 的资源开销更高,需要维护完整的事务日志和回滚状态
C. Confirm 能保证恰好一次语义,Transaction 只能做到最多一次
D. 生产环境中几乎不推荐使用 Transaction 模式
### Q5(深入)— 考察场景推理
一个订单支付推送系统使用 RabbitMQ 将支付结果通知给物流、库存等多个下游系统。在生产者配置中,如果 exchange 存在但没有绑定的队列能匹配该 routing key,且消息设置了 mandatory=false,会发生什么?
A. ReturnCallback 被触发,消息退回给生产者
B. 消息被 Broker 静默丢弃,没有任何通知
C. ConfirmCallback 收到 nack 通知
D. 消息自动路由到死信队列
### Q6(深入)— 考察源码级别细节
在 Go 中使用 amqp.go 库实现 Confirm + Return 回调时,以下代码片段的作用是什么?
```go
confirms := ch.NotifyPublish(nil)
for conf := range confirms {
if conf.Ack {
log.Printf("消息 delivery-tag=%d 已确认", conf.DeliveryTag)
} else {
log.Printf("消息 delivery-tag=%d 未被确认", conf.DeliveryTag)
}
}
```
A. 注册 ReturnCallback 处理路由失败的消息
B. 注册 ConfirmCallback 通道,逐条接收 Broker 对每条消息的处理结果
C. 向所有生产者广播消息
D. 监控 Broker 的健康状态
---
## 二、填空题(3道)
### F1 — 填空
RabbitMQ 消息的 DeliveryMode 为 ______ 时表示持久化消息,只有设置为这个值的消息在抵达 Queue 后才会刷盘到磁盘。
> **提示**: 取值为 1 或 2。
### F2 — 填空
只有同时开启 mandatory=true,ReturnCallback 才会在消息因无法路由而被退回时被触发。ConfirmCallback 告诉生产者消息是否被 Broker 安全接收(路由完成),而 ReturnCallback 处理强制性消息无法路由的情况——它区分了"Exchange 不存在"和"Exchange 存在但无绑定队列"这两种情况。
> **提示**: 思考 mandatory 参数在这个回调中的作用。
### F3 — 填空
如果需要"恰好一次"语义,正确做法是:______ 作为投递保障加上业务层幂等键(如 orderId 唯一索引)兜底。
> **提示**: 不是 Transaction,而是另一种更轻量的机制。
---
## 三、简答题(1道)
### S1
你正在为一个金融转账系统设计基于 RabbitMQ 的消息通知服务。转账成功后需要将交易记录通过 MQ 通知风控系统和审计系统。该系统有以下要求:
- 绝对不能丢失任何一条交易通知
- 下游系统具备幂等处理能力(可以安全重复消费)
- 高吞吐需求(峰值 QPS 5000+)
请设计一个完整的可靠性投递方案,回答以下问题:
1. Exchange、Queue 和消息的持久化如何配置?
2. 选择 Confirm 还是 Transaction?为什么?
3. Nack 的消息如何处理?
4. 如何配合业务幂等性设计确保"恰好一次"的效果?
> **答题框架提示**:
1. 三阶段持久化的具体配置
2. Confirm 模式的选择理由及批量策略
3. nack 的重试和补偿机制
4. 业务层幂等键的设计思路
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | 三个环节:Exchange 声明 durable=true、Queue 声明 durable=true、消息 DeliveryMode=2。三者缺一不可,任何一个环节未持久化都导致丢消息风险。A 讲的是加密而非持久化;C/D 涉及其他机制。 |
| Q2 | B | 常见误解:仅设置 Queue durable 并不能保证消息不丢。如果消息 DeliveryMode=1(transient),即使队列本身是持久的,这些瞬态消息在服务器重启时也会被丢弃。持久化是队列元数据和消息内容两回事。 |
| Q3 | B | 批量 Confirm 一次发送多条消息一次性收到确认回调,吞吐量接近未开启 Confirm 的水平。单条确认串行等待 ack/nack 吞吐量极低。实际性能差距可达数倍甚至十倍以上。 |
| Q4 | C | 反了!无论是 Confirm 还是 Transaction 都不够保证恰好一次。Confirm 最多一次(需重试才能至少一次),Transaction 配合回滚可做到更接近恰好一次但仍需业务幂等兜底。这是面试高频考点。 |
| Q5 | B | mandatory=false 时,如果 Exchange 存在但没有匹配的 Queue,消息被 Broker 静默丢弃,不会触发任何回调。只有 mandatory=true 时才会触发 ReturnCallback 退回消息。A 错误——mandatory=false 不会触发 Return。 |
| Q6 | B | NotifyPublish 注册一个 Confirmation 通道,每个发出的消息对应一个 Confirmation 对象,其 Ack 字段为 true 表示 Broker 已成功处理(路由完成),false 则说明未被确认(如 Exchange 不存在)。这是 Confirm 的核心结构。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | 2 | DeliveryMode=2 表示 persistent(持久化),DeliveryMode=1 表示 transient(瞬态)。持久化消息在抵达 Queue 后会刷盘而非仅仅留在内存 buffer 中。但这会带来明显的性能代价(大约 10 倍吞吐下降)。 |
| F2 | mandatory | 只有同时开启 mandatory=true,ReturnCallback 才会在路由失败时被触发。mandatory 控制的是"消息是否允许被丢弃"——true 表示不允许丢弃,必须退回或报错;false 表示允许 Broker 静默丢弃无法路由的消息。 |
| F3 | Confirm | 面试回答要点:如果需要"恰好一次"语义不要指望 RabbitMQ 本身保证——无论 Confirm 还是 Transaction 都不够。正确做法是 Confirm 作为投递保障 + 业务层幂等键(如 orderId 唯一索引)兜底。这才是工业级方案的思路。 |
### 简答题参考答案
S1:**参考答案要点**:
1. Exchange 和 Queue 均声明 durable=true 防止服务重启丢数据;消息设置 DeliveryMode=2(persistent)确保磁盘持久化。
2. 选择 Publisher Confirm 而非 Transaction。原因:Confirm 异步批量确认吞吐量大(峰值 QPS 5000+ 完全够用),资源占用低,主流客户端支持成熟。Transaction 同步阻塞吞吐不够且资源开销大。
3. Nack 的消息放入本地重试表(数据库),定时任务扫描并重试。超过最大重试次数后进入死信队列人工介入。
4. 业务幂等设计:每条交易通知携带唯一的 orderId + 目标系统标识作为幂等键。下游系统在数据库中建立联合唯一索引(order_id, target_system)。重复收到的消息因违反唯一约束而被拒绝,不会产生副作用。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖三阶段持久化、Confirm 选择理由、nack 处理策略和业务幂等设计四个维度。
## 关联笔记
- [[04.MQ/rabbitmq/ACK 确认与死信队列]]
- [[04.MQ/rabbitmq/Exchange 路由机制]]
- [[04.MQ/rabbitmq/推拉结合消费模式]]