vault backup: 2026-08-08 19:01:04

This commit is contained in:
2026-08-08 19:01:04 +08:00
parent 1a4a83bc67
commit e2975eb86b
46 changed files with 9370 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
---
tags: [mq/rabbitmq, manual-ack, dead-letter-exchange, dlq, retry-pattern, prefetch]
create time: 2026-08-08 18:52
update time: 2026-08-08 18:52
---
# ACK 确认与死信队列
## 概述
Consumer Acknowledgment(ACK)确认机制是 RabbitMQ 消费者可靠性保障的最后防线。Auto ACK 虽然高效但一旦消费者崩溃就会丢消息;Manual ACK 提供了精细的控制能力但增加了复杂度。配合死信队列(DLQ)和重试模式,可以构建出完整的异常消息处理流水线。
## 核心原理
### Auto ACK vs Manual ACK
RabbitMQ 的消费确认模式分为两类:
| 特性 | Auto ACK | Manual ACK |
|------|----------|-----------|
| 确认时机 | 消息交付即确认 | 消费者显式调用 basic.ack |
| 安全性 | 低(消费者 crash 会丢未处理消息) | 高(处理完才确认) |
| 吞吐量 | 略高 | 略低(额外的网络往返) |
| 适用场景 | 无状态处理、容忍少量丢失 | 持久化处理、金融类业务 |
Auto ACK 的隐患在于:AMQP 协议层面,Basic.GetOk / Basic.ConsumeOk 本身就是"投递动作",而 Auto ACK 把"投递"等同于"处理成功"。如果消费者在收到消息后尚未执行业务逻辑就宕机了,这条消息已经被认为被消费完了。
Manual ACK 的正确使用方式是在业务逻辑执行完毕后发送 `basic.ack`,如果出现异常则发送 `basic.nack` 并指定 requeue 策略。
```go
// Go 代码示例:Manual ACK 基本模式
err := ch.Qos(1, 0, false) // 预取 1 条,公平分发
msgs, err := ch.Consume("task.queue", "", false, // autoAck=false
false, false, false, nil)
for d := range msgs {
result, err := processJob(d.Body)
if err != nil {
// 业务异常:拒绝并路由到死信
ch.Nack(d.DeliveryTag, false, false) // requeue=false
continue
}
ch.Ack(d.DeliveryTag, false) // 处理完成
}
```
### unacked 消息的重放机制
当消费者未发送 ACK 时,消息处于 unacked 状态,存储在 Broker 内存中。此时有三种处置方式:
1. **basic.ack**:永久标记为已消费,从队列中移除
2. **basic.nack(requeue=true)**:放回队列头部,可能被同一消费者再次拉取
3. **basic.nack(requeue=false)**:根据队列的 DLX 配置路由到死信队列
> [!TIP]
> 面试高频追问:为什么手动 ACK 会比 Auto ACK 更安全但又可能更慢?答案在于 trade-off——手动 ACK 把控制权交给了消费者,你可以在数据库事务提交后才 ACK,这样即使消费者崩溃,未提交事务对应的消息也不会丢。代价是多了一次 RPC 调用(ack 请求到 Broker)。
### Dead Letter Exchange(DLX)
DLX 不是独立的消息队列,而是一种通过队列属性触发的路由行为。当队列配置了 `x-dead-letter-exchange`,满足以下条件的消息会被自动重新发布到指定的 DLX:
- 被 `basic.nack` 且 `requeue=false`
- 消息 TTL 到期(`x-message-ttl` 或单消息 TTL)
- 队列达到最大长度(`x-max-length`)
DLX 本身是一个普通的 Exchange,可以配置自己的 binding key(`x-dead-letter-routing-key`)来控制死信消息的去向。
```mermaid
sequenceDiagram
participant P as 生产者
participant REX as retry-exchange(Direct)
participant RQ as retry-queue<br/>(TTL+DLX)
participant DX as dead-letter-exchange
participant DQ as dead-letter-queue
participant C as 消费者
P->>REX: publish(order.id=123)
REX->>RQ: route to retry-queue<br/>(TTL=30s)
RQ->>RQ: 消息等待 TTL 过期
Note right of RQ: unacked / 被 nack(requeue=false) / TTL到期
RQ->>DX: DLX 捕获消息
DX->>DQ: 投递到死信队列
DQ->>C: consumer 消费死信
```
### TTL + DLX 实现重试队列模式
重试是 DLX 最经典的应用场景。通过多层队列接力,实现"尝试 N 次 → 最终死信"的自动降级流程。
```yaml
# 重试队列层级设计
retry-layer-1:
exchange: retry-exchange
queue: retry-queue-1
arguments:
x-message-ttl: 5000 # 5 秒后重试
x-dead-letter-exchange: retry-exchange
x-dead-letter-routing-key: retry-layer-2 # 第一次重试仍进 retry-exchange
retry-layer-2:
exchange: retry-exchange
queue: retry-queue-2
arguments:
x-message-ttl: 30000 # 30 秒后重试
retry-layer-3:
exchange: retry-exchange
queue: retry-queue-3
arguments:
x-message-ttl: 120000 # 2 分钟后重试
dead-letter-queue:
exchange: dead-letter-exchange
queue: dead-letter-queue
arguments:
x-dead-letter-exchange: monitoring-exchange # 最终告警
```
这种 N 层队列模式的优势在于每条消息只需要携带一个简单的 TTL header,而不需要维护计数器。当队列配置 `x-dead-letter-routing-key` 指向下一层重试队列所在的 Exchange,消息自然形成递增的时间链。
> [!NOTE]
> 如果不想用多层队列,也可以在消息 body 中嵌入一个 `retryCount` 字段,消费者处理失败时增加计数并重新发送到原 Exchange。这种方式更灵活,但需要应用层维护计数逻辑。
### Nack + Requeue 策略
Nack 是 Negative Acknowledgement 的缩写,用于告知 Broker 某条消息不应被继续交给当前消费者。关键的决策树如下:
- `nack(tag, multiple=false, requeue=true)`:单条重放,回到队列头。适用于临时故障(如下游短暂不可用)。
- `nack(tag, multiple=false, requeue=false)`:单条不重放,走 DLX。适用于不可恢复的错误(如数据校验失败)。
- `nack(tag, multiple=true, ...)`:一次性拒绝当前通道中所有 unacked 消息。谨慎使用,可能造成大面积消息堆积。
```go
// Go 代码示例:带指数退避的重试 Nack
const maxRetries = 3
func handleMessage(d amqp.Delivery) {
retries := d.Headers["x-retry-count"].(int32)
if retries >= maxRetries {
ch.Nack(d.DeliveryTag, false, false) // 进入死信
return
}
d.Headers["x-retry-count"] = retries + 1
if err := process(d.Body); err != nil {
backoff := math.Pow(2, float64(retries)) * 1000
d.Headers["x-delay"] = uint64(backoff)
ch.Publish("retry-exchange", "retry", false, false,
amqp.Publishing{
Body: d.Body,
Headers: d.Headers,
})
ch.Ack(d.DeliveryTag, false)
} else {
ch.Ack(d.DeliveryTag, false)
}
}
```
这段代码的关键在于:当处理失败时,先在消息头中递增重试次数,计算指数退避延迟,然后重新发布到重试 Exchange,最后对原消息发送 ACK。这样既避免了 nack 导致的重复拉取,又能让下次投递有时间间隔。
## 实践场景
**电商订单支付回调处理**:第三方支付平台发起异步回调时,可能出现网络抖动或服务短暂的负载过高。采用 Manual ACK + 三层重试队列 + DLX 的方案:
1. 第一层重试间隔 5 秒,适合处理瞬时网络问题
2. 第二层重试间隔 30 秒,给下游留出恢复窗口
3. 第三层重试间隔 2 分钟,覆盖短时服务重启
4. 三次重试均失败后进入死信队列,人工介入或对账修复
这个模式下,`prefetch_count` 通常设为与消费者并发度相当的值,配合 `no_ack=false` 实现精准控制。
> [!TIP]
> 实践中常见的坑:如果消费者处理耗时过长(比如几分钟),unacked 的消息会持续占用 prefetch 配额,导致其他消费者无法获得新消息。解决方案是将 prefetch 适当放大、或者在处理过程中定期发送 heartbeat ack。
## 关联笔记
- [[Exchange 路由机制]]
- [[消息持久化与可靠性投递]]
- [[推拉结合消费模式]]
+170
View File
@@ -0,0 +1,170 @@
---
tags: [mq/rabbitmq, exchange-routing, direct-exchange, fanout-exchange, topic-exchange, headers-exchange, delay-exchange]
create time: 2026-08-08 18:50
update time: 2026-08-08 18:50
---
# Exchange 路由机制
## 概述
Exchange(交换机)是 RabbitMQ 消息路由的核心组件,充当生产者与队列之间的中间层。生产者在发送消息时指定 Exchange 名称和 routing key,RabbitMQ 根据 Exchange 类型和路由规则将消息分发到一个或多个 Queue。掌握四种内置 Exchange 类型以及插件扩展的延迟交换能力,是设计可靠消息系统的基础。
## 核心原理
### 四种内置 Exchange 类型
#### Direct Exchange — 精确匹配
Direct Exchange 使用完全匹配原则:routing key 必须与 Queue 绑定到 Exchange 时的 binding key 完全一致,消息才会被投递。
```mermaid
sequenceDiagram
participant P as 生产者
participant E["Direct Exchange"]
participant Q1["Queue A (key: order.create)"]
participant Q2["Queue B (key: order.* )"]
participant Q3["Queue C (key: *.pay)"]
P->>E: 发送消息<br/>routing key: "order.create"
E->>Q1: 匹配成功,投递
Note over E,Q1: "order.create" == "order.create"
E->>Q2: 匹配失败
Note over E,Q2: "order.create" ne "order.*"
E->>Q3: 匹配失败
Note over E,Q3: "order.create" ne "*.pay"
```
关键特征:一对一或一对多投递(多个 Queue 绑定了相同的 binding key)。这是最常用也最可预测的路由方式。
#### Fanout Exchange — 广播模式
Fanout Exchange 忽略 routing key,将消息投递到所有绑定到该 Exchange 的 Queue。每次收到消息就是群发。
```mermaid
sequenceDiagram
participant P as 生产者
participant E["Fanout Exchange"]
participant Q1["订单队列"]
participant Q2["日志队列"]
participant Q3["通知队列"]
P->>E: 发送消息<br/>routing key: (任意值)
E->>Q1: 广播投递
E->>Q2: 广播投递
E->>Q3: 广播投递
Note right of E: 忽略 routing key<br/>全部投递
```
典型场景:事件广播、配置刷新、缓存失效通知。不需要关心消息内容被哪些消费者处理。
#### Topic Exchange — 通配符匹配
Topic Exchange 是最灵活的路由模式,支持通配符匹配:
- `*`(星号)匹配**恰好一个词**
- `#`(井号)匹配**零个或多个词**
词之间以点号 `.` 分隔。匹配规则示例:
| routing key | binding key | 是否匹配 |
|-------------|------------|---------|
| `quick.orange.fox` | `*.orange.*` | 是 |
| `quick.orange.fox` | `quick.*.fox` | 是 |
| `quick.brown.fox` | `*.orange.*` | 否 |
| `quick.orange.male.rabbit` | `#` | 是(匹配所有) |
| `lazy.orange.cat` | `lazy.#` | 是 |
```mermaid
sequenceDiagram
participant P as 生产者
participant E["Topic Exchange"]
participant Q1["Queue: *.create"]
participant Q2["Queue: *.pay.*"]
participant Q3["Queue: #"]
P->>E: routing key: "order.create"
E->>Q1: 匹配 "*" -> 投递
E->>Q2: "order.pay.*" ne "*.pay.*" -> 不投递
E->>Q3: "#" -> 投递
P->>E: routing key: "order.pay.success"
E->>Q1: "*.create" ne "order.pay.success" -> 不投递
E->>Q2: 匹配 "*.pay.*" -> 投递
E->>Q3: "#" -> 投递
```
#### Headers Exchange — 基于属性匹配
Headers Exchange 忽略 routing key,通过检查消息的 headers 属性(key-value 对)来决定路由。可以设置 `match` 参数:`all`(所有 header 都匹配)或 `any`(任一 header 匹配即可)。
> [!WARNING]
> Headers Exchange 性能较差且灵活性不如 Topic Exchange,官方文档明确标注"不推荐在生产中使用"。应优先选择 Topic Exchange。
### 死信交换机的特殊绑定
Dead Letter Exchange(DLX)本质上也是一个普通 Exchange,只是通过队列属性间接触发。当队列中的消息满足以下条件之一时,会被重新路由到 DLX:
- 消息被 nack 且不重新入队(`requeue=false`)
- 消息 TTL 过期
- 队列长度限制已满
```yaml
# 队列声明时指定 DLX
queue:
name: "order.processing"
arguments:
x-dead-letter-exchange: "dead-letter-exchange"
x-dead-letter-routing-key: "order.dead"
```
### 延迟交换插件(x-delayed-message)
RabbitMQ 本身不提供延迟消息功能,需要安装 `rabbitmq_delayed_message_exchange` 插件后使用自定义 Exchange 类型 `x-delayed-message`。它通过消息的 `x-delay` header 控制投递时机。
```go
// Go 代码示例:声明延迟 Exchange
// (amqp.go 库简化写法)
args := amqp.Table{
"x-delayed-type": "direct", // 内部转发使用的 Exchange 类型
}
ch.ExchangeDeclare(
"delayed.exchange", // name
"x-delayed-message", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // args
args, // 自定义参数
)
```
构建消息时设置 `x-delay` header(单位毫秒):
```go
// Go 代码示例:带延迟头的消息
msg := amqp.Publishing{
Body: []byte(`{"orderId":"123"}`),
Headers: amqp.Table{
"x-delay": uint32(60000), // 延迟 60 秒
},
}
```
> [!TIP]
> 面试常考点:RabbitMQ 原生不支持延迟消息。常见替代方案有定时任务轮询、Redis ZSet 定时弹出、或使用 Kafka 的分区时间排序特性。但在高可靠性场景中,x-delayed-message 插件是最直接可靠的方案。
## 实践场景
| 场景 | 推荐 Exchange | 原因 |
|------|--------------|-----|
| 订单创建通知下游 | Direct | 精确控制消息去向 |
| 全局缓存失效广播 | Fanout | 所有服务实例都需要收到 |
| 日志分级收集(info/error/warn) | Topic | 按级别前缀灵活路由 |
| 支付回调不同金额段分流 | Topic | 按 `amount.linux``amount.high` 等维度拆分 |
| 超时未支付取消订单 | x-delayed-message | 精确控制延迟投递时间 |
| 死信消息归档 | Direct + DLX 属性 | 标准化异常流程处理 |
> [!TIP]
> 秋招面试技巧:当被问到"如何设计一个支持多种消息类型的路由系统"时,优先考虑 Topic Exchange 的通配符能力,而非为每种类型创建独立的 Direct Exchange。Topic Exchange 天然支持层级化路由,运维上更简洁。
## 关联笔记
- [[消息持久化与可靠性投递]]
- [[ACK 确认与死信队列]]
- [[推拉结合消费模式]]
+162
View File
@@ -0,0 +1,162 @@
---
tags: [mq/rabbitmq, pull-model, qos, prefetch, backpressure, consumer-balance]
create time: 2026-08-08 18:53
update time: 2026-08-08 18:53
---
# 推拉结合消费模式
## 概述
RabbitMQ 的 Consumer API 名为 `basic.consume`(拉取),但实际运行模型是 Server Push(服务端推送)。这个"名不副实"的设计源于 AMQP 协议的早期约定,也是理解 RabbitMQ 消费模型的关键起点。深入掌握预取策略(QoS)、背压处理和多消费者负载均衡机制,才能在吞吐量和可靠性之间找到最佳平衡点。
## 核心原理
### 消费模型的本质
RabbitMQ 的消费入口确实是拉取操作(`basic.consume` 或 `basic.get`),两者行为截然不同:
| 模式 | 方法 | 行为 | 特点 |
|------|------|------|-----|
| 推模式(Push) | `basic.consume` | 建立订阅后,Broker 持续主动投递 | 默认模式,适用于大多数场景 |
| 拉模式(Pull) | `basic.get` | 每次调用阻塞获取单条消息 | 适用于管理界面、健康检查等非实时场景 |
`basic.consume` 的工作流程:
```mermaid
sequenceDiagram
participant C as 消费者
participant RMQ as RabbitMQ Broker
participant Q as Queue
C->>RMQ: basic.consume(queue="orders", no_ack=false, prefetch=10)
RMQ-->>C: consume-ok(consumer_tag="ct1")
Note over C,RMQ: 流控通道已建立
loop 消息到达队列
RMQ->>C: basic.deliver(tag=1)<br/>queue="orders"
C->>C: 处理消息...
C->>RMQ: basic.ack(tag=1)
C->>RMQ: window refill (available=10)
end
```
消费者调用 `basic.consume` 后,RabbitMQ 会在该 Channel 上建立一个流控窗口。每当收到 `basic.deliver` 消息时,窗口减少;消费者发送 `basic.ack` 后窗口恢复。Window 耗尽时,Broker 暂停投递直到窗口回收——这就是**内建背压机制**。
### Prefetch Count(QoS)设置原理
`prefetch_count` 定义了单个 Channel 上允许的最大未确认消息数。它是流量控制的调节阀。
设 prefetch = N 时:
1. Broker 可以在没有收到 ack 的情况下,最多向该 Channel 发送 N 条消息
2. 这 N 条消息累积在未确认(unacked)状态
3. 当 ack 一条消息后,unacked 数减一,Broker 再补发一条
```go
// Go 代码示例:Qos 配置
// prefetch=1 启用公平分发(Fair Dispatch)
ch.Qos(1, // prefetch count
0, // prefetch size(字节限制,0表示不限制)
false) // global=false 作用于单个 channel
```
为什么 prefetch=1 能实现公平分发?假设消费者 A 处理快、B 处理慢:
- 如果 prefetch=N(大数值),Broker 可能在短时间内把 N 条消息全部发给 A,而 B 空闲等待
- 如果 prefetch=1,A 每处理完一条、发一个 ack,才会收到下一条,自然形成了速度匹配
> [!WARNING]
> `global=true` 是遗留参数,在所有现代客户端中建议设为 `false`。它会对整个连接(而非单个 Channel)生效,在多 Channel 场景下会导致意外行为。
### 预取策略对吞吐量和内存的影响
| 预取值 | 吞吐量表现 | 内存占用 | 适用场景 |
|--------|-----------|---------|---------|
| 1 | 较低(串行节奏) | 最低 | 处理耗时差异大的消费者组 |
| 10~50 | 较高 | 中等 | 通用生产环境默认值 |
| 100~500 | 最高 | 较高 | 短处理、低延迟需求 |
| 0(不设置) | 取决于默认值(通常为 0 即无限) | 可能溢出 | **严禁在生产中使用** |
prefetch 越大意味着 Broker 可以积压更多消息到消费者内存中。这降低了每次投递的往返开销(减少网络 RTT),但同时增加了消费者的内存压力和崩溃恢复时的损失量。
### 背压处理机制
RabbitMQ 的背压在两个层面上工作:
**Layer 1:Channel 级别的 Window**(prefetch 控制)
- 消费者窗口耗尽时,Broker 停止向该 Channel 投递
- 消费者恢复正常后自动解封
**Layer 2:Queue 级别的持久化**
- 当消费者整体处理能力低于生产者速度时,消息堆积在 Queue 中
- Queue 受 `x-max-length` 约束时可拒绝新消息
- 不受限时占用磁盘空间,可能拖垮 Broker
```
生产者速度 > 消费者速度 = 消息在 Queue 中排队等待
↓
Queue 满 + 有限长 = 新消息被拒
↓
走 DLX 或触发告警
```
### 多消费者并发与负载均衡
同一个 Queue 可以有多个消费者(构成 Consumer Group),RabbitMQ 以 Round-Robin 方式将消息均匀分配给各消费者:
```mermaid
graph LR
Q["Order Queue"] -->|msg 1| C1["Consumer A"]
Q -->|msg 2| C2["Consumer B"]
Q -->|msg 3| C1
Q -->|msg 4| C2
Q -->|msg 5| C1
Q -->|msg 6| C2
style Q fill:#fff3e0
style C1 fill:#e8f5e9
style C2 fill:#e8f5e9
```
前提条件是 `prefetch_count` 足够大(否则 prefetch=1 时只有一个消费者活跃,另一个空等)。Round-Robin 的分发方式是平均的但不一定是公平的——处理快的消费者实际上承担了更多工作量,这正是 prefetch=1 要解决的问题。
### 预取策略调优经验
高吞吐场景(如日志采集、指标上报):
- `prefetch=100~500`,消费者端做好消息批处理
- 处理逻辑尽量无状态,避免加锁
- Monitor unacked 数量和 Queue depth,发现趋势性增长立即扩容
低延迟场景(如实时通知、在线游戏):
- `prefetch=1~10`,让消费者尽快处理完再拿下一条
- 消费者数量尽可能多,分散到不同机器
- 监控 p99 延迟而非平均值,个别慢消费者可能拉高尾部
> [!TIP]
> 调优黄金法则:先设 prefetch=1 观察各消费者的处理耗时分布,再根据 p99 耗时最高的那个消费者来估算合适的 batch size,初始设为预估值的 2~3 倍即可。
## 实践场景
**高并发商品详情页缓存预热**:每日定时从数据库全量读取商品信息,通过 MQ 推送到缓存集群预热。日终跑批期间数据量大但每条消息处理简单,采用 prefetch=200 配合批量更新 Redis pipeline,在保证不淹没缓存节点的前提下最大化吞吐。
**即时订单状态推送**:用户下单后需要实时更新前端页面。这里 latency 优先级高于 throughput,设置 prefetch=3,消费者部署 10 个副本分布在不同的 Pod 中,配合 Manual ACK 确保状态变更的可靠性。
```go
// Go 代码示例:根据环境动态调整 Qos
func configureQos(env string) int {
switch env {
case "high-throughput":
return 200 // 大批量批处理
case "low-latency":
return 3 // 追求快速响应
default:
return 10 // 通用场景
}
}
```
## 扩展阅读
- [[Exchange 路由机制]]
- [[消息持久化与可靠性投递]]
- [[ACK 确认与死信队列]]
@@ -0,0 +1,189 @@
---
tags: [mq/rabbitmq, publisher-confirm, message-persistence, rabbitmq-transaction, return-callback]
create time: 2026-08-08 18:51
update time: 2026-08-08 18:51
---
# 消息持久化与可靠性投递
## 概述
在分布式系统中,消息丢失是比重复消费更严重的问题。RabbitMQ 的可靠性投递涉及三个层面的协同:Exchange 和 Queue 的持久化配置、消息本身的 persistent 标记、以及 Publisher Confirm 机制提供的发送端反馈。理解这些机制的设计动机和取舍,才能在实际业务中做出正确的选型。
## 核心原理
### 三阶段持久化
消息从生产者发出到被消费者消费的整个链路中,任何一个环节未持久化都会导致消息丢失风险。
```mermaid
graph TD
A[生产者] -->|"1. Exchange durable"| B["Direct Exchange<br/>(durable=true)"]
B -->|"2. Queue durable"| C["Order Queue<br/>(durable=true)"]
C -->|"3. Message persistent"| D["磁盘存储"]
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#fce4ec
```
第一阶段:Exchange 声明时设置 `durable=true`。如果 Exchange 不存在,RabbitMQ 会在启动时自动重建;若未设置 durable,重启后 Exchange 会被删除。
第二阶段:Queue 声明时设置 `durable=true`。注意:**队列持久化只保证元数据不丢**。实际存储在 queue 中的消息默认为内存页缓存,除非消息带有 persistent 标记才会写入磁盘。
第三阶段:消息发送时设置 `DeliveryMode=2`(persistent)。持久化消息在抵达 Queue 后会刷盘,而非仅仅留在内存 buffer 中。
> [!WARNING]
> 常见误解:仅设置 Queue durable 并不能保证消息不丢。如果消息 DeliveryMode=1(transient),即使队列本身是持久的,这些瞬态消息在服务器重启时也会被丢弃。
### Publisher Confirm 机制
Publisher Confirm 是 RabbitMQ 提供的一种异步确认机制。生产者在开启 confirm 模式后,每条消息都会被分配一个唯一的 `delivery-tag`,Broker 处理完成后会通过回调通知生产者结果。
两种工作模式:
| 模式 | 说明 | 适用场景 |
|------|------|---------|
| 单条确认 | 每发一条消息等一次 ack/nack,串行等待 | 吞吐量要求极低、需严格逐条确认的场景 |
| 批量确认 | 一次发送多条,一次性收到确认回调 | **绝大多数场景的首选**,吞吐量接近未开启 confirm 的水平 |
Confirm 模式的回调有两种触发条件:
```mermaid
sequenceDiagram
participant P as 生产者
participant RMQ as RabbitMQ Broker
participant CC as ConfirmCallback
participant RC as ReturnCallback
P->>RMQ: Publish(msg, mandatory=true)
activate RMQ
RMQ-->>CC: ack(tag=1001)
activate CC
Note right of CC: 消息路由到 Queue 成功
CC-->>P: 回调确认
deactivate CC
deactivate RMQ
Note over P,RMQ: 另一条消息
P->>RMQ: Publish(msg2, mandatory=true)
activate RMQ
alt Exchange存在但无绑定队列
RMQ-->>RC: return(msg2)
activate RC
Note right of RC: 路由失败但非不可恢复
RC-->>P: 回调退回
deactivate RC
else Exchange不存在
RMQ-->>RC: return(msg2)
activate RC
Note right of RC: Exchange 不可达
RC-->>P: 回调退回
deactivate RC
end
deactivate RMQ
```
`ConfirmCallback` 告诉生产者消息是否被 Broker 安全接收(路由完成),`ReturnCallback` 处理强制性消息因无法路由而被退回的情况。只有同时开启 `mandatory=true`,ReturnCallback 才会在路由失败时被触发。
### RabbitMQ Transaction 模式
RabbitMQ 的 AMQP 协议原生支持事务。开启事务后,发送的消息只有在事务提交后才会真正进入队列。如果事务回滚,期间发送的消息不会进入任何队列。
事务消息的四项基本原则:
1. **Channel 必须声明为事务模式**:调用 `channel.txSelect()`
2. **消息发送后必须提交事务**:`channel.txCommit()`
3. **事务内发送的消息不会立即投递**:直到 txCommit 后才进入队列
4. **txCommit 失败则事务回滚**:可通过 `txRollback()` 手动回滚
```go
// Go 代码示例:Transaction 模式(慎用)
func sendWithTx(ch *amqp.Channel, body string) error {
if err := ch.Tx(); err != nil {
return fmt.Errorf("tx select failed: %w", err)
}
defer func() {
_ = ch.TxRollback() // 出错时回滚
}()
if err := ch.Publish("", "my.queue", false, false,
amqp.Publishing{Body: []byte(body)}); err != nil {
return err
}
return ch.TxCommit()
}
```
### Confirm vs Transaction:对比分析
| 维度 | Publisher Confirm | Transaction |
|------|------------------|-------------|
| 吞吐量 | 高(异步批量) | 低(同步阻塞) |
| 实现复杂度 | 简单 | 简单 |
| 故障恢复 | 需要重试逻辑 | Broker 自动回滚 |
| 资源占用 | 低 | 高(需维护事务日志) |
| 语义强度 | 最多一次(至少一次需重试) | 恰好一次(配合幂等设计) |
| 适用性 | **生产环境首选** | 几乎不推荐使用 |
为什么 Confirm 优于 Transaction?
1. **性能差距巨大**:Transaction 采用同步提交方式,每一条消息的事务提交都要等待 Broker 的 fsync 操作完成,而 Confirm 可以批量异步返回。实测差距可达数倍甚至十倍以上。
2. **资源开销更小**:Transaction 需要维护完整的事务日志和回滚状态,在大量并发下容易成为瓶颈。Confirm 模式无需这些额外开销。
3. **生态兼容性更好**:Spring AMQP、Go amqp 等主流客户端对 Confirm 的支持远成熟于 Transaction。
> [!TIP]
> 面试回答要点:如果需要"恰好一次"语义,不要指望 RabbitMQ 本身保证——无论是 Confirm 还是 Transaction 都不够。正确做法是:Confirm 作为投递保障 + 业务层幂等键(如 orderId 唯一索引)兜底。这才是工业级方案的思路。
## 代码示例
```go
// Go 代码示例:Confirm + Return 完整回调链路
ch.NotifyPublish(make(chan amqp.Confirmation)) // 注册 Confirm
ch.NotifyReturn(make(chan amqp.Return)) // 注册 Return
confirms := ch.NotifyPublish(nil)
for conf := range confirms {
if conf.Ack {
log.Printf("消息 delivery-tag=%d 已确认", conf.DeliveryTag)
} else {
// nack 时需要业务侧重试或写入死信表
log.Printf("消息 delivery-tag=%d 未被确认", conf.DeliveryTag)
}
}
```
这段代码展示了 Confirm 的核心结构:通过 `NotifyPublish` 注册一个 Confirmation 通道,每个发出的消息对应一个 Confirmation 对象,其 `Ack` 字段为 true 表示 Broker 已成功处理,false 则需要应用层处理重发或降级逻辑。
```java
// Java 代码示例:Spring AMQP 中的 Confirm 配置
@Bean
public RabbitTemplate rabbitTemplate(RabbitConnectionFactory factory) {
RabbitTemplate template = new RabbitTemplate(factory);
template.setMandatory(true); // 开启 ReturnCallback
template.setConfirmCallback((correlation, ack, reason) -> {
if (!ack) log.warn("消息未确认: {}", reason);
});
template.setReturnsCallback(returned -> {
log.warn("消息被退回: {} - {}", returned.getMessage(), returned.getReason());
});
return template;
}
```
## 实践场景
**订单支付后的状态推送**:用户支付完成后需要将状态变更推送给物流、库存等多个下游系统。在这个场景下:
- Exchange 和 Queue 均设置为 `durable=true` 防止服务重启丢数据
- 消息设置为 `DeliveryMode=2`(persistent)确保磁盘持久化
- 开启 Publisher Confirm,nack 的消息放入本地重试表,定时任务补偿
- 配合业务幂等键(支付流水号 + 目标系统标识),确保重复收到的消息不会产生副作用
> [!WARNING]
> 持久化消息会带来明显的性能代价(大约 10 倍吞吐下降)。在对丢消息敏感度低的场景(如埋点日志采集),可以使用 transient 消息获得更高吞吐。关键是评估业务的容错阈值后再决定。
## 关联笔记
- [[Exchange 路由机制]]
- [[ACK 确认与死信队列]]
- [[推拉结合消费模式]]