Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

中间件 / Middleware

1. Web 中间件全景 / The Web Middleware Landscape

2. 中间件——流量洪峰下的系统保护 / Middleware for Protecting Systems Under Traffic Spikes

cache layer

1. 缓存架构全景 / Cache Architecture Overview

2. Redis 深入——为什么你的缓存总是出问题 / Redis Internals and Cache Failure Modes

3. Redis 数据结构场景应用——什么时候用什么 / Choosing Redis Data Structures for Real Applications

4. 缓存策略——库存变了,缓存怎么处理 / Cache Strategies and Inventory Consistency

5. Redis 缓存三剑客——穿透/击穿/雪崩 + 一致性策略 / Redis Cache Penetration, Breakdown, Avalanche, and Consistency

6. Redis 分布式锁——从 SETNX 到 Redisson / Redis Distributed Locks from SETNX to Redisson

7. Redis Cluster 与 Sentinel 高可用架构 / Redis Cluster and Sentinel High-Availability Architecture

8. Redis 高级特性——Stream / PubSub / Module / LLM 应用

9. Redis 架构深度分析——为什么 Redis 能这么快 / Redis Architecture and the Sources of Its Performance

10. Redis 全景——为什么你的系统需要一个缓存层 / The Redis Landscape and Why Systems Need a Cache Layer

message queue

1. 消息队列全景——为什么你的系统需要一个中间人 / Message Queue Overview and Why Your System Needs a Middleman

2. Kafka 核心——为什么你的消息总是"丢"了 / Kafka Fundamentals and Message Delivery Semantics

3. 消息队列高级——死信队列、延迟消息、消息积压 / Advanced Messaging with Dead Letters, Delays, and Backlogs

4. Kafka Streams 与 Connect —— 让数据自己流动起来 / Kafka Streams and Connect for Streaming Data Pipelines

5. RabbitMQ 深度解析 —— 灵活路由与消息可靠性 / RabbitMQ Deep Dive into Flexible Routing and Reliability

6. 消息队列对比——为什么最终选了 Kafka / Comparing Message Queues and Choosing Kafka

search engine

1. 搜索引擎知识体系 / Search Engine Knowledge System

2. Elasticsearch——为什么 Like 查询总是那么慢 / Elasticsearch for Full-Text Search at Scale

3. Elasticsearch 查询 DSL 深入——为什么你的搜索总是不准 / Elasticsearch Query DSL Deep Dive

4. Elasticsearch 集群规划与运维——为什么你的集群总是"黄" / Elasticsearch Cluster Planning and Operations

5. Meilisearch 与轻量搜索替代方案——当 ES 太重时 / Meilisearch and Lightweight Search Alternatives

infrastructure

1. 基础设施组件全景 / Infrastructure Components Landscape

2. Nginx 与反向代理 / Nginx and Reverse Proxy

3. 服务发现 / Service Discovery

4. 配置中心 / Configuration Center

本页目录

消息队列高级——死信队列、延迟消息、消息积压 / Advanced Messaging with Dead Letters, Delays, and Backlogs ​

📅 创建时间:2026-05-08 🏷️ 标签:#死信队列 #延迟消息 #消息重试 #消息积压 #幂等消费 📚 前置知识:[[00-backend-overview]] [[02-mq-kafka]] [[03-mq-others]] 📚 相关知识:[[04-distributed-system]](分布式事务) [[11-architecture-patterns]](Saga)


场景:消息处理失败了,系统进入死循环 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  凌晨 3 点,监控报警:Kafka 消费 lag 暴增。             │
│                                                             │
│  排查发现:                                              │
│  订单服务消费消息时遇到 NPE(空指针异常)              │
│  → 消息被重复消费                                        │
│  → 又遇到 NPE                                            │
│  → 又重复消费                                            │
│  → 死循环了!                                            │
│                                                             │
│  更糟糕的是:                                            │
│  → 消息被消费了 1000 次                                   │
│  → 重复创建了 1000 个订单                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这一章,我们解决消息队列的三个高级问题:死信队列、延迟消息、消息积压。


第1节:死信队列——消息的"垃圾回收站" ​

什么是死信 ​

┌─────────────────────────────────────────────────────────────┐
│                 消息变成死信的条件                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 消费失败达到最大重试次数                             │
│  → 重试 3 次后仍然失败 → 进入死信队列                   │
│                                                             │
│  2. 消息超时未消费                                        │
│  → 延迟消息 TTL 到期 → 进入死信队列                     │
│                                                             │
│  3. 消息被拒绝(Reject)而不 requeue                    │
│  → 消费者明确拒绝消息 → 进入死信队列                   │
│                                                             │
│  4. 队列满了                                            │
│  → 超出队列容量 → 旧消息进入死信队列                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Kafka 死信队列实现 ​

python
from kafka import KafkaConsumer, KafkaProducer
import json

# 正常消费主题
main_topic = "order-created"

# 死信主题
dlq_topic = "order-created-dlq"

# 最大重试次数
MAX_RETRIES = 3

consumer = KafkaConsumer(
    main_topic,
    bootstrap_servers=['kafka:9092'],
    group_id='order-processor',
    enable_auto_commit=False,
    value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)

dlq_producer = KafkaProducer(
    bootstrap_servers=['kafka:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

for message in consumer:
    order = message.value
    retries = message.headers.get('retry-count', 0)

    try:
        process_order(order)
        consumer.commit()

    except RetryableError as e:
        # 可重试错误:重新入队
        if retries < MAX_RETRIES:
            # 重新发送消息,带上重试次数
            order['_retry_count'] = retries + 1
            producer.send(main_topic, order)
            producer.flush()
            consumer.commit()
        else:
            # 超过最大重试次数 → 进入死信队列
            order['_dlq_reason'] = str(e)
            order['_dlq_time'] = time.time()
            dlq_producer.send(dlq_topic, order)
            dlq_producer.flush()
            consumer.commit()

    except UnretryableError as e:
        # 不可重试错误:直接进入死信队列
        order['_dlq_reason'] = str(e)
        order['_dlq_time'] = time.time()
        dlq_producer.send(dlq_topic, order)
        dlq_producer.flush()
        consumer.commit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

RocketMQ 死信队列(原生支持) ​

┌─────────────────────────────────────────────────────────────┐
│              RocketMQ 死信队列                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  RocketMQ 原生支持死信队列,无需手动实现:             │
│                                                             │
│  消息重试次数超过阈值 → 自动进入 %DLQ% 前缀的队列      │
│                                                             │
│  原生主题:order-created                                  │
│  死信主题:%DLQ%order-created                            │
│                                                             │
│  消费配置:                                              │
│  consumer.setMaxReconsumeTimes(3)  # 最大重试 3 次   │
│                                                             │
│  死信消息特性:                                          │
│  • 不会自动删除,保留 3 天                              │
│  • 需要人工处理或定时清理                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

死信队列的监控和处理 ​

python
# 死信队列消费者:人工处理 / 自动修复
dlq_consumer = KafkaConsumer(
    dlq_topic,
    bootstrap_servers=['kafka:9092'],
    group_id='dlq-processor',
    auto_offset_reset='earliest'
)

for message in dlq_consumer:
    dlq_msg = json.loads(message.value)
    reason = dlq_msg.get('_dlq_reason', 'unknown')
    retry_count = dlq_msg.get('_retry_count', 0)

    # 分类处理
    if 'JsonParseException' in reason:
        # 数据格式错误:无法修复,直接记录日志
        log.error(f"DLQ 数据格式错误,无法处理: {dlq_msg}")

    elif 'DatabaseConnectionException' in reason:
        # 数据库连接问题:可能是临时故障,等待后重试
        time.sleep(60)
        kafka.send(main_topic, dlq_msg)  # 重新放回主队列

    elif 'BusinessLogicError' in reason:
        # 业务逻辑错误:需要人工介入
        send_alert(f"DLQ 业务错误,需要人工处理: {reason}")

    else:
        # 未知错误:统一记录
        log.error(f"DLQ 未知错误: {reason}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

第2节:延迟消息——订单 30 分钟未支付自动取消 ​

场景:订单超时取消 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  用户下单 → 订单状态 = "待支付"                          │
│  如果 30 分钟内未支付 → 自动取消订单                      │
│                                                             │
│  方案 A:定时轮询数据库(浪费资源)                      │
│  每分钟扫描:SELECT * FROM orders                         │
│            WHERE status='pending' AND create_time < NOW()-30min│
│  → 数据库压力大                                          │
│                                                             │
│  方案 B:延迟消息(推荐)                                │
│  下单时发送一条延迟消息(30 分钟后投递)                 │
│  → 30 分钟后消息被消费 → 检查订单状态 → 未支付则取消  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

RocketMQ 延迟消息 ​

java
// RocketMQ 原生支持延迟消息(18 个延迟级别)
public class OrderService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void createOrder(OrderDTO order) {
        // 1. 创建订单
        Order createdOrder = orderRepository.save(order);

        // 2. 发送延迟消息(延迟 30 分钟)
        rocketMQTemplate.asyncSend("order-timeout-topic", createdOrder,
            new MessageDelayLevel(30, TimeUnit.MINUTES));
    }
}

// 消费者:处理超时订单
@RocketMQMessageListener(topic = "order-timeout-topic", consumerGroup = "order-cancel")
public class OrderTimeoutConsumer implements RocketMQListener<Order> {
    @Override
    public void onMessage(Order order) {
        Order current = orderRepository.findById(order.getId());

        if ("pending".equals(current.getStatus())) {
            // 未支付,取消订单
            current.setStatus("cancelled");
            current.setCancelReason("超时未支付");
            orderRepository.save(current);

            // 恢复库存
            inventoryService.restore(order.getProductId(), order.getQuantity());
        }
        // 如果已支付,忽略
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

RabbitMQ 延迟队列(TTL + DLX) ​

┌─────────────────────────────────────────────────────────────┐
│              RabbitMQ 延迟队列实现                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                                                     │  │
│  │  order.delay.queue (TTL=30分钟)                    │  │
│  │      ↓ (消息过期)                                   │  │
│  │  order.dlx.exchange (死信交换机)                   │  │
│  │      ↓                                              │  │
│  │  order.cancel.queue (实际处理队列)                 │  │
│  │                                                     │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                             │
│  1. 下单时,消息发送到 order.delay.queue                 │
│  2. 消息在队列中等待 TTL(30 分钟)                     │
│  3. 消息过期后,进入 DLX                                  │
│  4. DLX 路由到 order.cancel.queue                       │
│  5. 消费者处理取消逻辑                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Redis ZSet 实现延迟队列 ​

python
import time
import json

# 使用 ZSet 实现延迟队列
# Score = 过期时间戳

def add_delay_task(task_id, task_data, delay_seconds):
    """添加延迟任务"""
    execute_at = time.time() + delay_seconds
    redis.zadd("delay:queue", {json.dumps(task_data): execute_at})

def process_delay_tasks():
    """定时扫描并执行到期的任务"""
    while True:
        now = time.time()
        # 获取所有到期的任务(score <= now)
        tasks = redis.zrangebyscore("delay:queue", 0, now)

        for task_data in tasks:
            task = json.loads(task_data)
            try:
                process_order_timeout(task)
                # 执行成功后删除
                redis.zrem("delay:queue", task_data)
            except Exception as e:
                log.error(f"处理延迟任务失败: {e}")
                # 可选择重试或进入死信队列

        time.sleep(1)  # 每秒检查一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

第3节:消息积压——消费者跟不上生产速度 ​

场景:消费者挂了 1 小时 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  消费者服务凌晨发布重启。                                 │
│                                                             │
│  发布时间:2:00 - 2:30(30 分钟消费者不可用)           │
│                                                             │
│  积压量:                                              │
│  生产速度:1000 条/秒 × 1800 秒 = 180 万条              │
│                                                             │
│  恢复后:                                              │
│  消费者需要处理 180 万条积压                             │
│  → 按正常速度需要 30 分钟才能追平                      │
│  → 这 30 分钟内新消息还在继续积压                     │
│                                                             │
│  后果:                                                 │
│  → 延迟越来越大                                        │
│  → 超时订单越来越多                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解决方案一:增加消费者 ​

python
# Kafka 消费者组:增加分区数和消费者数
# 分区数 = 最大并行度

# 当前:3 个分区,3 个消费者
# 积压后:扩容到 6 个分区,6 个消费者
# 并行度翻倍,消费速度翻倍

# 消费者配置
consumer = KafkaConsumer(
    'order-topic',
    bootstrap_servers=['kafka:9092'],
    group_id='order-processor',  # 同一消费者组
    # 消费者数量 <= 分区数量
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解决方案二:消息丢弃 + 补偿(极端情况) ​

python
# 当积压超过阈值时,丢弃旧消息,优先处理新消息
def consume_with_backpressure():
    # 检查积压量
    lag = consumer.end_offset() - consumer.position()

    if lag > 100000:  # 积压超过 10 万
        log.warning(f"消息积压严重 {lag},启动紧急处理")

        # 跳过旧消息,从最新开始消费
        consumer.seek_to_end()

        # 记录跳过的消息
        skipped_count = lag
        metrics.increment("messages_skipped", skipped_count)

        # 发送告警
        send_alert(f"跳过 {skipped_count} 条积压消息,请人工核对")

    else:
        # 正常消费
        for message in consumer:
            process_message(message)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

解决方案三:消费者优先级 ​

python
# 优先处理新消息(快速追平),旧消息异步补偿
class PriorityConsumer:
    def __init__(self):
        self.main_consumer = KafkaConsumer('order-topic', group_id='main')
        self.compensation_consumer = KafkaConsumer('order-topic', group_id='compensation')

    def run(self):
        lag = self.get_lag()

        if lag > 100000:
            # 积压严重:主消费者从最新开始,补偿消费者处理旧消息
            self.main_consumer.seek_to_end()
            self.compensation_consumer.seek_to_beginning()

            # 并行处理
            Thread(target=self.consume_main).start()
            Thread(target=self.consume_compensation).start()
        else:
            # 正常消费
            for msg in self.main_consumer:
                self.process(msg)

    def consume_compensation(self):
        # 异步处理积压消息
        for msg in self.compensation_consumer:
            # 标记为"补偿处理"
            order = json.loads(msg.value)
            order['_compensation'] = True
            self.process(order)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

第4节:消息幂等消费的完整方案 ​

消息幂等的五种方案 ​

┌─────────────────────────────────────────────────────────────┐
│              消息幂等五种方案                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  方案 1:唯一消息 ID(推荐)                             │
│  生产者生成唯一 ID,消费者去重                            │
│  ┌─────────────────────────────────────────────────────┐  │
│  │  producer: msg["idempotency_id"] = UUID()        │  │
│  │  consumer:                                          │  │
│  │    if redis.setnx(f"processed:{id}", 1, ex=86400): │  │
│  │        process(msg)                                 │  │
│  │    else:                                            │  │
│  │        pass  # 已处理过                             │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                             │
│  方案 2:数据库唯一索引                                  │
│  ┌─────────────────────────────────────────────────────┐  │
│  │  INSERT INTO orders (...)                           │  │
│  │  ON CONFLICT (idempotency_key) DO NOTHING         │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                             │
│  方案 3:Redis SET NX                                    │
│  SET order:process:idempotency_key unique_value NX EX 86400 │
│                                                             │
│  方案 4:状态机幂等                                      │
│  UPDATE orders SET status='paid'                          │
│  WHERE id=? AND status='pending'                        │
│  -- 如果已经支付,这条 SQL 影响 0 行                     │
│                                                             │
│  方案 5:Kafka 事务                                      │
│  消费消息 + 业务处理 + 提交 offset 在同一事务中         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

完整幂等消费实现 ​

python
class IdempotentConsumer:
    def __init__(self, redis_client, db):
        self.redis = redis_client
        self.db = db
        self.consumer = KafkaConsumer('order-topic', group_id='order-processor')

    def process(self, message):
        msg = json.loads(message.value)
        idempotency_key = msg['idempotency_key']

        # 1. Redis 去重(快速)
        if not self.redis.setnx(f"processed:{idempotency_key}", 1):
            log.info(f"消息已处理,跳过: {idempotency_key}")
            return

        # 2. 设置过期时间(防止 Redis 数据膨胀)
        self.redis.expire(f"processed:{idempotency_key}", 86400)

        # 3. 业务处理
        try:
            self.create_order(msg)

            # 4. 提交 offset
            self.consumer.commit()

        except DuplicateOrderException:
            # 订单重复(数据库唯一约束),幂等成功
            self.consumer.commit()

        except Exception as e:
            # 业务异常,删除 Redis 标记,允许重试
            self.redis.delete(f"processed:{idempotency_key}")
            raise e

    def create_order(self, msg):
        # 数据库幂等插入
        self.db.execute("""
            INSERT INTO orders (id, user_id, product_id, idempotency_key, ...)
            VALUES (?, ?, ?, ?, ...)
            ON CONFLICT (idempotency_key) DO NOTHING
        """, ...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

升华:MQ 高级模式的最佳实践 ​

┌─────────────────────────────────────────────────────────────┐
│              MQ 高级模式 Checklist                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  死信队列:                                              │
│  ✅ 消费失败的消息必须进入 DLQ,不能无限重试             │
│  ✅ DLQ 消息需要监控和定期清理                           │
│  ✅ 不同错误类型路由到不同的 DLQ                         │
│                                                             │
│  延迟消息:                                             │
│  ✅ RocketMQ 推荐(原生支持 18 个延迟级别)              │
│  ✅ Redis ZSet 适合自建延迟队列                         │
│  ✅ 延迟精度要求高时用时间轮算法                        │
│                                                             │
│  消息积压:                                             │
│  ✅ 监控 consumer lag,设置告警阈值                      │
│  ✅ 积压时快速扩容消费者                                 │
│  ✅ 极端情况可丢弃旧消息,优先处理新消息                │
│                                                             │
│  幂等消费:                                             │
│  ✅ 每条消息必须有唯一标识(idempotency_key)            │
│  ✅ 幂等去重 + 数据库唯一约束 双保险                    │
│  ✅ 处理失败要删除幂等标记,允许重试                    │
│                                                             │
│  一句话总结:                                              │
│  MQ 的可靠性 = 消息不丢 + 处理不重 + 失败可查。          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ Kafka DLQ 的具体配置方式
✅ RocketMQ 延迟消息的参数配置
✅ 各语言的 Kafka 消费者 API

必须理解:
🔴 消息变成死信的四种条件
🔴 延迟消息的实现原理(TTL+DLX vs ZSet vs 时间轮)
🔴 消费者积压时的应急处理策略
🔴 五种幂等消费方案及各自适用场景
1
2
3
4
5
6
7
8
9
10

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇2. Kafka 核心——为什么你的消息总是"丢"了 / Kafka Fundamentals and Message Delivery Semantics
下一篇4. Kafka Streams 与 Connect —— 让数据自己流动起来 / Kafka Streams and Connect for Streaming Data Pipelines

持续记录,持续成长

Copyright © Tidenflow