第二期:高级特性与可靠性保障------消息不丢失的硬核全链路拆解
免责声明:本文中出现的"CloudMart"是一个虚构的电商教学系统,仅用于串联技术知识点,与任何真实公司或产品无关。
目录
- [开篇:CloudMart 的噩梦------一笔订单,两条重复扣款](#开篇:CloudMart 的噩梦——一笔订单,两条重复扣款)
- 理论速览:可靠性全链路三道防线
- [防线一:发送端------Confirm + Return](#防线一:发送端——Confirm + Return)
- 防线二:Broker------三层持久化
- [防线三:消费端------手动 ACK + 重试策略](#防线三:消费端——手动 ACK + 重试策略)
- [实战案例:CloudMart 可靠性落地](#实战案例:CloudMart 可靠性落地)
- 案例一:订单创建------保证生产端不丢消息
- [案例二:库存扣减------消费端幂等 + 手动 ACK](#案例二:库存扣减——消费端幂等 + 手动 ACK)
- [源码走读:Spring AMQP 可靠性核心实现](#源码走读:Spring AMQP 可靠性核心实现)
- [RabbitTemplate 的 Confirm 回调链路](#RabbitTemplate 的 Confirm 回调链路)
- [SimpleMessageListenerContainer 的 ACK 处理](#SimpleMessageListenerContainer 的 ACK 处理)
- [死信队列 DLX:消息的"最终归宿"](#死信队列 DLX:消息的"最终归宿")
- 三大来源详解
- [TTL:队列级 vs 消息级](#TTL:队列级 vs 消息级)
- [实战案例:CloudMart 订单超时自动取消](#实战案例:CloudMart 订单超时自动取消)
- 延迟队列:两种实现方案对比
- [实战案例:CloudMart 支付倒计时提醒](#实战案例:CloudMart 支付倒计时提醒)
- [消费端流量控制:prefetch 与重试](#消费端流量控制:prefetch 与重试)
- [实战案例:CloudMart 秒杀限流](#实战案例:CloudMart 秒杀限流)
- [事务机制 vs Confirm 模式](#事务机制 vs Confirm 模式)
- [横向对比:RabbitMQ vs Kafka 可靠性模型](#横向对比:RabbitMQ vs Kafka 可靠性模型)
- [面试追问:消息可靠性 15 连问](#面试追问:消息可靠性 15 连问)
- 可靠性验证清单
- 必背速查:可靠性配置清单
开篇:CloudMart 的噩梦------一笔订单,两条重复扣款
凌晨 2 点 14 分,CloudMart 支付系统告警响起。客服工单涌入:订单 ORD-20260606-1999 被扣了两次款,用户只下了一单。
初步排查发现三条链路断裂:
- 发送端 :支付服务调用
rabbitTemplate.convertAndSend()发送扣款消息。方法返回无异常,但 ConfirmCallback 收到的是 NACK------消息实际未到达 Broker 就被丢弃。支付服务没有校验 Confirm 结果,直接更新了支付状态为"已扣款"。 - Broker 侧 :扣款队列声明时没有设置
durable=true。凌晨 Broker 例行重启后,队列消失,积压的未消费消息全部丢失。重启后重新声明的队列是全新的,之前那条"假成功"对应的支付状态已写入 DB,形成脏数据。 - 消费端 :库存服务配置了
acknowledge-mode: auto。退款消息到达后,invokeListener()内部抛出InventoryRestoreException(Redis 连接超时),Spring 自动返回 NACK 并触发重试。但重试三次后 Redis 仍未恢复,消息被转入 DLX------库存恢复失败,订单却早已标记为"退款成功"。
根本原因 :三道防线全部失守------发送端没有用 Confirm + Return 闭环校验,Broker 没有开启三层持久化(尤其是 Queue 持久化),消费端依赖自动 ACK 而非手动控制。
这个场景就是面试中最经典的**"消息可靠性"问题的全貌**。接下来我们逐层拆解,把这三道防线讲透。
理论速览:可靠性全链路三道防线
RabbitMQ 的消息从发送到消费,会经过三个关键卡点。任何一个卡点出了问题,都可能导致消息丢失:

防线一:发送端------Confirm + Return
生产者发送消息给 Broker 时,默认情况下 basicPublish 是即发即忘(fire-and-forget) :方法返回不代表消息到达 Broker,更不意味着消息被持久化到了磁盘。要解决这个问题,需要引入 Publisher Confirm 和 Return 回调。
Publisher Confirm:生产者将 Channel 设为 Confirm 模式后,Broker 会对每一条消息给出回执------ACK(成功)或 NACK(失败)。Confirm 有三种使用策略:
| 策略 | 方式 | 吞吐量 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 单条同步 | channel.waitForConfirms() |
极低 | 高 | 几乎不用 |
| 批量同步 | channel.waitForConfirmsOrDie() |
中 | 中(整批失败重发) | 非关键业务 |
| 异步回调 | setConfirmCallback() |
高 | 高 | 推荐,生产首选 |
Return 回调 :当消息被 Broker 接收但无法路由到任何队列时,Broker 会将消息 Return 给生产者。必须设置 mandatory=true 才能触发。
java
// Spring Boot 配置:同时启用 Confirm 和 Return
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
// Broker 确认接收,更新本地消息表状态为"已发送"
messageLogService.updateStatus(correlationData.getId(), SendStatus.SENT);
} else {
// NACK:记录失败,触发重试或告警
log.error("消息发送NACK: id={}, cause={}", correlationData.getId(), cause);
messageLogService.updateStatus(correlationData.getId(), SendStatus.FAILED);
}
});
rabbitTemplate.setReturnsCallback(returned -> {
// 消息无法路由,Return 回来
log.error("消息被Return: msg={}, replyCode={}, replyText={}, exchange={}, routingKey={}",
returned.getMessage(), returned.getReplyCode(), returned.getReplyText(),
returned.getExchange(), returned.getRoutingKey());
messageLogService.recordReturn(returned);
});
关键面试点 :Confirm 的 ACK 是异步的 。convertAndSend() 方法返回后,ACK 可能还没回来。所以如果你的业务需要"确认发送成功后才做下一步",必须在 confirmCallback 里写后续逻辑,而不是在 convertAndSend() 之后。

防线二:Broker------三层持久化
即便消息成功到达 Broker,默认情况下 Broker 重启后消息也会丢失------因为默认 Exchange、Queue 和 Message 都不是持久化的。
三层持久化缺一不可:
| 层次 | 配置方式 | 丢失后果 |
|---|---|---|
| Exchange | ExchangeBuilder.durable(true) |
重启后交换机消失,消息无法路由 |
| Queue | QueueBuilder.durable(true) |
重启后队列消失,消息无处存放 |
| Message | deliveryMode=PERSISTENT |
重启后消息丢失(队列和交换机还在) |
java
// 正确的三层持久化声明
@Configuration
public class ReliableConfig {
@Bean
public DirectExchange orderExchange() {
return ExchangeBuilder.directExchange("cloudmart.order.ex")
.durable(true) // ① Exchange 持久化
.build();
}
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("cloudmart.order.queue") // ② Queue 持久化
.build();
}
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with("order.create");
}
}
// 发送消息时指定持久化
MessageProperties props = new MessageProperties();
props.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // ③ Message 持久化
Message msg = new Message(orderJson.getBytes(StandardCharsets.UTF_8), props);
rabbitTemplate.send("cloudmart.order.ex", "order.create", msg);
更简洁的方式 :使用 MessagePostProcessor 一行搞定:
java
rabbitTemplate.convertAndSend(ex, rk, order,
msg -> {
msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return msg;
});
⚠️ 面试陷阱 :设置
deliveryMode=PERSISTENT并不等于"写入磁盘"。Broker 有自己的刷盘策略------消息可能还在操作系统的 Page Cache 里,Broker 崩溃同样会丢。若需要更高的持久化保证,需要配合 镜像队列(Mirrored Queue) 或 Quorum Queue。

防线三:消费端------手动 ACK + 重试策略
消息到达消费者后,默认的自动 ACK 模式(acknowledge-mode: none)在消息发出后立即删除,消费失败也无法恢复。生产环境必须使用手动 ACK (manual)。
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动 ACK
retry:
enabled: true
max-attempts: 3
initial-interval: 1000ms
multiplier: 2.0 # 指数退避:1s → 2s → 4s
java
@RabbitListener(queues = "cloudmart.order.queue")
public void handleOrder(Message message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
Order order = objectMapper.readValue(message.getBody(), Order.class);
inventoryService.deduct(order); // 库存扣减
channel.basicAck(tag, false); // 手动确认:单条
} catch (BusinessException e) {
// 业务异常(如库存不足)→ 拒绝且不重入队列
channel.basicNack(tag, false, false);
} catch (Exception e) {
// 系统异常(网络抖动等)→ 拒绝且重新入队
channel.basicNack(tag, false, true);
}
}

三种 ACK 方法的语义必须精确区分:
| 方法 | 语义 | 应用场景 |
|---|---|---|
basicAck(tag, false) |
确认单条消息 | 正常处理完成 |
basicNack(tag, false, true) |
拒绝且重新入队 | 临时故障,稍后重试 |
basicNack(tag, false, false) |
拒绝且不重入 | 业务失败,走死信队列 |
basicReject(tag, false) |
拒绝单条 | 等价 basicNack(tag, false, false) |
实战案例:CloudMart 可靠性落地
案例一:订单创建------保证生产端不丢消息
CloudMart 的订单创建是一个典型的可靠性场景:用户下单后,订单服务需要将订单消息发送给库存服务和物流服务。要求零丢失。
架构设计:
- 创建本地消息表
msg_log,记录每一条待发送消息 - 使用 Confirm 异步回调更新消息状态
- 定时任务扫描失败消息,进行补偿重发
java
// 订单服务:发送订单创建消息
@Transactional
public void createOrder(OrderCreateDTO dto) {
// 1. 保存订单
Order order = orderRepository.save(dto.toOrder());
// 2. 记录消息日志(与订单在同一事务中)
MessageLog msgLog = new MessageLog();
msgLog.setMessageId(UUID.randomUUID().toString());
msgLog.setExchange("cloudmart.order.ex");
msgLog.setRoutingKey("order.create");
msgLog.setStatus(SendStatus.PENDING);
msgLog.setPayload(JSON.toJSONString(order));
messageLogRepository.save(msgLog);
// 3. 发送消息
CorrelationData correlationData = new CorrelationData(msgLog.getMessageId());
rabbitTemplate.convertAndSend("cloudmart.order.ex", "order.create",
order, correlationData);
}
java
// Confirm 回调:更新消息状态
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
String msgId = correlationData.getId();
if (ack) {
messageLogRepository.updateStatus(msgId, SendStatus.SENT);
} else {
messageLogRepository.updateStatus(msgId, SendStatus.FAILED);
log.error("消息发送失败 msgId={}, cause={}", msgId, cause);
}
});
// 补偿任务:每 5 分钟扫描失败消息重发
@Scheduled(fixedDelay = 300000)
public void retryFailedMessages() {
List<MessageLog> failed = messageLogRepository
.findByStatusAndCreateTimeBefore(SendStatus.FAILED,
LocalDateTime.now().minusMinutes(5));
for (MessageLog log : failed) {
// 重试次数超过上限 → 人工介入
if (log.getRetryCount() >= 3) {
alertService.sendAlert("消息重试 3 次仍失败: " + log.getMessageId());
messageLogRepository.updateStatus(log.getMessageId(), SendStatus.MANUAL);
continue;
}
// 重发消息
rabbitTemplate.convertAndSend(log.getExchange(), log.getRoutingKey(),
log.getPayload(),
new CorrelationData(log.getMessageId()));
messageLogRepository.incrementRetryCount(log.getMessageId());
}
}
案例二:库存扣减------消费端幂等 + 手动 ACK
库存扣减是最容易出问题的消费端场景。即便 Confirm 保证了消息到达 Broker,消费者也可能因网络抖动多次收到同一条消息。必须做幂等。
java
@RabbitListener(queues = "cloudmart.order.queue")
public void handleOrderCreate(Message message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
// 幂等校验:检查 messageId 是否已处理
if (idempotentService.hasProcessed(messageId)) {
log.warn("重复消息,跳过: messageId={}", messageId);
channel.basicAck(tag, false);
return;
}
try {
Order order = objectMapper.readValue(message.getBody(), Order.class);
// 扣减库存(内部用 SELECT FOR UPDATE + 版本号 CAS)
inventoryService.deduct(order.getSkuId(), order.getQuantity());
// 标记 messageId 已处理(Redis SET NX,24h 过期)
idempotentService.mark(messageId);
// 手动确认
channel.basicAck(tag, false);
} catch (InsufficientStockException e) {
// 库存不足 ------ 业务失败,拒绝且不重入,走死信
log.warn("库存不足: {}", e.getMessage());
channel.basicNack(tag, false, false);
}
}
源码走读:Spring AMQP 可靠性核心实现
RabbitTemplate 的 Confirm 回调链路
Spring AMQP 的 Confirm 机制建立在 RabbitMQ 原生的 Publisher Confirm 之上。关键类是 RabbitTemplate 和 PublisherCallbackChannelImpl(位于 spring-rabbit 模块中,源码路径 org.springframework.amqp.rabbit.connection.PublisherCallbackChannelImpl)。
完整调用链:
发送端:
RabbitTemplate.doSend()
→ 包装 channel 为 PublisherCallbackChannelImpl(若未包装)
→ PublisherCallbackChannelImpl.addPendingConfirm()
将 (sequenceNumber, correlationData) 存入 SortedMap<Long, PendingConfirm>
其中 sequenceNumber 由 AtomicLong publisherSequence 自增产生
→ Channel.basicPublish(exchange, routingKey, mandatory, props, body)
Broker 异步返回 ACK/NACK:
→ PublisherCallbackChannelImpl.handleAck(long deliveryTag, boolean multiple)
当 multiple=true 时调用 pendingConfirms.headMap(deliveryTag + 1).clear()
单个确认时直接从 pendingConfirms.remove(deliveryTag)
→ 取出 PendingConfirm.correlationData
→ 遍历 confirmListeners,逐个调用 handleAck(correlationData, ack, cause)
→ 最终触发 RabbitTemplate.confirmCallback.confirm(...)
核心数据结构的线程安全设计------pendingConfirms 是 TreeMap 而非 ConcurrentHashMap,所有访问通过 synchronized 块保护:
java
// PublisherCallbackChannelImpl (Spring AMQP 2.4.x)
private final SortedMap<Long, PendingConfirm> pendingConfirms =
new TreeMap<>();
private static class PendingConfirm {
final CorrelationData correlationData;
final long timestamp;
}
@Override
public void addPendingConfirm(RabbitTemplate template, PendingConfirm pendingConfirm) {
synchronized (this.pendingConfirms) {
// sequenceNumber 是 RabbitMQ Client 分配的 publishSequence
this.pendingConfirms.put(template.getPublishSequence(), pendingConfirm);
}
}
关键设计细节 :multiple=true 是性能优化的核心。RabbitMQ 在批量确认时不会逐条回复,而是回复"该序号及之前的所有消息全部确认"。Spring 利用 SortedMap.headMap() 的 O(log n) 特性高效清理已确认条目,避免 O(n) 遍历。
为什么用 TreeMap + synchronized 而不是 ConcurrentHashMap? 原因有二:(1) 确认消息的顺序递增特性需要范围查询------headMap(deliveryTag + 1).clear() 是一次批量清理,NavigableMap 接口的原生支持让这层操作直接从 O(n) 降到 O(log n);(2) ConcurrentSkipListMap 虽然也是有序并发容器,但其 headMap() 返回的是实时视图 而非独立快照,在遍历过程中如果 Broker 并发推送新的 ACK,会触发 ConcurrentModificationException。Spring 选择显式 synchronized 块 + 普通 TreeMap,是权衡了并发粒度和正确性后的工程决策。
SimpleMessageListenerContainer 的消费与 ACK 链路
SimpleMessageListenerContainer(包路径 org.springframework.amqp.rabbit.listener)本质是一个 并发消费调度框架 ,通过 BlockingQueueConsumer 的 worker 线程不断从 Channel 拉取消息。
消费主链路:
SimpleMessageListenerContainer.start()
→ 创建 N 个 BlockingQueueConsumer(concurrentConsumers 指定)
→ 每个 BlockingQueueConsumer 内部持有
- Channel(独立 TCP 连接,通过 CachingConnectionFactory 获取)
- InternalConsumer(tagConsumer,负责 basicConsume 回调)
- LinkedBlockingQueue<Delivery>(容量 = prefetchCount,核心限流器)
→ 每个 consumer 启动 AsyncMessageProcessingConsumer 线程
→ 循环调用 BlockingQueueConsumer.nextMessage(timeout)
→ 队列空时阻塞在 LinkedBlockingQueue.poll()
→ 拿到 Delivery 组装 Message,调用 invokeListener()
→ 消息处理完后,触发 basicAck()
→ 回到循环取下一个
auto 模式的 ACK 触发时机 ------关键在于 AbstractMessageListenerContainer.executeListener():
java
// AbstractMessageListenerContainer (Spring AMQP 2.4.x 简化)
private void executeListener(Channel channel, Message message) {
try {
doInvokeListener(channel, message); // 反射调用 @RabbitListener 方法
channel.basicAck(deliveryTag, false); // 无异常 → 自动 ACK
} catch (Exception e) {
if (retryTemplate != null) {
// 配置了重试模板 → 按策略重试
retryTemplate.execute(context -> {
doInvokeListener(channel, message);
return null;
}, recoveryCallback); // 重试耗尽后的恢复策略
} else {
// 无重试配置 → 直接 NACK 并重新入队(requeue=true)
channel.basicNack(deliveryTag, false, true);
}
}
}
auto 模式的致命陷阱 :如果 @RabbitListener 方法内部 catch(Exception) { log.error(...) } 把异常吞掉了,doInvokeListener 正常返回,Spring 就会自动 basicAck------Broker 以为消费成功,实际业务处理失败。
防范方案 :auto 模式下务必配置全局
ErrorHandler或FatalExceptionStrategy,在误吞异常时强制抛出不重试异常(如AmqpRejectAndDontRequeueException)。更推荐直接使用 manual 模式,把 ACK 控制权完全交给业务代码。
manual 模式的精确控制 ------实现 ChannelAwareMessageListener,拿到原始 Channel 和 deliveryTag:
java
@Component
public class InventoryListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) {
long tag = message.getMessageProperties().getDeliveryTag();
try {
processBusinessLogic(message);
channel.basicAck(tag, false); // 成功 → ACK
} catch (RecoverableException e) {
channel.basicNack(tag, false, true); // 可恢复 → 重新入队
} catch (FatalException e) {
channel.basicNack(tag, false, false); // 不可恢复 → DLX
}
}
}
重试拦截器底层利用 Spring Retry 的 RetryTemplate:
java
RetryTemplate.builder()
.maxAttempts(3) // 含首次,共 3 次
.exponentialBackoff(1000, 2.0, 10000) // 1s → 2s → 4s,上限 10s
.retryOn(AmqpRejectAndDontRequeueException.class) // false=不重试的异常
.build();
面试要点 :说清楚 auto 的
executeListener如何判断 ACK(异常驱动)、manual 如何拿deliveryTag精确控制、异常分类(Recoverable vs Fatal)对应的重试与 DLX 策略。
死信队列 DLX:消息的"最终归宿"
三大来源详解
消息变成死信(Dead Letter)有三种情况:

来源一:消息被拒绝 (basicReject / basicNack 且 requeue=false)。典型场景:消费者发现消息格式非法,无法处理,直接拒绝。
来源二:消息 TTL 过期 。消息在队列中存活时间超过 x-message-ttl 设定值,且没有被消费。
来源三:队列溢出 。队列达到 x-max-length 或 x-max-length-bytes 上限,超出的消息(默认从队头丢弃)转入 DLX。
关键细节 :原始消息进入 DLX 时,会自动添加
x-death头信息,记录死信原因、时间戳、原始队列和交换机。排查问题时可以直接读x-death知道这条消息为什么变成了死信。

x-death 头结构示例(从死信消息的 Header 中解析):
json
{
"x-death": [{
"reason": "expired",
"count": 1,
"exchange": "cloudmart.order.ex",
"queue": "cloudmart.order.timeout.queue",
"routing-keys": ["order.create"],
"time": {"$date": "2026-06-06T12:30:00.000Z"}
}]
}
reason 的三种典型值:rejected(被拒绝)、expired(TTL 过期)、maxlen(队列溢出)。线上排查直接从 Dead Letter 消息的 Header 里读出最后一条 x-death,就能拿到完整的原路由信息。
TTL:队列级 vs 消息级

两种 TTL 设置方式的本质区别:
| 维度 | 队列级 TTL | 消息级 TTL |
|---|---|---|
| 设置方式 | x-message-ttl 参数 |
setExpiration(String) |
| 作用范围 | 队列内所有消息统一 | 每条消息可独立设置 |
| 删除机制 | 惰性删除(到队头才删) | 惰性删除(到队头才删) |
| 配置复杂度 | 低 | 中 |
核心陷阱 :两种 TTL 都是惰性删除------消息虽然过期了,但只要它不在队列头部,就不会被检查和删除。这意味着如果你用消息级 TTL 实现"不同消息不同延迟",排在后面的短 TTL 消息可能被前面的长 TTL 消息堵住,实际存活时间远超设定值。


实战案例:CloudMart 订单超时自动取消
CloudMart 订单创建后 30 分钟未支付自动取消。实现方案:
java
@Configuration
public class OrderTimeoutConfig {
// 死信交换机
@Bean
public DirectExchange dlxExchange() {
return ExchangeBuilder.directExchange("cloudmart.dlx.ex")
.durable(true).build();
}
// 死信队列
@Bean
public Queue dlxQueue() {
return QueueBuilder.durable("cloudmart.dlx.queue").build();
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange()).with("order.timeout");
}
// 订单队列:30分钟 TTL,过期转入 DLX
@Bean
public Queue orderTimeoutQueue() {
return QueueBuilder.durable("cloudmart.order.timeout.queue")
.ttl(30 * 60 * 1000) // 30分钟
.deadLetterExchange("cloudmart.dlx.ex") // 指定 DLX
.deadLetterRoutingKey("order.timeout") // 死信路由键
.build();
}
}
// 死信消费者:订单超时 → 检查支付状态 → 关单
@RabbitListener(queues = "cloudmart.dlx.queue")
public void handleTimeoutOrder(Message message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
Order order = objectMapper.readValue(message.getBody(), Order.class);
Order latest = orderRepository.findById(order.getId());
if (latest.getStatus() == OrderStatus.UNPAID) {
// 仍未支付 → 关单 + 恢复库存
orderRepository.updateStatus(order.getId(), OrderStatus.CANCELLED);
inventoryService.restore(latest.getSkuId(), latest.getQuantity());
log.info("订单超时自动取消: {}", order.getId());
}
// 已支付 → 忽略
channel.basicAck(tag, false);
}
延迟队列:两种实现方案对比
延迟队列的核心需求是:"消息发送后,在指定时间后才投递给消费者"。RabbitMQ 没有原生延迟队列,但有两种实现方案。
方案对比

选择建议:只有一种延迟时间 → TTL+DLX 够用;多种延迟时间 → 延迟插件避免时序 bug。

实战案例:CloudMart 支付倒计时提醒
CloudMart 需要支付倒计时提醒:下单后 5 分钟、15 分钟、25 分钟各提醒一次。
用延迟插件最合适:
java
// 声明延迟交换机
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "topic"); // 底层交换机类型
return new CustomExchange("cloudmart.pay.delay.ex",
"x-delayed-message", true, false, args);
}
// 发送延迟消息(5分钟后提醒)
public void sendPayReminder(Order order, int delayMinutes) {
rabbitTemplate.convertAndSend("cloudmart.pay.delay.ex",
"pay.remind", order,
msg -> {
msg.getMessageProperties().setDelay(delayMinutes * 60 * 1000);
return msg;
});
}
// 下单后依次发送三个提醒
sendPayReminder(order, 5); // 5 分钟后提醒
sendPayReminder(order, 15); // 15 分钟后提醒
sendPayReminder(order, 25); // 25 分钟后提醒
消费端流量控制:prefetch 与重试
prefetch 的本质

prefetch 不是"限制消费速率",而是限制消费者同时持有的未确认消息数量 。prefetch=1 意味着每一条消息都必须在 ACK 之后才推送下一条,天然形成 Round-Robin 公平分发。
yaml
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 公平分发,每条 ACK 后拿新消息
concurrency: 5 # 5个并发消费者
| prefetch 值 | 分发行为 | 适用场景 |
|---|---|---|
| 1 | ACK → 下一条,公平 Round-Robin | 任务耗时不均 |
| 10-50 | 一次性推送一批 | 高吞吐均匀任务 |
| 250(默认) | 一股脑推送 | 几乎不用 |
重试策略

重试由 Spring Retry 驱动。关键参数:
yaml
retry:
enabled: true
max-attempts: 3 # 最多重试 3 次
initial-interval: 1000 # 第一次重试 1 秒后
multiplier: 2.0 # 指数退避:1s → 2s → 4s
max-interval: 10000 # 最大间隔 10s
重试耗尽后的处理:
java
// 重试耗尽 MessageRecoverer → 发到死信队列
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
return new RepublishMessageRecoverer(rabbitTemplate,
"cloudmart.error.ex", "error.retry.exhausted");
}
实战案例:CloudMart 秒杀限流
java
@Configuration
public class SecKillConfig {
@Bean
public SimpleRabbitListenerContainerFactory secKillFactory(
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPrefetchCount(10); // 每次最多拿 10 条
factory.setConcurrentConsumers(3);
factory.setMaxConcurrentConsumers(10); // 动态扩容上限
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
}
@RabbitListener(queues = "cloudmart.seckill.queue",
containerFactory = "secKillFactory")
public void handleSecKill(Message message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
// 秒杀扣减逻辑(库存预热到 Redis,无需回源 DB)
String skuId = ...; // 从消息体解析
Long stock = redisTemplate.opsForValue().decrement("seckill:stock:" + skuId);
if (stock == null || stock < 0) {
channel.basicNack(tag, false, false); // 卖完了,拒绝
return;
}
// 异步写入订单
orderService.createSecKillOrder(skuId);
channel.basicAck(tag, false);
}
事务机制 vs Confirm 模式

AMQP 提供了事务机制(txSelect / txCommit / txRollback),但它和 Confirm 是互斥的------一个 Channel 不能同时开启事务和 Confirm。
| 维度 | 事务模式 | Confirm 模式 |
|---|---|---|
| 保证 | 原子性(一批消息要么全发要么全不发) | 每条消息独立 ACK |
| 性能 | 吞吐量下降约 100~250 倍(官方文档《Using transactions》实测数量级,具体与消息大小/批次数相关) | 异步非阻塞,性能接近无 Confirm |
| 使用场景 | 几乎不用 | 生产唯一推荐 |
| Spring Boot 配置 | publisher-confirm-type: none |
publisher-confirm-type: correlated |
结论:事务机制在 RabbitMQ 中基本没有生产价值。Confirm 模式 + 本地消息表/回调已经足够覆盖发送端可靠性需求。面试时被问到事务直接说"不用,用 Confirm"即可。
横向对比:RabbitMQ vs Kafka 可靠性模型
对比基于 2025-2026 年 RabbitMQ 3.12/3.13 与 Kafka 3.x 主流版本,随版本迭代部分特性可能调整。
面试中经常被问"RabbitMQ 和 Kafka 谁更可靠",这里给出精确回答:
| 维度 | RabbitMQ | Kafka |
|---|---|---|
| 可靠性模型 | 推送模型(Broker push),依赖消费端 ACK | 拉取模型(Consumer pull),依赖 offset 提交 |
| 消息持久化 | 三层持久化(Exchange/Queue/Message),逐条刷盘 | 分段日志(Segment Log),顺序写磁盘 + Page Cache |
| 消费确认 | 逐条 ACK / NACK,支持重新入队 | offset 提交,不支持单条回退 |
| 重复消费风险 | 高(NACK 重入队、网络重连) | 低(offset 精确控制),但 rebalance 期间可能重复 |
| 消息丢失风险 | 高(需手动三道防线) | 低(ISR + min.insync.replicas),默认持久化 |
| 事务支持 | AMQP 原生事务(性能差) | Kafka Transactions(幂等 + 事务,性能可控) |
| 典型场景 | 业务指令、RPC、延迟任务 | 日志采集、流处理、事件溯源 |

核心区别:RabbitMQ 设计目标是"灵活路由 + 单条精确控制",可靠性靠应用层配合(Confirm + 持久化 + 手动 ACK);Kafka 设计目标是"高吞吐 + 天然持久化",可靠性靠 ISR 副本机制和分段日志保证。选型时不是比谁更可靠,而是看业务对"单条消息的精确投递控制"和"天然顺序持久化"哪个需求更强。
面试追问:消息可靠性 15 连问
- RabbitMQ 如何保证消息不丢失? → 三道防线:发送方 Confirm + Broker 三层持久化 + 消费方手动 ACK。
- Confirm 的 ACK 是同步还是异步? → 异步。
convertAndSend()返回不代表 ACK 已到。 - 三层持久化缺一层会怎样? → 对应层重启后丢失,消息无法路由或存储。
deliveryMode=PERSISTENT就保证写入磁盘了吗? → 不保证。只在刷盘后才真正持久化,需镜像队列或 Quorum Queue 进一步加强。- 自动 ACK 和手动 ACK 的区别? → 自动 ACK:消息发出即删;手动 ACK:必须显式调用
basicAck。 basicNack(tag, false, false)和basicNack(tag, false, true)的区别? → 第三个参数:false不重新入队(走 DLX),true重新入队(重试)。- 消息变成死信有哪三种情况? → 被拒绝且不重入 / TTL 过期 / 队列溢出。
- TTL + DLX 实现延迟队列有什么坑? → 时序陷阱:惰性删除导致后面短 TTL 消息被前面长 TTL 消息堵住。
- 队列级 TTL 和消息级 TTL 能不能混用? → 能混用,取二者较小值。
- 延迟插件和 TTL+DLX 怎么选? → 多种延迟 → 插件;一种延迟 → TTL+DLX 也行。
- prefetch=1 有什么优缺点? → 优点:公平分发,防止消费者过载;缺点:单条 ACK 降低吞吐。
- 重试次数耗尽了怎么办? → MessageRecoverer 转发到死信队列,人工介入或定时补偿。
- 怎么保证消息不重复消费? → 消费端幂等:messageId + Redis SET NX / 数据库唯一约束。
- 事务机制和 Confirm 模式能同时开启吗? → 不能,互斥。Confirm 性能远优于事务。
- 消息可靠性方案有没有 100% 保证? → 没有。理论上 Broker 断电时 Page Cache 中的数据仍可能丢失。可以引入本地消息表 + 定时补偿兜底,做到 99.99% 的 at-least-once。exactly-once 需要业务层的幂等配合。
可靠性验证清单
配置写完了,怎么验证三道防线真的生效?以下是可直接执行的验证步骤:
| 验证目标 | 操作步骤 | 预期结果 |
|---|---|---|
| Confirm 回调 | 发送消息到不存在的 Exchange | 3 秒内 ConfirmCallback 收到 NACK |
| 消息持久化 | 发送一个 delivery-mode=2 的消息后执行 rabbitmqctl stop_app && rabbitmqctl start_app |
消息仍在原队列中 |
| Queue 持久化 | 不设 durable=true 声明队列,执行 rabbitmqctl stop_app && rabbitmqctl start_app |
队列消失,rabbitmqctl list_queues 找不到 |
| Manual ACK | 消费端收到消息后在代码中 Thread.sleep(60) 秒(模拟业务耗时),在此期间不调用 basicAck |
消息状态为 Unacked,Consumer 重启后 re-queue 回到 Ready |
| NACK 重入队 | 消费端故意抛 RecoverableException → basicNack(tag, false, true) |
消息立即回到队列头部,被下一个 Consumer 重新消费 |
| DLX 转入 | 消费端抛 FatalException → basicNack(tag, false, false) |
消息出现在 DLX 队列中,`rabbitmqctl list_queues |
| prefetch 生效 | 设置 spring.rabbitmq.listener.simple.prefetch=1,发送 3 条消息,第一条 sleep 60s |
其余 2 条停留在 Ready 状态,Consumer Unacked 始终为 1 |
关键命令 :
rabbitmqctl list_queues name messages_ready messages_unacknowledged可实时观察队列状态。验证 DLX 时执行rabbitmqctl list_queues name messages | grep dlx。
下期预告:第三期「应用问题与面试精讲」------ 消息堆积诊断与处理、集群搭建、高可用架构、RabbitMQ vs Kafka 终极对比。