RabbitMQ高级篇总结(黑马微服务课day11)(包含黑马商城业务改造)

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) 配合实现。

实现原理

  1. TTL:给消息设置过期时间,或者给队列设置消息过期时间。
  2. 死信交换机:当队列中的消息因为过期(TTL结束)未被消费时,它会成为"死信"。
  3. 流程
  • 发送消息到 普通队列(没有消费者),设置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-serviceapplication.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应用,需要从三个层面入手:

  1. 发送端:开启Confirm和Return机制,确保消息发出去。
  2. MQ端:开启持久化,必要时使用Lazy Queue应对积压。
  3. 消费端:开启自动确认与失败重试,配置RepublishRecoverer处理异常消息,并做好幂等性校验。

通过这套组合拳,最大程度地保证了分布式系统中的数据一致性。

相关推荐
人道领域2 小时前
SSM框架从入门到入土(SSM框架整合)
java·spring boot·spring
fouryears_234172 小时前
源码阅读:Spring AI 框架是如何进行工具调用以及循环调用的过程
java·人工智能·spring·spring ai
钛态2 小时前
Flutter for OpenHarmony 实战:Supabase — 跨平台后端服务首选
flutter·ui·华为·架构·harmonyos
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于Java的网上书店管理系统为例,包含答辩的问题和答案
java·开发语言
东东5162 小时前
ssm机场网上订票系统 +VUE
java·前端·javascript·vue.js·毕设
倚肆2 小时前
Kafka 生产者与消费者配置详解
java·分布式·后端·kafka
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Java的体育馆管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
听麟2 小时前
HarmonyOS 6.0+ PC端分布式并行计算引擎开发实战:边缘协同场景下的异构资源调度与任务优化
分布式·华为·音视频·harmonyos·政务