vault backup: 2026-05-24 20:51:06

This commit is contained in:
hhs
2026-05-24 20:51:06 +08:00
parent 910d16682b
commit 686c0a5bd5
52 changed files with 9246 additions and 0 deletions
@@ -0,0 +1,140 @@
---
tags:
- MQ
create time: 2026-05-24 19:52
---
# 死信队列与消息回溯
## 概述
死信队列(DLQ)是消息消费失败后的"兜底方案",消息回溯则是重新处理历史数据的能力。本文详解死信队列的产生机制、各主流 MQ 的实现差异,以及消息回溯的实践策略。
## 正文
### 死信队列的产生原因
消息进入死信队列通常有三种触发条件:
**消费失败超过最大重试次数**:这是最常见的情况。消费者处理消息时抛出异常,经过 N 次重试后仍然失败,消息被转移到死信队列。
**消息过期(TTL)**:消息在队列中等待时间超过设定的 TTL(Time To Live),始终未被消费,成为"死信"。
**队列容量满**:队列达到最大长度限制,新消息无法入队,部分消息可能被丢弃或转入死信队列。
> [!question]
> 消费失败和消费超时是同一种情况吗?在设计重试策略时需要区别对待吗?
### 死信消息的特征与处理策略
死信消息有几个显著特征:**携带原始元数据**(生产时间、重试次数、失败原因)、**语义已不确定**(不知道业务状态是否已被部分修改)、**需要人工判断**。
处理策略通常有三种:
1. **人工介入排查**:最常见的做法。运维或开发人员分析失败原因,修复问题后手动重发。
2. **自动告警 + 限流处理**:当死信消息数量超过阈值时触发告警,同时对死信队列的消费进行限流,避免雪崩。
3. **转存到其他系统**:将死信消息写入数据库或 ES,便于后续分析和批量重处理。
```mermaid
flowchart TD
P["Producer"]
Q["Normal Queue"]
C["Consumer"]
DLQ["Dead Letter Queue"]
R["Retry"]
A["Alert System"]
DB["Database / ES"]
H["Human Intervention"]
P --> Q
Q --> C
C -->|"Success"| ACK["ACK"]
C -->|"Fail"| R
R -->|"Retry < Max"| C
R -->|"Retry >= Max"| DLQ
DLQ --> A
DLQ --> DB
DLQ --> H
```
### 各主流 MQ 的 DLQ 实现
**RabbitMQ**:通过 `x-dead-letter-exchange` 和 `x-dead-letter-routing-key` 属性配置。当消息被拒绝(reject/nack)且 `requeue=false` 时,自动路由到指定的死信交换机。
```go
// 声明带死信配置的队列
args := amqp.Table{
"x-dead-letter-exchange": "dlx-exchange",
"x-dead-letter-routing-key": "dlq-routing-key",
"x-message-ttl": 30000, // 30 秒 TTL
}
ch.QueueDeclare("order-queue", true, false, false, false, args)
```
**RocketMQ**:内置 `%DLQ%+ConsumerGroup` 命名的死信 Topic。消费失败超过最大重试次数(默认 16 次)后自动进入,无需额外配置。
**Kafka**:没有原生 DLQ 机制,需要自行实现。常见的做法是在消费者 catch 异常后,将失败消息发送到专门的死信 Topic。
### 消息重试机制
合理的重试策略是减少死信消息的关键。核心原则是**间隔递增 + 次数上限**:
```go
func retryDelay(attempt int) time.Duration {
// 指数退避:1s, 2s, 4s, 8s...
base := time.Second
delay := base * time.Duration(1<<uint(attempt))
if delay > 5*time.Minute {
delay = 5 * time.Minute // 设置上限
}
return delay
}
```
为什么用指数退避?如果下游服务暂时不可用,立即重试只会加重负担。递增间隔给下游恢复的时间,也能减少无效的重试次数。
> [!question]
> 重试间隔设多长合适?太短可能压垮下游,太长又影响消息时效性。你会怎么平衡?
### 消息回溯
消息回溯是指将消费位点回退到某个历史位置,重新消费已处理过的消息。典型场景包括:**业务逻辑 Bug 修复后需要重新处理**、**数据丢失后从消息中恢复**、**新上线的消费者需要历史数据初始化**。
**按时间回溯**:Kafka 支持 `offsetsForTimes` API,可以找到指定时间戳对应的偏移量:
```go
// 回溯到 2024-01-01 00:00:00
targetTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.Local)
offset, err := client.GetOffset(topic, partition, targetTime.UnixMilli())
if err == nil {
// 从该偏移量开始重新消费
consumer.Seek(topic, partition, offset)
}
```
**按偏移量回溯**:直接指定要回退到的偏移量位置,精确但需要提前记录关键节点的偏移量。
**重放消费**:创建新的消费组,从头消费整个 Topic 的消息。适用于数据恢复或新消费者冷启动。
```mermaid
flowchart LR
T["Timeline"]
N["Now"]
B["Bug Introduced"]
F["Bug Fixed"]
R["Replay From B"]
T --> B
B --> F
F --> N
F -.->|"Seek to offset"| R
R -->|"Re-consume"| N
```
> [!question]
> 如果消息回溯时发现部分消息已经被下游系统消费并产生了副作用(比如扣款),你会如何处理这种"幂等性"问题?
## 关联笔记
- [[14-MQ-消息可靠性]]
- [[15-MQ-顺序性保障]]