← 返回场景决策

📨 消息队列场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 8 大核心场景全解析

要不要用MQ Kafka vs RabbitMQ vs RocketMQ 消费模式 幂等 顺序消费 死信队列 延迟队列 消息积压

1. 要不要用消息队列?— 引入决策

MQ = 快递中转站 寄件人和收件人不用见面,中转站暂存、分发
什么时候用 MQ
  • 异步处理:注册后发短信/邮件 — 不用等短信发完才返回成功
  • 解耦:订单创建后通知库存/物流/积分 — 订单服务不用知道下游有多少
  • 削峰填谷:秒杀请求瞬间涌入 — MQ 缓冲,后端匀速消费
  • 最终一致性:分布式事务的替代方案 — 本地消息表 + MQ
  • 广播通知:配置变更通知所有节点 — 发布/订阅模式
什么时候不用 MQ
  • 强一致性:扣款必须立即知道结果 — MQ 是最终一致,不是强一致
  • 实时性要求:毫秒级延迟不可接受 — MQ 有网络+序列化开销
  • 简单同步调用:查个接口返回个结果 — 直接 HTTP/RPC 更简单
  • 数据量极小:一天就几条消息 — 不值得上 MQ
  • 团队没有运维能力:MQ 挂了=业务停 — 没有能力运维就别上

📊 引入 MQ 决策表

场景不用MQ用MQ推荐
注册发短信同步等待100ms异步发,秒返回✅ MQ
订单→库存→积分3个服务直接调用订单发事件,各服务自己消费✅ MQ
秒杀数据库直接扛MQ缓冲削峰✅ MQ
查询商品详情直接查数据库-❌ 直接查
支付扣款同步扣异步扣(可能不一致)❌ 同步

2. Kafka vs RabbitMQ vs RocketMQ — 选型决策

RabbitMQ = 邮局 可靠投递,路由灵活,信件级别
Kafka = 传送带 超快吞吐,大批量数据流水线
RocketMQ = 国企快递 可靠+功能全+国产,阿里出品

📊 MQ 选型决策表

维度RabbitMQKafkaRocketMQ
语言ErlangScala/JavaJava
吞吐量万级/秒百万级/秒十万级/秒
延迟微秒级毫秒级毫秒级
消息可靠性高(确认机制)较高高(回查机制)
消息顺序队列内有序Partition内有序Queue内有序
事务消息❌(精确一次较难)✅ 原生支持
延迟消息插件✅ 原生支持
适合场景中小项目/复杂路由大数据/日志/流处理电商/金融/交易
运维难度中(ZK/KRaft)

3. 消费模式 — 推 vs 拉

📊 推模式 vs 拉模式决策表

模式谁主动优点缺点代表
推模式 (Push)Broker 推给 Consumer实时性好消费者可能撑不住RabbitMQ
拉模式 (Pull)Consumer 找 Broker 拉消费者按自己节奏有延迟(轮询间隔)Kafka
长轮询Consumer 请求,Broker 有数据才返回兼顾实时和流量控制实现复杂RocketMQ

4. 幂等消费 — 重复消息处理决策

不幂等 = 同一笔订单扣了两次库存 消息重复投递 = 业务重复执行
幂等 = 同一笔订单扣多少次都只扣一次 重复消息 = 不重复执行
幂等消费方案
  • 唯一ID + 去重表:消费前检查消息ID是否已处理 — 最通用
  • 业务幂等键:订单号/支付流水号做唯一约束 — 数据库层面防重
  • Redis SETNX:消息ID做key,SET成功才处理 — 高性能去重
  • 状态机:订单只有"待支付→已支付→已发货"单向流转 — 重复消费无副作用

💻 唯一ID + 去重表实现

📁 order-service
📂 src/main/java
📂 com.shop.order.consumer
📄 OrderConsumer.java
📂 com.shop.order.mapper
📄 DeduplicationMapper.java
@Component @RequiredArgsConstructor public class OrderConsumer { private final DeduplicationMapper dedupMapper; private final OrderMapper orderMapper; @RabbitListener(queues = "order.queue") public void handleOrder(Message message) { String msgId = message.getMessageProperties().getMessageId(); // 1. 检查是否已处理 if (dedupMapper.existsById(msgId)) { log.info("重复消息,跳过: {}", msgId); return; // 幂等:重复消息不重复执行 } // 2. 业务处理 OrderEvent event = JSON.parseObject( new String(message.getBody()), OrderEvent.class); orderMapper.updateStatus(event.getOrderId(), "PAID"); // 3. 记录已处理(同一事务) dedupMapper.insert(new DedupRecord(msgId, LocalDateTime.now())); } } // 去重表DDL // CREATE TABLE msg_dedup ( // msg_id VARCHAR(64) PRIMARY KEY, -- 消息唯一ID // created_at DATETIME // );
面试金句 幂等消费三步法(Check→Process→Record)必须在同一数据库事务中完成,否则 Check 和 Record 之间存在窗口期,并发重复消息可能同时通过检查。msg_id PRIMARY KEY 是最后一道防线:即使事务隔离失败,INSERT 也会因主键冲突而报错,避免脏数据。

💻 Redis SETNX 高性能去重

📁 order-service
📂 src/main/java
📂 com.shop.order.service
📄 RedisDedupService.java
@Component public class RedisDedupService { @Autowired private StringRedisTemplate redis; /** * 幂等检查:SETNX + 过期时间 * @return true=首次处理 false=重复消息 */ public boolean tryAcquire(String msgId) { Boolean success = redis.opsForValue() .setIfAbsent( "dedup:" + msgId, // key "1", // value Duration.ofDays(7) // 7天过期自动清理 ); return Boolean.TRUE.equals(success); } }
面试金句 SETNX(setIfAbsent) 是 Redis 原子操作:Key 不存在时才设置成功,利用 Redis 单线程模型保证并发安全。相比数据库去重表,Redis 方案延迟在 1ms 以内,适合高吞吐场景;但 Redis 重启可能丢失 Key,因此 关键业务仍需 DB 去重表兜底(Redis 做第一道快速过滤,DB 做最终一致性保障)。

5. 顺序消费 — 消息有序性决策

什么时候需要顺序消费
  • 订单状态变更:创建→支付→发货 → 消费顺序不能乱
  • 数据同步:MySQL Binlog → ES,binlog顺序必须保证
  • 同一用户的操作:用户A的操作按顺序,用户B可以并行

📊 顺序消费方案

方案原理优点缺点
单Queue单Consumer一个队列一个人消费绝对有序吞吐量极低
相同Key路由到同一QueueorderId hash到固定队列局部有序+并行Queue不均衡
业务层排序消费后按时间戳重排灵活复杂、有延迟

💻 RocketMQ 顺序消息

📁 order-service
📂 src/main/java
📂 com.shop.order.mq
📄 OrderMessageProducer.java
📄 OrderConsumer.java
// 发送时指定队列选择器 — 同一订单ID的消息进同一Queue sendResult = producer.send(msg, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Long orderId = (Long) arg; int index = (int) (orderId % mqs.size()); return mqs.get(index); // 同一orderId永远进同一Queue } }, orderId); // 消费时用 OrderlyMessageListener — 单Queue单线程消费 @RocketMQMessageListener( topic = "order-topic", consumerGroup = "order-consumer", consumeMode = ConsumeMode.ORDERLY // ← 顺序消费 ) public class OrderConsumer implements RocketMQListener<String> { ... }
面试金句 Hash 路由顺序消费是分布式 MQ 的核心设计:Producer 用 orderId % Queue数 将同一业务实体的消息哈希到固定 Queue,Consumer 用 ORDERLY 模式单线程消费每个 Queue,从而实现"全局无序、局部有序"——同一订单的状态变更有序,不同订单可并行处理,兼顾顺序性和吞吐量。

6. 死信队列 — 消费失败处理决策

死信 = 无法投递的退信 消费重试N次还是失败→转到死信队列→人工/定时任务处理

💻 RabbitMQ 死信配置

📁 order-service
📂 src/main/java
📂 com.shop.order.config
📄 RabbitMQConfig.java
📂 com.shop.order.consumer
📄 DeadLetterHandler.java
// 1. 定义死信交换机和队列 @Bean public DirectExchange dlxExchange() { return new DirectExchange("order.dlx"); } @Bean public Queue dlqQueue() { return QueueBuilder.durable("order.dlq").build(); } // 2. 业务队列绑定死信 @Bean public Queue orderQueue() { return QueueBuilder.durable("order.queue") .withArgument("x-dead-letter-exchange", "order.dlx") .withArgument("x-dead-letter-routing-key", "order.dlq") .build(); } // 3. 消费重试3次后进死信 // application.yml // spring: // rabbitmq: // listener: // simple: // retry: // max-attempts: 3 // enabled: true // 4. 死信消费者 — 人工处理或告警 @RabbitListener(queues = "order.dlq") public void handleDeadLetter(Message message) { log.error("死信消息: {}", new String(message.getBody())); // 发送告警 / 存入数据库 / 人工处理界面 }
面试金句 Dead Letter Exchange (DLX) 是 RabbitMQ 的"消息太平间":消息被拒绝(basic.nack)、TTL 过期、队列满时自动转发到绑定的 DLX。x-dead-letter-exchange 参数在队列声明时绑定,Broker 自动路由,生产者无感知。死信消费者负责告警+记录+人工干预,是消息可靠性的最后一道防线

7. 延迟队列 — 定时消息决策

什么时候用延迟队列
  • 订单超时取消:下单30分钟未支付自动取消 — 延迟30分钟
  • 预约提醒:会议开始前15分钟提醒
  • 重试延迟:操作失败后5秒/30秒/5分钟递增重试
  • 定时推送:每天早上9点推送日报

📊 延迟方案对比

方案精度适合缺点
RabbitMQ TTL + DLX秒级RabbitMQ用户相同TTL的队头阻塞问题
RocketMQ 延迟消息秒级RocketMQ用户固定延迟等级(1s/5s/10s/30s/1m/...)
Redis Key过期秒级轻量场景不可靠(Redis重启丢)
定时任务扫描分钟级简单可靠延迟精度差、数据库压力大
时间轮毫秒级高性能场景实现复杂

💻 RabbitMQ 延迟队列(TTL + DLX)

📁 order-service
📂 src/main/java
📂 com.shop.order.config
📄 DelayQueueConfig.java
📂 com.shop.order.consumer
📄 OrderTimeoutHandler.java
// 延迟交换机 + 延迟队列 @Bean public Queue delayQueue() { return QueueBuilder.durable("order.delay") .withArgument("x-message-ttl", 30 * 60 * 1000) // 30分钟TTL .withArgument("x-dead-letter-exchange", "order.exchange") .withArgument("x-dead-letter-routing-key", "order.queue") .build(); } // 发送延迟消息 rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", orderEvent); // 30分钟后消息过期 → 进入死信 → 被order.queue消费 // 消费者检查订单状态:还是"待支付"→ 取消订单 @RabbitListener(queues = "order.queue") public void handleOrderTimeout(OrderEvent event) { Order order = orderMapper.selectById(event.getOrderId()); if (order != null && order.getStatus().equals("PENDING")) { order.setStatus("CANCELLED"); orderMapper.updateById(order); // 恢复库存 } }
面试金句 TTL + DLX 延迟队列利用 RabbitMQ 的消息过期机制:消息在 delay 队列中存活 30 分钟,过期后自动变成死信转发到业务队列。⚠️ 队头阻塞陷阱:RabbitMQ 按队列头部消息检查 TTL,如果队头消息 TTL 是 60 分钟、后面的消息 TTL 是 5 分钟,后面的消息要等 60 分钟后才会被检查——所以不同延迟级别必须用不同队列。

💻 RocketMQ 延迟消息

📁 order-service
📂 src/main/java
📂 com.shop.order.mq
📄 DelayMessageProducer.java
// RocketMQ 支持固定延迟等级 // 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h Message msg = new Message("order-topic", "tag", JSON.toJSONBytes(orderEvent)); msg.setDelayTimeLevel(16); // 16 = 30分钟 producer.send(msg); // 30分钟后消费者才会收到这条消息
面试金句 RocketMQ 延迟消息原生支持 18 个固定延迟等级,底层用时间轮+延迟队列实现,消息发送后存储在 SCHEDULE_TOPIC_XXXX 中,到期后重新投递到目标 Topic。相比 RabbitMQ TTL+DLX,无队头阻塞问题,支持任意延迟级别互不影响;但固定等级不够灵活(不支持任意时间),RocketMQ 5.0+ 才支持任意时刻延迟。

8. 消息积压 — 紧急处理决策

⚠️ 消息积压的典型原因
  • 消费太慢:消费者处理逻辑重 — 1条消息处理500ms
  • 消费者挂了:下游服务宕机 — 生产者还在发
  • 突发流量:大促/秒杀 — 瞬间涌入平时100倍的消息
  • 消费报错:某条消息导致消费者反复异常/重试 — 积压越来越大

📊 消息积压应急方案

方案操作适合风险
增加消费者扩容Consumer实例数消费慢、有机器
临时转发积压队列→新队列→批量消费积压严重
跳过非关键日志/通知类消息先丢弃非核心消息丢数据
优化消费逻辑批量插入代替逐条插入消费逻辑可优化

💻 积压紧急处理步骤

📁 order-service
📂 src/main/java
📂 com.shop.order.consumer
📄 BacklogFastConsumer.java
// Step 1: 先看积压量 // RabbitMQ: rabbitmqctl list_queues name messages // Kafka: kafka-consumer-groups --describe --group xxx // Step 2: 快速扩容消费者 // K8s: kubectl scale deployment order-consumer --replicas=10 // Step 3: 如果扩容也来不及 → 转发到新Topic批量消费 // 创建临时消费者,快速消费积压消息,转发到新Topic @RabbitListener(queues = "order.queue.backlog", concurrency = "20") public void fastConsume(Message message) { // 转发到新Topic,用批量消费者慢慢处理 rabbitTemplate.convertAndSend("order.batch.queue", message.getBody()); } // Step 4: 积压清完后恢复正常消费者数 // kubectl scale deployment order-consumer --replicas=2 // ⚠️ 预防措施 // 1. 监控队列深度,积压超阈值告警 // 2. 设置队列最大长度,防止磁盘打满 // 3. 压测确认消费能力
面试金句 消息积压四步应急预案:监控发现→快速扩容→转发分流→事后缩容。核心思想是用空间换时间——临时消费者 concurrency=20 快速消费积压队列,转发到新队列后由批量消费者异步处理。⚠️ RabbitMQ 中 Consumer 数不能超过 Queue 数(否则多余的 Consumer 空闲),Kafka 中 Consumer 数不能超过 Partition 数。

📨 消息队列场景决策 · 全栈资料库

← 返回场景决策