RabbitMQ高级篇总结
本文总结了黑马课程中的MQ高级篇的内容,从而对RabbitMQ中的进阶的使用技巧有更好的理解
MQ入门篇的内容:MQ入门篇
📚 目录(点击跳转对应章节)
一、发送者可靠性
二、数据持久化 (MQ的可靠性)
三、消费者可靠性
四、业务幂等性
五、常见业务问题与兜底
六、延迟消息
七、黑马商城业务改造(超时订单问题)
前言
在微服务架构中,RabbitMQ作为主流的消息中间件,承担着削峰填谷、异步解耦的重要职责。然而,在实际生产环境中,消息丢失 、重复消费 以及消息积压是开发人员必须面对的挑战。
一、发送者可靠性
消息从生产者发送到Broker的过程中,可能会因为网络波动或配置错误导致丢失。我们需要确保消息成功到达了交换机(Exchange)和队列(Queue)。
1. 发送者重连机制
为了应对网络抖动,RabbitMQ提供了发送者重连机制。当连接断开时,框架会尝试自动重新连接。
配置示例(YAML):
yaml
spring:
rabbitmq:
connection-timeout: 1s # 连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次等待时长的倍数
max-attempts: 3 # 最大重试次数
注意事项:

2. 发送者确认机制 (Publisher Confirm & Return)
Spring AMQP提供了两种回调机制来确认消息投递状态:
ConfirmCallback:消息发送到交换机后触发。ReturnCallback:消息从交换机路由到队列失败时触发(例如路由键错误)。
流程图如下:

配置开启:
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并支持关联confirm
publisher-returns: true # 开启publisher return机制
publisher-confirm-type说明:
none:禁用confirm机制(默认)。simple:同步阻塞等待confirm结果(MQ回执消息),直到超时,性能差,不推荐使用。correlated:异步回调,性能好,推荐使用。
代码实现:
我们需要在配置类中定义ReturnCallback,并在发送消息时绑定ConfirmCallback。
1.配置ReturnCallback:

2.发送消息,指定消息ID,消息ConfirmCallback:

二、数据持久化 (MQ的可靠性)
即使消息成功发送,由于RabbitMQ会将接收到的信息保存在内存中用以降低接发消息的延迟,同样也会导致如下两个问题:
- MQ宕机导致内存中的消息丢失
- 内存空间是有限的,如果消费者出现故障或处理速度过慢时,会导致消息大量积压导致MQ阻塞
针对如上两个问题,MQ 中通过一些手段让发送的消息进行持久化从而降低问题带来的问题
1. 交换机持久化
在定义交换机时,默认情况下Spring AMQP会将durable设置为true。
java
// 默认持久化
new DirectExchange("simple.direct", true, false);

2. 队列持久化
同样,定义的队列默认也是持久化的。
java
// 默认持久化
new Queue("simple.queue", true);

3. 消息持久化
发送消息时,需要将消息标记为持久化。Spring AMQP的convertAndSend默认发送的消息通常是持久化的(DeliveryMode = 2)。
性能权衡 :持久化会涉及磁盘IO,因此性能会低于非持久化模式。但在可靠性要求高的业务中,这是必须的代价。
4. Lazy Queue (惰性队列)
从RabbitMQ 3.6.0版本开始引入了Lazy Queue。
特点:
- 消息接收后直接写入磁盘,不再存储在内存中。
- 只有消费者来消费时,才从磁盘加载到内存(可以提前缓存部分消息到内存,最多2048条)。
- 优势 :支持数百万条消息的堆积,适合高并发或消费者由于故障挂起导致消息大量堆积的场景。


小结:

三、消费者可靠性
1. 消费者确认机制 (Consumer Acknowledgement)

消费者处理消息后,需要告知RabbitMQ,以便MQ删除消息。Spring AMQP提供了三种确认模式:
配置:
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 可选值:none, manual, auto
- none:消息投递即自动确认。如果消费者处理抛出异常,消息会丢失。
- manual:手动确认。业务代码中调用API确认,灵活但繁琐。
- auto (推荐):由Spring框架依据业务逻辑执行情况自动确认。
- 正常执行 -> ack
- 抛出异常 -> nack/reject
2. 失败重试机制
当消费者处理消息异常时,如果立即将消息退回队列,可能形成死循环(反复消费-失败-退回)。Spring提供了本地重试机制。
配置:
yaml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始失败等待时长
multiplier: 1 # 下次失败等待时长的倍数,下次等待时长=multiplier*last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态; false有状态 业务中包含事务时这里需要改为false
当重试次数耗尽后,Spring默认会丢弃消息。这通常不符合业务需求,我们需要配置失败处理策略。
3. 失败消息处理策略 (MessageRecoverer)

Bean配置示例:
java
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
// 指定异常交换机和路由键
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
具体实现:
java
@Configuration
public class ErrorMessageConfiguration {
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
@Bean
public Binding errorQueueBinding(Queue errorQueue, DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
MQ中接收到的消息格式:

四、业务幂等性

解决方案
唯一消息ID:

结合业务逻辑进行处理:

java
// 伪代码示例
String msgId = message.getMessageProperties().getMessageId();
Boolean isConsumed = redisTemplate.opsForValue().setIfAbsent("mq:consumed:" + msgId, "1", 30, TimeUnit.MINUTES);
if (!isConsumed) {
// 已经消费过,直接返回
return;
}
// 执行业务逻辑...
五、常见业务问题与兜底
1. 支付服务与交易服务的一致性
如何保证支付成功后,订单状态一定能更新?

- 基础保障:使用上述的发送端确认、持久化、消费者确认和重试机制。
- 兜底方案:如果MQ彻底挂了或消息丢失,需要有定时任务(Job)扫描支付表和订单表,主动查询支付状态并补偿更新订单状态。
2. 交易服务处理失败怎么办?
如果消费者重试多次依然失败,消息进入死信/异常队列。此时需要人工介入查看异常日志,或者开发专门的工具重新解析异常队列中的消息进行修复。
六、延迟消息

RabbitMQ本身没有直接的"延迟队列"功能(除非使用插件),通常通过死信交换机 (DLX) 和 TTL (Time To Live) 配合实现。
实现原理
- TTL:给消息设置过期时间,或者给队列设置消息过期时间。
- 死信交换机:当队列中的消息因为过期(TTL结束)未被消费时,它会成为"死信"。
- 流程:
- 发送消息到
普通队列(没有消费者),设置TTL(例如30分钟)。 普通队列绑定死信交换机。- 消息过期后,被自动转发到
死信交换机。 死信交换机将消息路由到死信队列。- 消费者监听
死信队列,从而实现在30分钟后才收到消息的效果。

六、DelayExchange插件的安装和使用
基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。
官方文档:Scheduling Messages with RabbitMQ | RabbitMQ
下载地址:rabbitmq/rabbitmq-delayed-message-exchange:RabbitMQ 的延迟消息传递 --- rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ
安装(基于docker,且以下出现的mq皆为RabbitMQ对应的docker容器名称):
使用命令查看RabbitMQ的插件目录对应的数据卷:
shell
docker volume inspect mq-plugins
会得到如下类似的结果:
json
[
{
"CreatedAt": "2024-06-19T09:22:59+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mq-plugins/_data",
"Name": "mq-plugins",
"Options": null,
"Scope": "local"
}
]
这代表mq的插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data这个目录,我们上传插件到该目录下。
bash
cd /var/lib/docker/volumes/mq-plugins/_data
一般我们会使用SSH客户端进行远程连接Linux,因此直接拖拽下载好的插件到该目录下即可
随后执行如下命令即可启动插件:
shell
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
出现如下结果则代表成功运行:

具体使用
声明延迟交换机
1.基于注解方式:
java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}
2.基于@Bean的方式:
java
package com.itheima.consumer.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class DelayExchangeConfig {
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayQueueBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
}
}
发送延迟消息
发送消息时,必须通过x-delay属性设定延迟时间:
java
@Test
void testPublisherDelayMessage() {
// 1.创建消息
String message = "hello, delayed message";
// 2.发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
注意事项:
延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息
七、黑马商城业务改造(超时订单问题)
流程图:

假如订单超时支付时间为30分钟,理论上说我们应该在下单时发送一条延迟消息,延迟时间为30分钟。这样就可以在接收到消息时检验订单支付状态,关闭未支付订单。
定义常量
无论是消息发送还是接收都是在交易服务完成,因此我们在trade-service中定义一个常量类,用于记录交换机、队列、RoutingKey等常量:

在 trader-service配置routingkey常量类MQConstants:
java
package com.hmall.trade.constants;
public interface MQConstants {
// 延时交换机
String DELAY_EXCHANGE_NAME = "trade.delay.direct";
// 延时队列
String DELAY_ORDER_QUEUE_NAME = "trade.delay.queue";
// 延时队列绑定的key
String DELAY_ORDER_KEY = "delay.order.query";
}
配置依赖
在trade-service模块的pom.xml中引入amqp的依赖:
xml
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在trade-service的application.yaml中添加MQ的配置:
yaml
spring:
rabbitmq:
host: 192.168.150.101 # 自己的主机名
port: 5672
virtual-host: /hmall
username: hmall
password: 123
改造下单业务,发送延迟消息
前置条件完成后,修改trade-service模块的com.hmall.trade.service.impl.OrderServiceImpl类的createOrder方法,添加消息发送的代码:
java
// 5.发送延迟消息,检测订单支付状态
rabbitTemplate.convertAndSend(
MQConstants.DELAY_EXCHANGE_NAME,
MQConstants.DELAY_ORDER_KEY,
order.getId(),
message -> {
message.getMessageProperties().setDelay(60000*15);//15分钟,如果为了测试可以改成10000(10秒钟)
return message;
}
);
编写查询支付状态接口
由于MQ消息处理时需要查询支付状态,因此我们做如下的添加
首先,在hm-api模块定义三个类:

- PayOrderDTO:支付单的数据传输实体:
java
package com.hmall.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* <p>
* 支付订单
* </p>
*/
@Data
@ApiModel(description = "支付单数据传输实体")
public class PayOrderDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("业务订单号")
private Long bizOrderNo;
@ApiModelProperty("支付单号")
private Long payOrderNo;
@ApiModelProperty("支付用户id")
private Long bizUserId;
@ApiModelProperty("支付渠道编码")
private String payChannelCode;
@ApiModelProperty("支付金额,单位分")
private Integer amount;
@ApiModelProperty("付类型,1:h5,2:小程序,3:公众号,4:扫码,5:余额支付")
private Integer payType;
@ApiModelProperty("付状态,0:待提交,1:待支付,2:支付超时或取消,3:支付成功")
private Integer status;
@ApiModelProperty("拓展字段,用于传递不同渠道单独处理的字段")
private String expandJson;
@ApiModelProperty("第三方返回业务码")
private String resultCode;
@ApiModelProperty("第三方返回提示信息")
private String resultMsg;
@ApiModelProperty("支付成功时间")
private LocalDateTime paySuccessTime;
@ApiModelProperty("支付超时时间")
private LocalDateTime payOverTime;
@ApiModelProperty("支付二维码链接")
private String qrCodeUrl;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}
- PayClient:支付系统的Feign客户端:
java
package com.hmall.api.client;
import com.hmall.api.client.fallback.PayClientFallback;
import com.hmall.api.dto.PayOrderDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "pay-service", fallbackFactory = PayClientFallback.class)
public interface PayClient {
/**
* 根据交易订单id查询支付单
* @param id 业务订单id
* @return 支付单信息
*/
@GetMapping("/pay-orders/biz/{id}")
PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id);
}
- PayClientFallback:支付系统的fallback逻辑:
java
package com.hmall.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* <p>
* 支付订单
* </p>
*/
@Data
@ApiModel(description = "支付单数据传输实体")
public class PayOrderDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("业务订单号")
private Long bizOrderNo;
@ApiModelProperty("支付单号")
private Long payOrderNo;
@ApiModelProperty("支付用户id")
private Long bizUserId;
@ApiModelProperty("支付渠道编码")
private String payChannelCode;
@ApiModelProperty("支付金额,单位分")
private Integer amount;
@ApiModelProperty("付类型,1:h5,2:小程序,3:公众号,4:扫码,5:余额支付")
private Integer payType;
@ApiModelProperty("付状态,0:待提交,1:待支付,2:支付超时或取消,3:支付成功")
private Integer status;
@ApiModelProperty("拓展字段,用于传递不同渠道单独处理的字段")
private String expandJson;
@ApiModelProperty("第三方返回业务码")
private String resultCode;
@ApiModelProperty("第三方返回提示信息")
private String resultMsg;
@ApiModelProperty("支付成功时间")
private LocalDateTime paySuccessTime;
@ApiModelProperty("支付超时时间")
private LocalDateTime payOverTime;
@ApiModelProperty("支付二维码链接")
private String qrCodeUrl;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}
最后,在pay-service模块的PayController中实现该接口:
java
@ApiOperation("根据id查询支付单")
@GetMapping("/biz/{id}")
public PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id){
PayOrder payOrder = payOrderService.lambdaQuery().eq(PayOrder::getBizOrderNo, id).one();
return BeanUtils.copyBean(payOrder, PayOrderDTO.class);
}
设置监听,查询支付的状态
首先,在 trader-service编写一个监听器,监听延迟消息,查询订单支付状态:

其中的内容:
java
package com.hmall.trade.listener;
import com.hmall.api.client.PayClient;
import com.hmall.api.dto.PayOrderDTO;
import com.hmall.trade.constants.MQConstants;
import com.hmall.trade.domain.po.Order;
import com.hmall.trade.service.IOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderDelayMessageListener {
private final IOrderService orderService;
private final PayClient payClient;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MQConstants.DELAY_ORDER_QUEUE_NAME),
exchange = @Exchange(name = MQConstants.DELAY_EXCHANGE_NAME, delayed = "true"),
key = MQConstants.DELAY_ORDER_KEY
))
public void listenOrderDelayMessage(Long orderId){
// 1.查询订单
Order order = orderService.getById(orderId);
// 2.检测订单状态,判断是否已支付
if(order == null || order.getStatus() != 1){
// 订单不存在或者已经支付
return;
}
// 3.未支付,需要查询支付流水状态
PayOrderDTO payOrder = payClient.queryPayOrderByBizOrderNo(orderId);
// 4.判断是否支付
if(payOrder != null && payOrder.getStatus() == 3){
// 4.1.已支付,标记订单状态为已支付
orderService.markOrderPaySuccess(orderId);
}else{
// 4.2.未支付,则修改订单状态为取消
orderService.cancelOrder(orderId);
}
}
}
cancelOrder() 中的内容:
java
@Override
public void cancelOrder(Long orderId) {
// 1.取消订单
// 构建更新条件
Order order = new Order();
order.setId(orderId);
order.setStatus(3);
order.setCloseTime(LocalDateTime.now());
updateById(order);
// 2. 恢复库存
recoverStock(orderId);
}
private void recoverStock(Long orderId) {
// 2.1 查询订单详情
List<OrderDetail> orderDetails = detailService.listByOrderId(orderId);
if (orderDetails == null || orderDetails.isEmpty()) {
throw new BadRequestException("订单详情不存在!");
}
// 2.2 构造恢复库存的参数列表
List<OrderDetailDTO> recoverItems = orderDetails.stream()
.map(detail -> {
OrderDetailDTO recoverItem = new OrderDetailDTO();
recoverItem.setItemId(detail.getItemId());
recoverItem.setNum(-detail.getNum()); // TODO: 注意 数量取反,表示恢复库存
return recoverItem;
})
.collect(Collectors.toList());
// 2.3 调用 deductStock 方法恢复库存
try {
itemClient.deductStock(recoverItems);
} catch (Exception e) {
throw new RuntimeException("恢复库存失败!", e);
}
}
自此超时订单问题改造完成
总结
构建高可靠的RabbitMQ应用,需要从三个层面入手:
- 发送端:开启Confirm和Return机制,确保消息发出去。
- MQ端:开启持久化,必要时使用Lazy Queue应对积压。
- 消费端:开启自动确认与失败重试,配置RepublishRecoverer处理异常消息,并做好幂等性校验。
通过这套组合拳,最大程度地保证了分布式系统中的数据一致性。
