← 返回工作场景纪实

🐰 Kafka 🚀 消息队列架构手册

RabbitMQ / Kafka / RocketMQ 从入门到生产实战 — 消息从哪来、往哪去、怎么保证不丢

RabbitMQKafkaRocketMQ 消息可靠性幂等性消息积压 最终一致性

1. 消息队列是什么?— 为什么需要中间件来传递消息

为什么需要消息队列?

没有消息队列时(同步调用):订单服务直接调用库存服务、积分服务、短信服务。如果其中积分服务响应慢,整个下单请求就会被拖慢;如果积分服务挂了,整个下单直接失败——一个下游故障级联到上游。

有了消息队列后(异步解耦):订单服务只管把"订单已创建"这个事件发到 MQ,立即返回给用户。下游各服务各自从 MQ 取消息处理。订单服务不关心下游有几个服务、是否在线。

这就是消息队列的三大核心作用解耦(上游不需要知道下游有谁)、异步(发完消息就返回,不等处理完)、削峰(突发流量先堆积在 MQ,下游按自己的能力消费)。

📐 消息队列通用模型

Producer
生产者
发送消息的应用
如:订单服务
Message Queue
消息队列(Broker)
存储 & 转发消息
独立的中间件服务
Consumer
消费者
处理消息的应用
如:库存/积分服务
Producer → 发消息(Message) → Broker(队列/主题) → Consumer 拉取/推送 → 处理业务逻辑

🎯 三大核心作用详解

1️⃣ 解耦(Decoupling)

订单服务发消息到 MQ,下游的库存服务、积分服务、短信服务各取所需。订单服务不需要知道下游有几个服务——新增一个数据分析服务,只需订阅对应的队列即可,订单服务代码完全不改。

代码对比:
❌ 没有 MQ:orderService.deductStock() → orderService.addPoints() → orderService.sendSMS()(耦合 3 个服务)
✅ 有了 MQ:orderService.send("order_created", orderInfo)(只管发消息,下游各自消费)

2️⃣ 异步(Async)

用户下单后,接口立即返回"下单成功"。扣库存、加积分、发短信这些耗时操作异步处理。用户感知的响应时间从同步的 700ms 降到 60ms。

耗时对比:
同步:下单(50ms) + 扣库存(200ms) + 加积分(150ms) + 发短信(300ms) = 700ms
异步:下单(50ms) + 发 MQ(10ms) = 60ms,其余操作异步完成

3️⃣ 削峰(Peak Clipping)

秒杀活动瞬间 10 万请求涌入,数据库扛不住。先把请求全部丢进 MQ,消费端按数据库能承受的速度(比如 1000/s)慢慢消费。MQ 起到了"蓄水池"的作用。

削峰效果:
没有削峰:10万请求 → 直接打数据库 → 数据库连接池耗尽 → 系统崩溃 💥
有了削峰:10万请求 → MQ 积压消息 → 消费端 1000/s 稳定消费 → 数据库安然无恙 ✅

🔍 消息队列的核心概念

概念技术含义说明
Message传递的数据单元,包含 headers + body最小信息单位,如一条 JSON 格式的订单事件
Producer发送消息的应用消息的生产方,如订单服务、日志采集器
Consumer接收处理消息的应用消息的消费方,如库存服务、数据分析服务
Broker消息队列服务器,负责存储转发独立的中间件进程,如 RabbitMQ Server、Kafka Broker
Queue/Topic消息存放的地方RabbitMQ 叫 Queue,Kafka 叫 Topic
ExchangeRabbitMQ 特有:按规则把消息路由到不同队列消息不直接进 Queue,先到 Exchange 做路由匹配
PartitionKafka 特有:一个 Topic 分成多个 Partition实现并行读写,Partition 数 = 最大并行消费数
Consumer GroupKafka 特有:同组消费者共同消费一个 Topic组内每个 Consumer 负责不同 Partition,互不重复
ACK消费者确认消息已处理完毕消费者发 ACK 后,Broker 才删除该消息
OffsetKafka 消息的位置编号消费者记住自己消费到哪个 Offset,重启后从该位置继续

2. RabbitMQ 架构 — Exchange 路由模型

核心设计:RabbitMQ 的消息不是直接从 Producer 扔进 Queue 的。Producer 先把消息发给 Exchange(交换机),Exchange 根据 Routing Key + Binding 规则,决定把消息路由到哪个(或哪些)Queue。这个两层设计(Exchange → Queue)使得消息路由非常灵活——同一条消息可以根据规则分发到不同队列、做优先级处理、做按类型分发等。

📐 RabbitMQ 整体架构图

Producer
订单服务
发送消息
Exchange
交换机
根据路由规则分发
Queue 1
订单队列
Queue 2
积分队列
Queue 3
短信队列
Consumer
库存服务
Consumer
积分服务
Consumer
短信服务
Binding = Exchange 和 Queue 之间的绑定规则(Binding Key)
Routing Key = 消息上标注的目标地址,Exchange 拿着它去匹配 Binding Key

🔍 核心组件详解

1. Connection(连接)

Producer / Consumer 与 RabbitMQ 之间的一条 TCP 长连接。所有通信都基于这个连接。

2. Channel(通道)

一条 Connection 上可以开多个 Channel(多路复用)。每个 Channel 是独立的轻量级虚拟连接,不用每次都建 TCP。

为什么需要 Channel?
一个应用可能要发很多种消息(订单、积分、短信),如果每种都建一个 TCP 连接,太浪费资源。
用 Channel:一个 TCP 连接 + 多个 Channel,每个 Channel 处理一种消息,效率高。

3. Exchange(交换机)

消息的第一站。Producer 的消息不是直接进 Queue,而是先到 Exchange。Exchange 根据类型 + Routing Key + Binding 规则决定把消息路由到哪个 Queue。

RabbitMQ 有 4 种 Exchange 类型(下一节详讲)。

4. Queue(队列)

真正存储消息的地方。FIFO(先进先出)。多个 Consumer 可以订阅同一个 Queue,但一条消息只会被其中一个 Consumer 消费(竞争消费)。

5. Binding(绑定)

Exchange 和 Queue 之间的关系。Binding Key 就是路由规则——"key=order" 的消息进 Queue1,"key=points" 的进 Queue2。

6. Virtual Host(虚拟主机)

RabbitMQ 里的"租户隔离"。每个 vhost 有自己的 Exchange、Queue、Binding、权限。不同业务线用不同 vhost,互不影响。默认 vhost 是 /

📊 消息流转完整过程

1. Producer 创建消息 2. 选择 Exchange + Routing Key 3. Exchange 路由匹配 4. 投递到 Queue 5. Queue 存储消息 6. Consumer 拉取/推送 7. 处理业务逻辑 8. 发送 ACK 9. Queue 删除消息
完整链路:订单服务发消息(指定 Exchange 和 Routing Key)→ Exchange 根据 Binding 规则路由到目标 Queue → Consumer 从 Queue 取消息 → 执行业务逻辑(扣库存等)→ 发 ACK 确认 → RabbitMQ 才从 Queue 中删除该消息。如果 Consumer 处理失败发 NACK,消息会重新入队。

🖥️ RabbitMQ 管理界面

RabbitMQ Management — http://localhost:15672
Overview
📤 Messages ready: 1,234
📨 Messages unacked: 56
📊 Publish rate: 245/s
📥 Deliver rate: 230/s
Queues
NameMessagesConsumersRate
order_queue8563120/s
points_queue234185/s
sms_queue144260/s
dlx_order_queue1200/s

3. RabbitMQ 五种工作模式 — 5种路由策略

📦 模式 1:简单模式(Simple)— 一对一直送

Producer
Queue
Consumer

最简单的模式:一个生产者、一个队列、一个消费者。没有 Exchange(用默认 Exchange),消息直接进 Queue。

适用场景:简单的点对点任务分发,如发一封邮件通知、执行一个后台批处理任务。

// Producer — 发送消息 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try (Connection conn = factory.newConnection(); Channel ch = conn.createChannel()) { // 声明队列(不存在则自动创建) ch.queueDeclare("order_queue", false, false, false, null); // 发送消息 String msg = "{"orderId":1001,"product":"无线耳机","qty":2}"; ch.basicPublish("", "order_queue", null, msg.getBytes()); System.out.println(" [x] Sent '" + msg + "'"); } // Consumer — 消费消息 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection conn = factory.newConnection(); Channel ch = conn.createChannel(); ch.queueDeclare("order_queue", false, false, false, null); // 定义回调:收到消息时执行 DeliverCallback cb = (tag, delivery) -> { String msg = new String(delivery.getBody(), "UTF-8"); System.out.println(" [x] Received '" + msg + "'"); // 处理订单... }; ch.basicConsume("order_queue", true, cb, tag -> {});
✅ 运行结果
Producer 输出: [x] Sent '{"orderId":1001,"product":"无线耳机","qty":2}'
Consumer 输出: [x] Received '{"orderId":1001,"product":"无线耳机","qty":2}'

📦 模式 2:工作队列模式(Work Queue)— 多消费者竞争消费

Producer
Queue
order_queue
Consumer 1
Consumer 2
Consumer 3
一条消息只会被一个 Consumer 消费(竞争消费),消息在多个消费者之间分配

一个生产者、一个队列、多个消费者。消息在消费者之间轮询分配(默认 Round-Robin)。适合任务分发场景——多个消费者分担处理同一个队列的消息,提高吞吐。

注意:能者多劳(Fair Dispatch)
默认是轮询(你一张我一张),但有个问题:Consumer A 处理快(100ms/条),Consumer B 处理慢(500ms/条)。如果平均分配,A 早就干完闲着了,B 还在忙。
解决:channel.basicQos(1) —— 一次只给一个消费者派 1 条消息,消费者处理完(ACK)后才给下一条。这样快的消费者会多处理,慢的少处理。
// Consumer — 开启公平分发 ch.basicQos(1); // 一次只处理 1 条消息,处理完 ACK 后才给下一条 DeliverCallback cb = (tag, delivery) -> { String msg = new String(delivery.getBody(), "UTF-8"); System.out.println(" [Consumer-A] 处理: " + msg); Thread.sleep(100); // 模拟处理耗时 ch.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 手动 ACK }; // 第二个参数 false = 手动 ACK(不自动确认) ch.basicConsume("order_queue", false, cb, tag -> {});
✅ 运行结果(6 条消息,2 个消费者)
Consumer A: [Consumer-A] 处理: order-1(100ms 后 ACK)
Consumer B: [Consumer-B] 处理: order-2(500ms 后 ACK)
Consumer A: [Consumer-A] 处理: order-3(A 先 ACK,立即拿到下一条)
Consumer A: [Consumer-A] 处理: order-5(A 又先 ACK)
Consumer B: [Consumer-B] 处理: order-4
Consumer B: [Consumer-B] 处理: order-6
效果:A 处理了 4 条,B 处理了 2 条(能者多劳)

📦 模式 3:发布/订阅模式(Fanout)— 广播:所有队列都收到

Producer
Exchange
(fanout)
广播:不管 key
全部分发
Queue A
库存队列
Queue B
积分队列
Queue C
短信队列
同一条消息被复制到所有绑定的 Queue —— 每个 Queue 的 Consumer 都能收到完整消息

Exchange 类型为 fanout:不理会 Routing Key,消息被广播到所有绑定的 Queue。每个 Queue 都会收到一份消息的副本。

适用场景:订单创建后需要同时通知库存、积分、短信等多个下游服务。一条消息广播给所有绑定的队列。

// Producer — 广播消息 ch.exchangeDeclare("order_exchange", "fanout"); // 声明 fanout 类型交换机 String msg = "新订单: 无线耳机x2"; ch.basicPublish("order_exchange", "", null, msg.getBytes()); // Routing Key 为空,因为 fanout 根本不看 key // Consumer 1(库存服务)— 每个消费者创建自己的临时队列 ch.exchangeDeclare("order_exchange", "fanout"); String queueName = ch.queueDeclare().getQueue(); // 随机队列名 ch.queueBind(queueName, "order_exchange", ""); // 绑定到交换机 ch.basicConsume(queueName, true, (tag, delivery) -> { String msg = new String(delivery.getBody(), "UTF-8"); System.out.println(" [库存] 扣减库存: " + msg); }, tag -> {});
✅ 运行结果(发送 1 条消息)
库存服务: [库存] 扣减库存: 新订单: 无线耳机x2
积分服务: [积分] 增加积分: 新订单: 无线耳机x2
短信服务: [短信] 发送通知: 新订单: 无线耳机x2
同一条消息被 3 个服务同时消费!

📦 模式 4:路由模式(Direct)— 按类型精确分发

Producer
Exchange
(direct)
按 key 精确匹配
Queue A
key=order
Queue B
key=points
Queue C
key=order
key=points
Routing Key = "order" → 进 Queue A 和 Queue C;Routing Key = "points" → 进 Queue B 和 Queue C

Exchange 类型为 direct:消息的 Routing Key 必须和 Binding Key 精确匹配才能进队列。

适用场景:按消息类型精确路由——"order" 类消息进订单队列、"points" 类消息进积分队列。比 fanout 更精细。

// Producer — 指定 Routing Key 发消息 ch.exchangeDeclare("direct_exchange", "direct"); ch.basicPublish("direct_exchange", "order", null, "订单消息".getBytes()); ch.basicPublish("direct_exchange", "points", null, "积分消息".getBytes()); ch.basicPublish("direct_exchange", "log", null, "日志消息".getBytes()); // Consumer — 只绑定自己关心的 key ch.queueBind(queueName, "direct_exchange", "order"); // 只收 order
✅ 运行结果
库存服务(绑 order): 收到 "订单消息"
积分服务(绑 points): 收到 "积分消息"
日志服务(绑 log): 收到 "日志消息"
注意:key="log" 的消息只有日志服务收到,其他服务没收到(精确匹配)

📦 模式 5:主题模式(Topic)— 按通配符模糊匹配

Producer
Exchange
(topic)
按模式匹配
* = 一个词
# = 零或多个词
Queue A
order.*
所有订单
Queue B
*.urgent
所有加急
Queue C
#
所有消息
key="order.normal" → Queue A ✅ Queue B ❌ Queue C ✅
key="sms.urgent" → Queue A ❌ Queue B ✅ Queue C ✅
key="order.urgent" → Queue A ✅ Queue B ✅ Queue C ✅

Exchange 类型为 topic:Routing Key 可以用通配符匹配。

  • *(星号):匹配恰好一个单词。如 order.* 匹配 order.create 但不匹配 order.create.vip
  • #(井号):匹配零或多个单词。如 order.# 匹配 orderorder.createorder.create.vip

适用场景:需要按层级分类路由,如 "order.create"、"order.cancel.urgent" 等多级事件。

// Producer — 发送带层级 key 的消息 ch.exchangeDeclare("topic_exchange", "topic"); ch.basicPublish("topic_exchange", "order.create", null, "创建订单".getBytes()); ch.basicPublish("topic_exchange", "order.cancel.urgent", null, "加急取消".getBytes()); ch.basicPublish("topic_exchange", "sms.urgent", null, "短信加急".getBytes()); // Consumer A — 关心所有订单事件 ch.queueBind(queueName, "topic_exchange", "order.*"); // Consumer C — 全部消息(日志审计) ch.queueBind(queueName, "topic_exchange", "#");
✅ 运行结果
消息 key="order.create"
→ Consumer A (order.*) ✅ | Consumer C (#) ✅
消息 key="order.cancel.urgent"
→ Consumer A (order.*) ❌ (* 只匹配一个词) | Consumer C (#) ✅
消息 key="sms.urgent"
→ Consumer A (order.*) ❌ | Consumer C (#) ✅
五种模式总结对比:
Simple = 1P→1Q→1C(简单直连)
Work Queue = 1P→1Q→多C(竞争消费)
Fanout = 1P→Exchange→所有Q(广播)
Direct = 1P→Exchange→精确匹配的Q(定向)
Topic = 1P→Exchange→模糊匹配的Q(模式匹配)
补充:还有一种 Headers 模式(根据消息头匹配),极少使用。

4. RabbitMQ 高级特性 — 怎么保证消息不丢?

🔒 4.1 消息确认(ACK)— 处理完才确认

RabbitMQ 默认是自动确认(autoAck=true):消息一发给消费者就从队列删除。但如果消费者处理到一半崩了,消息就丢了

手动确认(autoAck=false):消费者处理完后主动发 ACK,RabbitMQ 才删除消息。如果消费者崩了没发 ACK,消息会重新入队

核心机制:自动确认模式下,消息被取出就立即删除——如果消费者处理到一半崩溃,这条消息就永久丢失了。手动确认模式下,消息被取出后标记为 unacked,只有消费者处理完毕并发送 ACK 后才真正删除。如果消费者崩溃(TCP 断开),unacked 的消息会自动重新入队,被其他消费者处理。
// 手动 ACK 模式 Channel ch = conn.createChannel(); ch.basicQos(1); // 每次只发 1 条 DeliverCallback cb = (tag, delivery) -> { String msg = new String(delivery.getBody(), "UTF-8"); try { processOrder(msg); // 处理订单 // 处理成功 → ACK ch.basicAck(delivery.getEnvelope().getDeliveryTag(), false); System.out.println(" [✓] ACK: " + msg); } catch (Exception e) { // 处理失败 → NACK,消息重新入队 ch.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); System.out.println(" [✗] NACK, 重新入队: " + msg); } }; // 第二个参数 false = 手动 ACK ch.basicConsume("order_queue", false, cb, tag -> {});

⏰ 4.2 消息 TTL(Time To Live)— 消息有效期

消息可以设置过期时间,超过 TTL 的消息会被 RabbitMQ 自动删除(或进入死信队列)。

适用场景:订单 30 分钟未支付自动取消——订单消息设置 TTL=30min,过期后进入死信队列触发取消逻辑。

// 方式1:给队列设置 TTL(队列中所有消息统一过期时间) Map<String, Object> args = new HashMap<>(); args.put("x-message-ttl", 30000); // 30秒 = 30000ms ch.queueDeclare("order_queue", false, false, false, args); // 方式2:给单条消息设置 TTL(每条消息可以不同) AMQP.BasicProperties props = new AMQP.BasicProperties() .builder() .expiration("60000") // 这条消息 60 秒后过期 .build(); ch.basicPublish("", "order_queue", props, msg.getBytes());

💀 4.3 死信队列(DLX)— 处理失败的异常消息

消息变成"死信"(Dead Letter)的三种情况:

  1. 消息被消费者 NACK 或 REJECT,且 requeue=false
  2. 消息超过 TTL 过期
  3. 队列达到最大长度,新消息被挤掉

死信不会被直接丢弃,而是被路由到一个死信交换机(DLX),再由死信交换机路由到死信队列,等待人工处理。

正常队列
order_queue
TTL=30s
满/被NACK/过期
DLX
死信交换机
自动路由
死信队列
dlx_order_queue
人工排查
告警/人工处理
运营介入
为什么需要死信队列?在生产环境中,总有一些消息无法被正常消费(消费者报错、数据格式错误、依赖服务不可用等)。如果不做死信处理,这些消息会无限重试导致消息积压;如果直接丢弃,会造成数据丢失。死信队列是一种"异常消息兜底"机制——消费失败的消息被安全地转移到死信队列,等后续人工排查或补偿处理。
// 1. 创建死信交换机和死信队列 ch.exchangeDeclare("dlx_exchange", "direct"); ch.queueDeclare("dlx_order_queue", true, false, false, null); ch.queueBind("dlx_order_queue", "dlx_exchange", "order"); // 2. 创建正常队列,绑定死信交换机 Map<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange", "dlx_exchange"); // 指定死信交换机 args.put("x-dead-letter-routing-key", "order"); // 死信路由 key args.put("x-message-ttl", 30000); // 30秒 TTL ch.queueDeclare("order_queue", true, false, false, args);

⏳ 4.4 延迟队列 — "30分钟后自动取消未支付订单"

RabbitMQ 没有原生的延迟队列,但可以通过 TTL + DLX 组合实现:

消息发到"等待队列"(设了 TTL)→ 消息过期 → 进入死信交换机 → 路由到"处理队列" → 消费者处理。

Producer
下单
等待队列
TTL=30min
消息在这里等30分钟
没人消费

过期
DLX
处理队列
cancel_queue
Consumer
检查并取消
实现原理:"等待队列"设置了 30 分钟 TTL 但没有消费者。消息进入后无人消费,30 分钟后过期成为死信,被路由到死信交换机,再进入"处理队列"。消费者监听处理队列,收到消息后检查订单状态——如果已付款就跳过,未付款就执行取消逻辑。
// Spring Boot + RabbitMQ 延迟队列配置 @Configuration public class DelayQueueConfig { // 1. 死信交换机 @Bean public DirectExchange orderDlxExchange() { return new DirectExchange("order.dlx.exchange"); } // 2. 处理队列(真正消费的队列) @Bean public Queue orderCancelQueue() { return QueueBuilder.durable("order.cancel.queue").build(); } // 3. 等待队列(TTL=30分钟,绑定死信交换机) @Bean public Queue orderWaitQueue() { return QueueBuilder.durable("order.wait.queue") .withArgument("x-dead-letter-exchange", "order.dlx.exchange") .withArgument("x-dead-letter-routing-key", "cancel") .withArgument("x-message-ttl", 1800000) // 30分钟 = 1800000ms .build(); } // 4. 绑定死信交换机 → 处理队列 @Bean public Binding dlxBinding() { return BindingBuilder.bind(orderCancelQueue()) .to(orderDlxExchange()).with("cancel"); } } // 5. 发送延迟消息 @Service public class OrderService { @Autowired private RabbitTemplate rabbitTemplate; public void createOrder(Order order) { // 保存订单到数据库... orderMapper.insert(order); // 发送延迟消息到等待队列 rabbitTemplate.convertAndSend("", "order.wait.queue", JSON.toJSONString(order)); } } // 6. 消费延迟消息 @Component public class OrderCancelConsumer { @RabbitListener(queues = "order.cancel.queue") public void handleCancel(String msg) { Order order = JSON.parseObject(msg, Order.class); if (order.getStatus() == OrderStatus.UNPAID) { order.setStatus(OrderStatus.CANCELLED); orderMapper.updateById(order); log.info("订单{}超时未支付,已自动取消", order.getId()); } } }
✅ 效果
12:00:00 - 用户下单,发送延迟消息到 order.wait.queue
12:00:00 ~ 12:30:00 - 消息在等待队列中等待(无消费者)
12:30:00 - 消息过期 → 进入 DLX → 路由到 order.cancel.queue
12:30:00 - OrderCancelConsumer 消费:检查订单状态
12:30:00 - 订单1001状态=UNPAID → 自动取消 ✅

💾 4.5 持久化 — Broker 重启也不丢消息

RabbitMQ 默认数据在内存里,重启就没了。要保证消息不丢失,需要三重持久化

⚠️ 必须三重持久化缺一不可!
1️⃣ Exchange 持久化(durable=true)
2️⃣ Queue 持久化(durable=true)
3️⃣ Message 持久化(deliveryMode=2)
只做 Queue 持久化不做 Message 持久化 = 队列还在但消息丢了,没意义。
// 1. Exchange 持久化 ch.exchangeDeclare("order_exchange", "direct", true); // 第3个参数 true = durable // 2. Queue 持久化 ch.queueDeclare("order_queue", true, false, false, null); // 第2个参数 true = durable // 3. Message 持久化 AMQP.BasicProperties props = new AMQP.BasicProperties() .builder() .deliveryMode(2) // 2 = 持久化,1 = 非持久化 .build(); ch.basicPublish("order_exchange", "order", props, msg.getBytes());
三层为什么要都做?
Exchange 持久化 = 交换机的元数据(名称、类型、Binding 规则)写入磁盘
Queue 持久化 = 队列的元数据写入磁盘
Message 持久化 = 消息体本身写入磁盘
缺任何一层,重启后对应的数据就不存在了——只有三层都持久化,Broker 重启后才能恢复完整的路由规则和消息内容。

🔄 4.6 生产者确认(Publisher Confirm)— 确认消息真的到了 Broker

生产者发消息后,怎么知道消息真的到了 RabbitMQ?用 Publisher Confirm 机制。

Producer 发消息 Broker 写入队列 Broker 回发 Confirm Producer 收到确认
// 开启 Confirm 模式 ch.confirmSelect(); // 异步确认:监听 Confirm 回调 ch.addConfirmListener(new ConfirmListener() { public void handleAck(long seq, boolean multiple) { System.out.println(" [✓] 消息已确认: seq=" + seq); } public void handleNack(long seq, boolean multiple) { System.out.println(" [✗] 消息被拒绝: seq=" + seq + ",需要重发!"); } });
Spring Boot 配置:
spring.rabbitmq.publisher-confirm-type=correlated(异步回调)
spring.rabbitmq.publisher-returns=true(消息无法路由时回调)

5. RabbitMQ 集群 — 多节点高可用

为什么需要集群?单节点 RabbitMQ 存在单点故障风险——如果这台机器宕机,整个消息系统不可用。RabbitMQ 集群通过多节点部署 + 队列数据复制来保证高可用:任何一个节点故障,其他节点自动接管,消息不丢失。

📐 集群架构图

Producer
Node A
Master
队列数据主节点
Node B
Slave
队列数据从节点
Node C
Slave
队列数据从节点
3 节点组成集群,元数据全节点同步,队列数据可配置镜像队列(HA Queue)
Consumer

RabbitMQ 集群的两种高可用方案:

方案原理优点缺点版本
镜像队列
(HA Queue)
每个队列在多个节点上有完整副本(master + mirrors) 配置简单,真正的高可用 同步复制性能差,镜像太多浪费资源 3.x 以前
仲裁队列
(Quorum Queue)
基于 Raft 协议的共识队列,多数节点写入成功即算成功 性能好、数据一致性强、推荐方案 不支持某些非持久化特性 3.8+ 推荐
// 仲裁队列声明(RabbitMQ 3.8+) Map<String, Object> args = new HashMap<>(); args.put("x-queue-type", "quorum"); // 声明为仲裁队列 args.put("x-quorum-initial-group-size", 3); // 3 个节点 ch.queueDeclare("order_queue", true, false, false, args);
# Docker Compose 搭建 3 节点 RabbitMQ 集群 $ docker-compose up -d Creating rabbit-node1 ... done Creating rabbit-node2 ... done Creating rabbit-node3 ... done # 查看集群状态 $ docker exec rabbit-node1 rabbitmqctl cluster_status Cluster status of node rabbit@node1 ... [{running_applications,[{rabbit,"RabbitMQ","3.12.0"}]}] Disk Nodes: rabbit@node1, rabbit@node2, rabbit@node3 Running Nodes: rabbit@node1, rabbit@node2, rabbit@node3 ✓ 集群 3 节点全部在线

6. Kafka 架构 — 日志模型与分区并行

核心设计:Kafka 与 RabbitMQ 有本质区别。RabbitMQ 是"消息代理"模型——消息被消费后即删除。Kafka 是"追加日志"模型——消息以日志文件形式持久化存储,消费后不删除(默认保留 7 天),可以被回放(重新消费)。

关键区别:RabbitMQ 是"邮递员模型"(送完就走),Kafka 是"日志模型"(追加写入,保留历史)。这使得 Kafka 特别适合大数据分析、日志收集、流处理等需要回放历史数据的场景。

📐 Kafka 整体架构图

Kafka Cluster(3 个 Broker)
Producer
订单服务
Topic: order-events(3 个 Partition)
P0
Leader
Broker 1
P1
Leader
Broker 2
P2
Leader
Broker 3
P0: [msg0] [msg3] [msg6] ... | P1: [msg1] [msg4] [msg7] ... | P2: [msg2] [msg5] [msg8] ...
Consumer Group A
C1 → P0
C2 → P1
C3 → P2
Consumer Group B
C4 → P0+P1+P2
Broker = Kafka 服务器节点 | Topic = 消息分类 | Partition = 并行分片 | Consumer Group = 消费者组
同一 Consumer Group 内:一个 Partition 只能被一个 Consumer 消费(不重复)
不同 Consumer Group:各自独立消费,都能收到全量数据

🔍 Kafka 核心概念详解

概念技术含义说明
BrokerKafka 服务器节点,默认端口 9092一个集群有多个 Broker,数据分布在各节点上
Topic消息的逻辑分类如 order-events、user-behavior、access-log
Partition一个 Topic 分成多个 Partition分布在不同 Broker 上,实现并行读写。Partition 数 = 最大并行消费数
Offset每条消息在 Partition 中的唯一编号(0, 1, 2, ...)单调递增,消费者记住 Offset 就知道消费到哪了
Replica每个 Partition 有多个副本(Leader + Followers)保证高可用,Leader 挂了 Follower 顶上
Leader/Follower读写都走 Leader,Follower 只负责同步Leader 挂了,ISR 中最新的 Follower 自动成为新 Leader
Consumer Group同组消费者共同消费一个 Topic 的所有 Partition组内互不重复,不同组各自独立消费全量数据
ZooKeeper / KRaft管理集群元数据(哪些 Broker 活着、谁是 Leader)新版 Kafka 用 KRaft 替代 ZooKeeper

📊 Partition 的数据结构

每个 Partition 就是一个追加写入的日志文件(Append-Only Log):

Partition 0 — order-events
OffsetTimestampKeyValue
012:00:01order-1001{"product":"无线耳机","qty":2}
112:00:03order-1002{"product":"蓝牙音箱","qty":1}
212:00:05order-1003{"product":"充电宝","qty":3}
312:00:08order-1004{"product":"数据线","qty":1}
Write Pointer →新消息追加到末尾,Offset 单调递增,永不修改
Partition 的三大特性:
1️⃣ 有序性:同一个 Partition 内消息严格按 Offset 顺序,先来先服务
2️⃣ 追加写入:只能在末尾添加,不能修改/删除历史消息(顺序 IO → 超级快!)
3️⃣ 分片并行:不同 Partition 可以并行读写,3 个 Partition = 3 倍吞吐

🖥️ 消息发到哪个 Partition?— 分区策略

策略规则适用场景
指定 Partition 直接指定 partition 号 特殊需求
按 Key Hash hash(key) % partition数 最常用:保证同一个 key 的消息进同一个 Partition,从而保证有序
Round Robin 无 Key 时轮询分配 无序要求、均衡负载
自定义分区器 实现 Partitioner 接口 业务特殊需求(如 VIP 用户单独分区)
// Java Producer — 指定 Key,自动按 Hash 分区 ProducerRecord<String, String> record = new ProducerRecord<>( "order-events", // topic "order-1001", // key → hash(key) % 3 决定进哪个 partition "{"product":"无线耳机","qty":2}" // value ); producer.send(record, (metadata, ex) -> { System.out.println("分区=" + metadata.partition() + ", offset=" + metadata.offset()); }); // 输出: 分区=1, offset=42 (hash("order-1001") % 3 = 1)

7. Kafka 核心机制 — Consumer Group / 消息顺序 / Exactly-Once

👥 7.1 Consumer Group — 消费者如何分工

核心规则:同一个 Consumer Group 内,一个 Partition 只能被一个 Consumer 消费。不同 Group 之间互不影响。

Topic: order-events(3 个 Partition)
Consumer Group A(3个消费者)
C1
→ P0
C2
→ P1
C3
→ P2
✅ 完美分配,每人一个
Consumer Group B(1个消费者)
C4
→ P0 + P1 + P2
⚠️ 消费者不够,一人消费3个
Consumer Group C(5个消费者)
C5
→ P0
C6
→ P1
C7
→ P2
❌ C8、C9 闲置(消费者 > Partition)
重要规则:
• 消费者数 = Partition 数 → 完美分配,每人一个 ✅
• 消费者数 < Partition 数 → 部分消费者消费多个 Partition
• 消费者数 > Partition 数 → 多出的消费者闲置,浪费资源 ❌
所以 Kafka 的 Partition 数决定了最大并行消费数!
// Spring Boot Kafka Consumer 配置 @Configuration public class KafkaConsumerConfig { @Bean public ConsumerFactory<String, String> consumerFactory() { Map<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service-group"); // Group ID props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); // 从最早开始消费 props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交 offset return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new StringDeserializer()); } } // 消费消息 @Component public class OrderConsumer { @KafkaListener(topics = "order-events", groupId = "order-service-group") public void handleOrder(ConsumerRecord<String, String> record) { log.info("收到消息: partition={}, offset={}, key={}, value={}", record.partition(), record.offset(), record.key(), record.value()); // 处理订单... } }

📏 7.2 消息顺序保证

常见误解:"Kafka 保证消息有序"——不完整!

Kafka 的顺序保证:
同一个 Partition 内:消息严格按写入顺序消费(Offset 单调递增)
不同 Partition 之间:不保证全局有序(P0 的消息可能比 P1 的消息先发后到)

解决方案:如果需要全局有序,只设 1 个 Partition(牺牲并行性)。
如果只需要同一个实体的操作有序(比如同一笔订单),把该订单的消息发到同一个 Partition(用 orderId 作为 Key,Kafka 会保证 hash(orderId) 一致的消息进同一个 Partition)。
为什么用 Key 可以保证有序?同一笔订单的"创建→支付→发货"这三个事件如果用 orderId 做 Key,它们会被 Hash 到同一个 Partition,在该 Partition 内按写入顺序排列,消费者按顺序消费就能保证业务操作的先后顺序正确。

🎯 7.3 Exactly-Once 语义 — 怎么保证"恰好一次"

消息系统有三种语义:

语义含义可能出现的问题实现难度
At Most Once最多一次(可能丢)消息发出后不等确认,如果传输中途失败则丢失简单
At Least Once至少一次(可能重复)消息确认前超时重发,导致消费者收到重复消息中等
Exactly Once恰好一次(不丢不重)需要 Producer 幂等 + Consumer 去重 + 事务保证复杂

Kafka 通过 幂等 Producer + 事务 + Consumer 端去重 实现 Exactly-Once:

// Kafka Producer — 开启幂等(防止生产端重复) props.put("enable.idempotence", "true"); // Kafka 自动去重 props.put("acks", "all"); // 所有副本确认后才返回成功 // Kafka 事务(Consumer → 处理 → Producer 端到端 Exactly-Once) @Transactional @KafkaListener(topics = "order-events") public void processAndForward(ConsumerRecord<String, String> record) { // 1. 处理消息 Order order = JSON.parseObject(record.value(), Order.class); orderService.process(order); // 2. 发送到下游 Topic kafkaTemplate.send("order-processed", order.getId(), "PROCESSED"); // 3. 手动提交 Offset(和数据库事务绑定) ack.acknowledge(); }

📝 7.4 Offset 管理

Consumer 怎么记住"我消费到哪了"?靠 Offset

Offset 提交方式:
1️⃣ 自动提交(enable.auto.commit=true):每隔 5s 自动提交。简单但可能丢消息或重复消费。
2️⃣ 手动同步提交(commitSync()):处理完消息后同步提交。可靠但阻塞。
3️⃣ 手动异步提交(commitAsync()):处理完消息后异步提交。性能好但可能提交失败。
4️⃣ 最佳实践:手动提交 + 和数据库事务绑定(消费+入库+提交 Offset 三者原子化)。
// 手动提交 Offset(推荐) @KafkaListener(topics = "order-events") public void handleOrder(ConsumerRecord<String, String> record, Acknowledgment ack) { try { Order order = JSON.parseObject(record.value(), Order.class); orderService.process(order); // 处理业务 ack.acknowledge(); // 手动提交 Offset } catch (Exception e) { log.error("处理失败,不提交 offset,消息会重新消费", e); } }

8. Kafka 高可用 — 副本机制与故障转移

📐 副本机制(Replica)

每个 Partition 有多个副本:1 个 Leader + N 个 Follower

Topic: order-events, Partition 0(replication.factor=3)
Broker 1
Leader (P0)
负责读写
Offset: 0~999
←同步→
Broker 2
Follower (P0)
实时同步
Offset: 0~997
←同步→
Broker 3
Follower (P0)
实时同步
Offset: 0~996
Producer/Consumer 只和 Leader 交互。Follower 通过拉取(Fetch)保持同步。
如果 Broker 1 挂了,Broker 2(ISR 中最新的)自动成为新 Leader。
故障转移原理:所有读写都由 Leader 处理,Follower 只负责从 Leader 拉取数据保持同步。当 Leader 所在的 Broker 宕机时,Kafka 从 ISR(同步副本集)中选出数据最新的 Follower 作为新 Leader,继续提供读写服务。这个过程对 Producer 和 Consumer 透明——它们只需重连新 Leader 即可。

🔍 ISR(In-Sync Replicas)— 同步副本集

ISR = 和 Leader 保持同步的副本集合。Follower 如果落后太多(超过 replica.lag.time.max.ms),会被踢出 ISR。

ISR 关键配置:
min.insync.replicas=2:至少 2 个副本写入成功才算成功(配合 acks=all)
• 如果 ISR 中只剩 1 个(只有 Leader),这条消息仍然可以写入,但降低了可靠性
最佳实践:replication.factor=3, min.insync.replicas=2, acks=all → 可以容忍 1 台机器故障
// Producer 配置 — 确保消息写入所有 ISR props.put("acks", "all"); // 等待所有 ISR 副本确认(最可靠,最慢) // acks=0: 不等确认(最快,可能丢) // acks=1: 等 Leader 确认(中等) // acks=all: 等所有 ISR 确认(最可靠) // Topic 配置 // kafka-topics.sh --create --topic order-events \ // --partitions 3 --replication-factor 3 \ // --config min.insync.replicas=2
# 查看 Topic 详细信息 $ kafka-topics.sh --describe --topic order-events --bootstrap-server localhost:9092 Topic: order-events PartitionCount: 3 ReplicationFactor: 3 Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3 Topic: order-events Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1 Topic: order-events Partition: 2 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2 ✓ 3 个 Partition,每个有 3 个副本,ISR 全部同步

⚡ Kafka 性能优化

优化方向配置/方法原理
批量发送batch.size=16384
linger.ms=5
攒一批消息再发,减少网络请求次数
压缩compression.type=lz4消息压缩后传输,减少带宽
批量消费max.poll.records=500一次拉 500 条批量处理
页缓存OS 级别自动Kafka 利用操作系统的 Page Cache,不自己管缓存
零拷贝OS 级别自动sendfile() 直接从文件到网卡,不经过用户空间
分区数Partition 数 = 期望并行度更多 Partition = 更多并行,但也更多文件句柄

9. RocketMQ 概览 — 阿里出品的企业级消息队列

核心定位:RocketMQ 是阿里开源的消息队列,专为电商、金融等对消息可靠性要求极高的场景设计。

架构:Producer → NameServer(路由注册中心,告诉 Producer/Consumer 哪个 Broker 有哪个 Topic)→ Broker(消息存储服务器,Master 负责读写,Slave 负责备份)→ Consumer。

三大特色:事务消息(下订单和发消息要么同时成功要么同时失败)、延迟消息(原生支持!不用像 RabbitMQ 那样 TTL+DLX 拼凑)、消息过滤(按 Tag 过滤)。

📐 RocketMQ 架构图

Producer
订单服务
①查询
NameServer
路由注册中心
②发消息
Broker-Master
存储消息
处理读写
Broker-Slave
同步复制
故障备份
③推送/拉取
Consumer
库存服务
NameServer:无状态的路由注册中心(类似注册中心),Producer/Consumer 通过它找到 Broker 地址
Broker:消息存储服务器,Master 负责读写,Slave 负责备份
Topic + Tag:Topic 是一级分类,Tag 是二级分类(如 Topic=ORDER, Tag=create/cancel)

🌟 RocketMQ 特色功能

1. 延迟消息(原生支持)

不用像 RabbitMQ 那样拼 TTL + DLX。RocketMQ 原生支持 18 个延迟级别(1s/5s/10s/30s/1m/.../2h)。

// RocketMQ 延迟消息 Message msg = new Message("ORDER_TOPIC", "create", "{"orderId":1001}".getBytes()); msg.setDelayTimeLevel(16); // 第16级 = 30分钟 producer.send(msg);

2. 事务消息

本地事务(扣库存)和发消息(通知积分服务)原子化——要么都成功,要么都回滚。

// 事务消息 TransactionMQProducer producer = new TransactionMQProducer("order_group"); producer.setTransactionListener(new TransactionListener() { // 1. 执行本地事务 public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { orderService.createOrder((Order) arg); // 本地事务:创建订单 return LocalTransactionState.COMMIT_MESSAGE; // 提交消息 } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚消息 } } // 2. 回查本地事务状态(如果长时间没收到确认) public LocalTransactionState checkLocalTransaction(MessageExt msg) { Order order = orderMapper.selectById(getOrderId(msg)); return order != null ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE; } });

3. 消息过滤(Tag)

// Producer — 带 Tag 发送 Message msg = new Message("ORDER_TOPIC", "create", body); // ↑ Tag // Consumer — 按 Tag 过滤订阅 consumer.subscribe("ORDER_TOPIC", "create || cancel"); // 只收 create 和 cancel

4. 消息重试 & 死信

RocketMQ 消费失败自动重试:
• 默认重试 16 次:10s, 30s, 1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 9m, 10m, 20m, 30m, 1h, 2h
• 16 次仍然失败 → 进入死信 Topic(%DLQ%ConsumerGroup)
• 比 RabbitMQ 需要自己拼 DLX 方便多了

10. MQ 对比选型 — RabbitMQ vs Kafka vs RocketMQ

维度RabbitMQ 🐰Kafka 🚀RocketMQ 📦
开发语言 Erlang Scala + Java Java
模型 Exchange → Queue Topic → Partition Topic → Queue
吞吐量 万级/s 百万级/s 十万级/s
延迟 微秒级 毫秒级 毫秒级
消息可靠性 高(ACK + 持久化) 高(副本 + ISR) 非常高(事务消息)
消息顺序 单 Queue 有序 单 Partition 有序 单 Queue 有序
延迟消息 TTL + DLX 拼凑 不支持(需外部) 原生支持(18级)
事务消息 不支持 支持(Exactly-Once) 原生支持(半消息)
消息回溯 不支持(消费即删) 支持(按 Offset 重置) 支持(按时间戳)
管理界面 自带 Web UI 需第三方(CMAK) 自带 Dashboard
社区生态 成熟,多语言客户端 极其活跃,大数据生态 阿里生态,中文文档好
核心设计 消息代理模型
消费后删除
追加日志模型
保留历史可回放
混合模型
支持事务+延迟
适合场景 业务消息、路由复杂
中小规模
大数据、日志、流处理
超高吞吐
电商、金融、订单
对可靠性要求极高

🎯 选型决策图

你需要消息队列做什么?

├─ 业务解耦(订单→库存→积分)→ RabbitMQRocketMQ

├─ 日志收集 / 大数据分析Kafka

├─ 秒杀削峰(超高吞吐) → Kafka

├─ 延迟消息(30分钟取消订单)→ RocketMQ(原生支持)

├─ 事务消息(下单+发消息原子化)→ RocketMQ

├─ 复杂路由(5种Exchange模式)→ RabbitMQ

└─ 小团队 / 快速上手RabbitMQ(自带管理界面,文档丰富)

11. 消息可靠性全链路 — 怎么保证消息不丢?

全链路可靠性三段论:保证消息不丢需要覆盖消息生命周期的三个阶段:
生产端:Producer 发消息后必须确认 Broker 确实收到了(Publisher Confirm)
Broker 端:消息到达后必须持久化到磁盘 + 多副本同步(防止单机故障)
消费端:Consumer 必须处理完业务逻辑后才发 ACK(防止处理失败但消息已被删除)
① 生产端
Confirm / Retries
确保消息到达 MQ
② MQ 端
持久化 + 副本
确保消息不丢
③ 消费端
手动 ACK
确保处理完才确认

① 生产端可靠性(RabbitMQ 为例)

// Spring Boot 配置 — 生产端可靠性 spring.rabbitmq.publisher-confirm-type=correlated // 异步 Confirm 回调 spring.rabbitmq.publisher-returns=true // 消息无法路由时回调 spring.rabbitmq.template.mandatory=true // 强制路由,无法路由时 ReturnCallback // 自定义 Confirm 回调 @Component public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback { @Override public void confirm(CorrelationData data, boolean ack, String cause) { if (ack) { log.info("消息投递成功: {}", data.getId()); } else { log.error("消息投递失败: {}, 原因: {}", data.getId(), cause); // 重发或记录到数据库,后续补偿 } } }

② MQ 端可靠性

// RabbitMQ:Exchange + Queue + Message 三重持久化(见第 4.5 节) // Kafka:acks=all + min.insync.replicas=2(见第 8 节)

③ 消费端可靠性

// Spring Boot — 手动 ACK 模式 spring.rabbitmq.listener.simple.acknowledge-mode=manual spring.rabbitmq.listener.simple.prefetch=1 // 一次只发 1 条 @Component public class OrderConsumer { @RabbitListener(queues = "order_queue") public void handleOrder(String msg, Channel ch, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { try { Order order = JSON.parseObject(msg, Order.class); orderService.process(order); // 业务处理 ch.basicAck(tag, false); // 成功 → ACK log.info("订单处理成功: {}", order.getId()); } catch (Exception e) { ch.basicNack(tag, false, true); // 失败 → NACK + 重新入队 log.error("订单处理失败,重新入队", e); } } }
✅ 三重保障下的完整链路
1. Producer 发消息 → Broker Confirm 确认 → 消息已到达 MQ ✅
2. MQ 持久化到磁盘 + 多副本同步 → 机器重启消息不丢 ✅
3. Consumer 取消息 → 处理业务 → 手动 ACK → MQ 才删除消息 ✅
4. Consumer 处理失败 → NACK → 消息重新入队 → 其他消费者处理 ✅
5. 多次失败 → 进入死信队列 → 人工介入 ✅

12. 幂等性方案 — 怎么保证"重复消费不出错"?

为什么需要幂等性?在分布式消息系统中,消息重复几乎不可避免——网络抖动导致 ACK 丢失、Consumer 重启后从旧 Offset 重新消费、Producer 超时重发等。如果没有幂等性保障,同一条消息被消费两次就会导致业务错误(扣两次库存、加两次积分)。幂等性的目标是:同一条消息消费一次和消费多次的结果完全一致

方案 1:数据库唯一约束

// 利用数据库唯一索引去重 @Transactional public void processOrder(Order order) { // 1. 检查是否已处理(幂等表) IdempotentRecord record = idempotentMapper.selectById(order.getId()); if (record != null) { log.info("订单{}已处理,跳过", order.getId()); return; // 直接返回,不重复处理 } // 2. 记录幂等(利用唯一索引,并发时只有一个成功) idempotentMapper.insert(new IdempotentRecord(order.getId())); // 3. 业务处理 stockService.deduct(order.getProductId(), order.getQty()); }

方案 2:Redis SETNX

// 利用 Redis 的 SETNX(Set if Not eXists)去重 public boolean isDuplicate(String orderId) { Boolean success = redisTemplate.opsForValue() .setIfAbsent("idempotent:" + orderId, "1", 24, TimeUnit.HOURS); // 24小时过期 return !success; // false = 第一次,true = 重复 } public void processOrder(Order order) { if (isDuplicate(order.getId())) { log.info("重复消息,跳过"); return; } // 正常处理... }

方案 3:状态机(乐观锁)

// 利用订单状态流转来保证幂等 @Update("UPDATE order SET status = 'PAID' WHERE id = #{id} AND status = 'UNPAID'") int payOrder(String id); // 返回 1 = 更新成功(第一次),返回 0 = 已经是 PAID(重复,跳过)
三种方案对比:
• 数据库唯一约束:最可靠,但有数据库开销。适合关键业务(支付)。
• Redis SETNX:高性能,但 Redis 宕机可能失效。适合高并发场景。
• 状态机:最优雅,但只适用于有状态流转的业务(订单状态、库存扣减)。
推荐组合:Redis SETNX 快速去重 + 数据库唯一约束兜底。

13. 消息积压处理 — 生产速率远超消费速率时怎么办?

什么是消息积压?当 Producer 发送消息的速率远超 Consumer 消费的速率时,消息会在 Broker 中大量堆积。如果积压持续增长,会导致:内存溢出、消息延迟增大、磁盘空间耗尽、消费者永远追不上。

常见原因:① 消费者处理逻辑变慢(如依赖的数据库慢查询)② 消费者实例宕机未及时发现 ③ 突发流量(秒杀、促销)④ 消费者代码 bug 导致大量 NACK 重试。

🔍 如何发现消息积压?

Grafana — RabbitMQ 监控面板
Messages Ready
52,431
↑ 比昨日 +4800%
Consume Rate
230/s
正常
Publish Rate
5,000/s
↑ 秒杀活动!
⚠️ 告警:队列 order_queue 消息积压 52,431 条,生产速率 5000/s 远超消费速率 230/s!

🛠️ 应急处理方案

方案 1:快速扩容消费者(最常用)

// Kafka:增加 Consumer 实例(注意不要超过 Partition 数!) // 如果 Partition=3,最多 3 个 Consumer 有用 // 需要先扩 Partition: # 将 Partition 从 3 扩到 10 kafka-topics.sh --alter --topic order-events --partitions 10 --bootstrap-server localhost:9092 // 然后部署更多 Consumer 实例(K8s 扩副本) // kubectl scale deployment order-consumer --replicas=10

方案 2:临时转发 + 批量快速消费

// 写一个"快速消费"程序:只入库不处理业务,先快速消费掉积压消息 @Component public class FastConsumer { @RabbitListener(queues = "order_queue", concurrency = "20") public void fastConsume(String msg, Channel ch, @Header long tag) { // 快速写入数据库(批量 INSERT) batchInsertMapper.insert(JSON.parseObject(msg, Order.class)); ch.basicAck(tag, false); } } // 等积压消完,再用定时任务慢慢处理入库的订单

方案 3:消息 TTL + 死信兜底

给队列设 TTL(比如 1 小时),超时的消息自动进入死信队列。正常队列消费积压,死信队列后续人工处理。
这样可以保证正常队列不会无限膨胀导致内存溢出。
千万不要做:直接清空队列(消息全丢)!除非确认这些消息可以丢弃。正确做法是先扩容消费,积压的消息慢慢消化。

14. 最终一致性 — 分布式事务的"妥协方案"

强一致性 vs 最终一致性:
强一致性(不用 MQ):订单服务调用库存、积分、短信服务,全部成功才返回"下单成功"。任何一个下游服务故障,整个下单失败。用户等待时间长,系统可用性差。
最终一致性(用 MQ):订单服务发消息后立即返回"下单成功"。下游服务异步处理——积分可能晚 1 秒到账,短信可能晚 2 秒发送。用户感知"下单成功"了,数据"最终"会一致。这是互联网公司处理分布式事务的主流方案。

📐 最终一致性方案对比

方案原理优点缺点适合场景
本地消息表 业务操作和消息记录在同一个数据库事务中,后台线程扫描消息表发送到 MQ 实现简单,不需要特殊 MQ 支持 消息表会膨胀,需要定时清理 最常用,适合大多数业务
事务消息
(RocketMQ)
MQ 原生支持半消息机制,本地事务和消息提交原子化 不需要消息表,代码简洁 只有 RocketMQ 原生支持 用 RocketMQ 的项目
TCC / Saga 分布式事务框架(Seata),Try-Confirm-Cancel 强一致性 复杂度高,性能差 金融级场景

📝 本地消息表方案详解(最实用!)

① 订单服务
1. 插入订单
2. 插入消息表
(同一事务)
② 定时任务
扫描消息表
发送到 MQ
③ MQ
消息队列
Broker
④ 积分服务
消费消息
加积分
// ===== 第一步:业务操作 + 写消息表(同一事务)===== @Transactional public void createOrder(Order order) { // 1. 插入订单 orderMapper.insert(order); // 2. 插入消息表(和订单在同一个事务里) OutboxMessage msg = new OutboxMessage(); msg.setId(UUID.randomUUID().toString()); msg.setTopic("order-created"); msg.setKey(order.getId()); msg.setBody(JSON.toJSONString(order)); msg.setStatus("PENDING"); // 待发送 msg.setRetryCount(0); msg.setCreateTime(new Date()); outboxMapper.insert(msg); // 事务提交后:订单和消息要么都成功,要么都回滚 } // ===== 第二步:定时任务扫描消息表,发送到 MQ ===== @Scheduled(fixedDelay = 5000) // 每 5 秒扫描一次 public void scanAndPublish() { List<OutboxMessage> messages = outboxMapper.selectPending(); for (OutboxMessage msg : messages) { try { rabbitTemplate.convertAndSend(msg.getTopic(), msg.getKey(), msg.getBody()); msg.setStatus("SENT"); // 标记为已发送 outboxMapper.updateById(msg); } catch (Exception e) { msg.setRetryCount(msg.getRetryCount() + 1); if (msg.getRetryCount() > 5) { msg.setStatus("FAILED"); // 超过 5 次标记为失败 } outboxMapper.updateById(msg); } } } // ===== 第三步:消费者正常消费 ===== @RabbitListener(queues = "points_queue") public void addPoints(String msg) { Order order = JSON.parseObject(msg, Order.class); pointsService.addPoints(order.getUserId(), order.getAmount()); }
✅ 完整流程
1. 用户下单 → 订单入库 + 消息入表(同一事务,原子性 ✅)
2. 定时任务扫描 → 发送到 MQ → 标记已发送
3. 积分服务消费 → 加积分 → ACK
4. 如果 MQ 发送失败 → 重试(最多 5 次)→ 仍失败则标记 FAILED
5. FAILED 的消息 → 人工排查或补偿脚本处理

效果:即使 MQ 短暂不可用,消息也不会丢失(在消息表里)。等 MQ 恢复后定时任务自动补发。

🔍 最终一致性的补偿机制

即使有了本地消息表,也需要补偿机制兜底:

  1. 定时补偿:定时扫描长时间 SENT 但下游未确认的消息,重新发送
  2. 对账:每天凌晨对比订单表和积分表,找出不一致的数据
  3. 人工处理:FAILED 状态的消息,运营人员在后台手动处理
  4. 监控告警:MQ 积压 > 阈值、消费失败率 > 1% 时告警
总结:最终一致性 = 本地消息表 + 定时任务 + 重试 + 补偿 + 告警
不追求"实时一致",而是保证"最终"(通常几秒到几分钟内)数据会一致。
这是互联网公司处理分布式事务的主流方案。

📋 全篇总结 — 消息队列知识地图

消息队列架构知识体系

├─ 为什么需要 MQ:解耦、异步、削峰(第 1 节)

├─ RabbitMQ

│ ├─ 架构:Exchange → Queue → Consumer(第 2 节)

│ ├─ 5 种模式:Simple / Work / Fanout / Direct / Topic(第 3 节)

│ ├─ 高级:ACK / TTL / 死信 / 延迟 / 持久化 / Confirm(第 4 节)

│ └─ 集群:镜像队列 / 仲裁队列(第 5 节)

├─ Kafka

│ ├─ 架构:Broker / Topic / Partition / Offset(第 6 节)

│ ├─ 核心:Consumer Group / 顺序 / Exactly-Once(第 7 节)

│ └─ 高可用:副本 / ISR / 性能优化(第 8 节)

├─ RocketMQ:延迟消息 / 事务消息 / Tag 过滤(第 9 节)

├─ 对比选型:RabbitMQ vs Kafka vs RocketMQ(第 10 节)

├─ 可靠性:生产端 Confirm + MQ 持久化 + 消费端 ACK(第 11 节)

├─ 幂等性:唯一约束 / Redis SETNX / 状态机(第 12 节)

├─ 消息积压:扩容 / 转发 / 快速消费 / TTL 兜底(第 13 节)

└─ 最终一致性:本地消息表 + 定时任务 + 补偿(第 14 节)