4. 幂等消费 — 重复消息处理决策
不幂等 = 同一笔订单扣了两次库存
→
消息重复投递 = 业务重复执行
幂等 = 同一笔订单扣多少次都只扣一次
→
重复消息 = 不重复执行
✅ 幂等消费方案
- 唯一ID + 去重表:消费前检查消息ID是否已处理 — 最通用
- 业务幂等键:订单号/支付流水号做唯一约束 — 数据库层面防重
- Redis SETNX:消息ID做key,SET成功才处理 — 高性能去重
- 状态机:订单只有"待支付→已支付→已发货"单向流转 — 重复消费无副作用
💻 唯一ID + 去重表实现
@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 高性能去重
@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路由到同一Queue | orderId hash到固定队列 | 局部有序+并行 | Queue不均衡 |
| 业务层排序 | 消费后按时间戳重排 | 灵活 | 复杂、有延迟 |
💻 RocketMQ 顺序消息
// 发送时指定队列选择器 — 同一订单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 死信配置
// 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)
// 延迟交换机 + 延迟队列
@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 延迟消息
// 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实例数 | 消费慢、有机器 | 低 |
| 临时转发 | 积压队列→新队列→批量消费 | 积压严重 | 中 |
| 跳过非关键 | 日志/通知类消息先丢弃 | 非核心消息 | 丢数据 |
| 优化消费逻辑 | 批量插入代替逐条插入 | 消费逻辑可优化 | 低 |
💻 积压紧急处理步骤
// 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 数。