MQ高级 · RabbitMQ
可靠性
1. 生产者
生产者重连
由于网络波动,可能会出现客户端连接 MQ 失败的情况。 通过配置我们可以开启连接失败后的重连机制:
发信层 template.retry()
代码块收起展开
spring:
rabbitmq:
host: 192.168.1.100
port: 5672
virtual-host: /
username: admin
password: 123456
# ===== 重连配置 =====
connection-timeout: 1s # 建立连接超时时间
template:
retry:
enabled: true # 开启 RabbitTemplate 的重试
initial-interval: 1000ms # 首次重试等待 1s
multiplier: 2 # 每次重试等待时间 * 2(指数退避) 下次等待=initial*mult
max-attempts: 3 # 最多重试 3 次(含首次)
max-interval: 10000ms # 重试等待上限 10sSpringAMQP 提供的重试机制是阻塞式的重试 即多次重试等待的过程中, 当前线程是被阻塞的, 会影响业务性能。
如果对于业务性能有要求,建议禁用重试机制。 如果一定要使用,请合理配置等待时长和重试次数,或使用异步线程来执行发送消息的代码。
连接层面的自动恢复
代码块收起展开
/**
* 连接层面的自动恢复配置
* Spring AMQP 底层 AMQP Client 默认已开启 automatic recovery
* 但生产环境建议显式配置,更可控
*/
@Configuration
public class RabbitConnectionConfig {
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost("192.168.1.100");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
factory.setVirtualHost("/");
// 连接缓存模式:CHANNEL(默认)vs CONNECTION
// CHANNEL 模式:一个物理连接,多个 channel 复用
// CONNECTION 模式:每次 getConnection() 可能返回新连接(适合高并发)
factory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
factory.setChannelCacheSize(10); // channel 缓存数量
return factory;
}
}生产者确认
发消息出去了,但消息真的到 Broker 了吗? 到了 Exchange 之后路由到 Queue 了吗?这就是 Publisher Confirms + Returns 要解决的问题。
- 消息投递到了 MQ , 但是路由失败。此时会通过 PublisherReturn 返回路由异常原因, 然后返回 ACK, 告知投递成功
- 临时消息投递到了 MQ , 并且入队成功, 返回 ACK, 告知投递成功
- 持久消息投递到了 MQ , 并且入队完成持久化, 返回 ACK, 告知投递成功
- 其他都是失败, 返回NACK
flowchart LR
P[Publisher<br/>rabbitTemplate]
E1[Topic Exchange]
E2[Exchange2]
Q1[Queue1<br/>non durable]
Q2[Queue2<br/>durable]
C1[Consumer1]
C2[Consumer2]
S[(Persistent Storage)]
P --> E1
P --> E2
E1 --> Q1
E1 --> Q2
Q1 --> C1
Q2 --> C2
Q2 -. persist .-> S
E1 -. ack .-> P
E2 -. ack .-> P
E2 -. return .-> P
三种 ConfirmType 模式需要特别理解:
NONE 是默认值,完全不确认,性能最好,消息丢了也不知道。
CORRELATED 是真正的异步确认,每条消息有唯一 correlationId,Broker 处理后异步回调,性能好且可靠,生产推荐。
SIMPLE 是同步等待确认,发一条等一条,性能差,不推荐。
生产者确认代码实现
配置
代码块收起展开
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启 CORRELATED 异步确认
publisher-returns: true # 开启 Return 退回回调
template:
mandatory: true # 消息路由失败时退回而非丢弃(触发 Return)配置类
代码块收起展开
/**
* Publisher Confirms + Returns 完整实现
*/
@Slf4j
@Configuration
public class PublisherConfirmConfig implements RabbitTemplate.ConfirmCallback,
RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 注意:RabbitTemplate 是 prototype 的,但这里通过 @PostConstruct
* 把 callback 注入进去,每个 RabbitTemplate 实例只设置一次
*/
@PostConstruct
public void init() { //接口的子实现类, 所以init可以直接this
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* Confirm 回调
* 消息成功到达 Exchange → ack=true
* Broker 内部错误 → ack=false, cause 说明原因
*
* @param correlationData 发送时传入的关联数据(含消息ID)
* @param ack 是否确认成功
* @param cause 失败原因(ack=true 时为 null)
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String msgId = correlationData != null ? correlationData.getId() : "unknown";
if (ack) {
log.info("[Confirm ACK] 消息已到达Exchange, msgId={}", msgId);
// 可选:更新数据库消息状态为"已投递"
} else {
log.error("[Confirm NACK] 消息投递Exchange失败, msgId={}, cause={}", msgId, cause);
// 触发补偿:重发消息 / 告警
// 注意:这里是异步线程,不能抛异常影响主流程
handleSendFailed(msgId, cause);
}
}
/**
* Return 回调(路由失败时触发)
* 消息到了 Exchange 但没有匹配的 Queue binding
*
* @param returned 退回的消息详情(含原始 Message、routingKey、replyText 等)
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("[Return] 消息路由失败被退回! exchange={}, routingKey={}, replyCode={}, replyText={}, body={}",
returned.getExchange(),
returned.getRoutingKey(),
returned.getReplyCode(),
returned.getReplyText(),
new String(returned.getMessage().getBody()));
// 处理:检查 routingKey / binding 配置是否正确
// 生产建议:写入 DB 做人工复查,或者投递到专用的 unroutable 队列
}
private void handleSendFailed(String msgId, String cause) {
// 示例:从 DB 捞出消息体重新发送
// messageService.markFailed(msgId);
// alertService.send("MQ投递失败: " + msgId);
}
}2. MQ
开启持久化和生产者确认时,RabbitMQ只有在消息持久化完成后才 会给生产者返回ACK回执
消息持久化
Transient 消息堆积时,内存压力触发”被迫写盘”这一步,写盘来不及就触发 flow control,严重时触发 memory alarm,Publisher 被 block,发布速率才会掉到 0。
Persistent 消息从头就在走写盘路径,内存压力来时只是”丢 RAM 缓存”,没有突发的额外 I/O,所以更不容易因为这个原因被 block。
持久化的代价是性能下降(写磁盘)。如果场景允许少量丢失(比如日志统计),可以关闭 deliveryMode=1 提升 TPS。但对于订单/支付等核心业务,必须三层全开。
code
代码块收起展开
/**
* 持久化相关配置代码
*/
@Configuration
public class PersistenceConfig {
@Bean // Exchange 持久化
public DirectExchange orderExchange() {
// 三个参数:name, durable, autoDelete
return new DirectExchange("order.direct", true, false);
}
@Bean // Queue 持久化
public Queue orderQueue() {
return QueueBuilder
.durable("order.queue") // durable=true
// .nonDurable() // durable=false(重启后消失,不要用在生产)
.build();
}
/**
* 全局消息转换器配置
* Jackson2JsonMessageConverter 序列化时默认设置 deliveryMode=PERSISTENT
* 这是 Spring AMQP 2.x 之后的默认行为
*/
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
// 创建时配置 ObjectMapper,处理日期/空值等
DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper();
typeMapper.setTrustedPackages("*"); // 反序列化时信任所有包
converter.setJavaTypeMapper(typeMapper);
return converter;
}
/**
* 配置 RabbitTemplate 使用上面的转换器
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
MessageConverter messageConverter) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(messageConverter);
return template;
}
}LazyQueue
- 直接存磁盘, 读的时候再加载
- 支持数百万数据的存储
- 写磁盘有优化, 因此会快很多
普通 Queue 消息默认存在内存里,大量消息堆积时内存撑爆,Broker 触发流控(flow control)影响所有 Producer。LazyQueue 解决这个问题:
普通 Queue:消息 → 内存(paging机制,内存满了才 swap 到磁盘) Lazy Queue:消息 → 直接写磁盘(内存只保留索引),消费时再加载
code
代码块收起展开
/**
* LazyQueue 声明方式
* RabbitMQ 3.6+ 支持 x-queue-mode=lazy
* RabbitMQ 3.12+ LazyQueue 成为默认模式
*/
@Configuration
public class LazyQueueConfig {
/**
* 方式一:通过 QueueBuilder(推荐)
*/
@Bean
public Queue lazyOrderQueue() {
return QueueBuilder
.durable("order.lazy.queue")
.lazy() // 等价于 withArgument("x-queue-mode", "lazy")
.build();
}
/**
* 方式二:手动 withArgument(老版本 Spring AMQP 没有 .lazy() 方法时用)
*/
@Bean
public Queue lazyQueueManual() {
return QueueBuilder
.durable("order.lazy.queue.v2")
.withArgument("x-queue-mode", "lazy")
.build();
}
/**
* 方式三:LazyQueue + 死信队列一起配
* 生产上推荐这种完整配置
*/
@Bean
public Queue lazyQueueWithDLX() {
return QueueBuilder
.durable("order.main.queue")
.lazy()
.withArgument("x-dead-letter-exchange", "order.dlx.exchange")
.withArgument("x-dead-letter-routing-key", "order.dlx")
.withArgument("x-message-ttl", 300000) // 5 分钟 TTL
.build();
}
}3. 消费者
消费者确认
- queue投递给消费者, 有三种ack处理方式:
- none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调API,发送ack或reject,存在业务入侵,但更灵活
- auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执时则动返回ack.
2.当业务出现异常时,根据异常判断返回不同结果: 如果是业务异常,会自动返回nack 如果是消息处理或校验异常,动返回reject
代码块收起展开
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode : none #none, 关闭ack; manual, 手动ack; auto, 自动ack消费失败处理
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接来处理,它包含三种不同的实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
#### RepublishMessageRecoverer
失败消息处理策略
将失败处理策略改为RepublishMessageRecoverer:
1首先,定义接收失败消息的交换机、队列及其绑定关系,此处略;
然后,定义RepublishMessageRecoverer.
code
配置
代码块收起展开
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 重试机制下一般配 auto,让 Spring 接管
retry:
enabled: true # 开启消费者本地重试
initial-interval: 1000ms # 首次重试等待 1s
multiplier: 2 # 指数退避:1s → 2s → 4s
max-attempts: 3 # 最多重试 3 次(含首次消费,实际重试 2 次)
max-interval: 10000ms # 重试等待上限 10s
stateless: true # 无状态重试(不依赖事务),一般用这个messageRecoverer
重试耗尽后的消息处理策略 — MessageRecoverer, 三种内置实现:
-
- RejectAndDontRequeueRecoverer(默认):直接丢弃 / 进 DLX
-
- ImmediateRequeueMessageRecoverer:重新入队(危险,一般不用)
-
- RepublishMessageRecoverer:重新发布到指定队列(推荐!)
代码块收起展开
@Configuration
public class RetryConfig {
/**
* 推荐方案:RepublishMessageRecoverer
* 重试耗尽后,把消息投递到一个专门的"失败队列"
* 保留消息 + 异常信息 + 原始队列信息,方便人工排查
*/
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
// 参数1:失败后投递到的 exchange
// 参数2:routing key
return new RepublishMessageRecoverer(
rabbitTemplate,
"error.exchange", // 专门收失败消息的 exchange
"error.routing.key"
);
}
/**
* 配套声明:error exchange 和 error queue
*/
@Bean
public DirectExchange errorExchange() {
return new DirectExchange("error.exchange");
}
@Bean
public Queue errorQueue() {
return QueueBuilder.durable("error.queue").build();
}
@Bean
public Binding errorBinding() {
return BindingBuilder.bind(errorQueue())
.to(errorExchange())
.with("error.routing.key");
}
}consumer
代码块收起展开
/**
* 配合重试机制的消费者写法(auto ack 模式)
* Spring 会自动重试,无需手动 channel.basicNack
*/
@Slf4j
@Component
public class OrderConsumerWithRetry {
@RabbitListener(queues = "order.queue")
public void consume(OrderDTO order, Message message) {
log.info("[消费] orderId={}", order.getOrderId());
// 直接抛异常即可,Spring 的 RetryInterceptor 会捕获并重试
// 重试次数耗尽后,MessageRecoverer 处理(进 error.queue)
orderService.processOrder(order);
// 正常返回 → Spring 自动 ack
log.info("[完成] orderId={}", order.getOrderId());
}
}Spring AMQP 的本地重试是”在内存里重试”,不会把消息重新放回 Broker,所以不占用 Broker 资源, 重试期间消息一直是 Unacked 状态。重试耗尽才由
MessageRecoverer决定最终命运。
4. 业务的幂等性
幂等是一个数学概念,函数表达式来描述是这样的:f(x)=f(f(x))。 在程序开发中,则是指同个业务,执次一次或多次对业务状态的影响是一致的。
唯一消息id
给每个消息都设置一个唯一id,利用id区分是否是重复消息:
- 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
- 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
- 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
业务判断
结合业务逻辑,基于业务本身做判断, 有些业务天然幂等性 以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理.
5. 延迟消息
延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。 场景比如: 外卖未支付取消订单
死信交换机
原来官方推出用作msg可靠性的兜底方案
死信
当一个队列中的消息满足下列情况之一时,就会成为死信(deadletter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
死信交换机
队列通过
dead-letter-exchange属性指定了一个交换机, 那么该队列的私信会投递到此交换机, 称为死信交换机![]()
延迟信息插件
消息投递到交换机后可以暂存一定时间, 到期再投递到队列
创建方式
代码块收起展开
//注解创建
@RabbitListener(
bindings = @QueueBinding(
value=@Queue(name=, durable=),
exchange=@Exchange(name=,delayed=true),
key=""
)
)
//bean创建
@Bean
public dirEx delayExchange(){
new ExchangeBuilder
.directExchange()
.delayed()
.durable(true)
.build();
}
设置过期时间
发送消息时需要通过消息头x-delay来设置过期时间:
代码块收起展开
//2.发送消息,利用消息后置处理器添加消息头
@Test
void testPublisherDelayMessage() {
//1.创建消息
String message = "hello, delayed message";
rabbitTemplate.convertAndSend("delay.direct","delay",message,new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//添加延迟消息属性
message·getMessageProperties().setDelay(5000);
return message;
});
}bug
延时的实现需要让CPU维护一个时 钟, 需要CPU不断计算, 是CPU密集型计算.
因此延迟时间不能太长, 延迟任务不能太多
取消超时订单
设置30分钟后检测订单支付状态实现起来非常简单,但是存在两个问题:
- 如果并发较高,30分钟可能堆积消息过多,对MQ压很
- 多数订单在下单后1分钟内就会付,但是却需要在MQ内等待30分钟,浪费资源
解决: 基于正态分布, 我们可以做出如下优化
- 可以在订单确立后短时间内检查几次, 如果已付款就从dlq删掉, 这样可以排除大部分的订单
- 把一段长时间的延迟拆成N段短时间的check
注意这里查询支付状态可能是msg没送达队列, 需要主动查询真伪