Java连接RabbitMQ(SpringBoot版·上)

一:引言

通过《Java连接RabbitMQ(原生版)》一文中,我们了解了RabbitMQ的一些基本的使用方式,如交换机的创建及消息的投递,但是在企业中我们大部分都是把RabbitMQ集成到SpringBoot中的,所以原生的方式我们则不怎么使用到,下面我将和大家一起走入SpringBoot整合RabbitMQ的世界。

<math xmlns="http://www.w3.org/1998/Math/MathML"> 本系列博文主要包含如下: \color{#f00}{本系列博文主要包含如下:} </math>本系列博文主要包含如下:

①:关于RabbitMQ部署和命令使用请参考: 彻底掌握RabbitMQ部署和操作(超详细)

②:使用官方自带Java API 连接RabbitMQ: Java连接RabbitMQ(原生版)

③:将RabbitMQ客户端集成进SpringBoot(上): Java连接RabbitMQ(SpringBoot版.上)

④:将RabbitMQ客户端集成进SpringBoot(下): Java连接RabbitMQ(SpringBoot版.下)
<math xmlns="http://www.w3.org/1998/Math/MathML"> 本文全部代码拉取地址 G i t e e : \color{#f00}{本文全部代码拉取地址Gitee:} </math>本文全部代码拉取地址Gitee:

本文给出的只是核心代码,完整代码请参考Gitee:所有案例完整代码
<math xmlns="http://www.w3.org/1998/Math/MathML"> 关于在生产环境中交换机、队列、路由 K e y 的创建规范说明: \color{#f00}{关于在生产环境中交换机、队列、路由Key的创建规范说明:} </math>关于在生产环境中交换机、队列、路由Key的创建规范说明:

js 复制代码
Exchange 的命名建议格式为:`{业务}.{类型}.exchange`,例如:`order.direct.exchange`。
Queue 的命名建议格式为:`{业务}.{操作}.queue`,例如:`order.create.queue`。
RoutingKey 的命名建议格式为:`{业务}.{操作}`,例如:`order.create`。
🔸 其中,类型可以是:`direct`、`topic`、`fanout`、`headers`...。
🔸 命名时应避免使用拼音、无语义的名称,例如:`test.queue`、`aaa.bbb`、`queue1` 等。
⚠️ 说明:以下示例仅用于测试,**未严格遵循命名规范**!

<math xmlns="http://www.w3.org/1998/Math/MathML"> 下面的所有示例只是核心代码,具体代码得参考完整案例代码!! \color{#f00}{下面的所有示例只是核心代码,具体代码得参考完整案例代码!!} </math>下面的所有示例只是核心代码,具体代码得参考完整案例代码!!

二:入门简单案例

在本示例中,我们将基于Spring Boot集成RabbitMQ客户端 ,构建一个完整的消息发送与接收流程。整体流程如下:通过接口调试工具发送请求至ControllerController调用生产者组件,生产者将消息推送至 Broker(即 RabbitMQ 交换机)。随后,交换机根据路由规则将消息投递到指定队列,最终由相应的消费者监听该队列并消费消息。本示例采用direct(直接)交换机 进行消息路由,以实现基本的消息传输功能。 如下是SpringBoot整合RabbitMQ的基本案例,实现直接交换机模式,具体流程如上图:
下面这些代码是基础代码,关于整体环境请参考我开头给出的案例代码地址:

java 复制代码
src/
└── main
    ├── java
    │ └── cn
    │     └── ant
    │         ├── config
    │         │ └── RabbitMQConfig.java          'RabbitMQ配置(交换机、队列的创建以及他们之间的绑定)'
    │         ├── controller
    │         │ └── SimpleController.java        '基本的Controller,用来接收消息,并调用生产者'
    │         ├── mqHandle
    │         │ ├── SimpleConsumer.java          '消费者'
    │         │ └── SimpleProducer.java          '生产者'
    │         └── HelloWorldDemoApplication.java
    └── resources
        ├── application.yml                      '配置信息'
        └── log4j2.xml                           '日志信息'

'Spring Boot的AMQP启动器,用于整合RabbitMQ,提供消息队列支持。(必须导入)'
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-amqp</artifactId>  
    </dependency>

点开查看详情:RabbitMQ配置信息

java 复制代码
/**
 * 关于Hello World模块的RabbitMQ配置信息
 * 一定需要加@SpringBootConfiguration注解,标注当前为配置类,交由Spring管理。
 * @version 1.0
 **/
@SpringBootConfiguration
public class RabbitMQConfig {
 
    // 定义1:简单的直接交换机名称;定义2:简单队列名称;定义3:路由key
    public static final String SIMPLE_DIRECT_EXCHANGE = "simpleDirectExchange";
    public static final String SIMPLE_QUEUE_NAME = "simpleQueueName";
    public static final String SIMPLE_KEY = "simpleKey";
 
    /***
     * 创建交换机信息;
     * 注:Bean注解内不写名称,则Bean名称默认就是方法名
     * @return Exchange
     */
    @Bean(value = "simpleDirectExchange")
    public Exchange createSimpleDirectExchange() {
        // 这个ExchangeBuilder就是我们当初使用的如下方式一样:
        // channel.exchangeDeclare("交换机名称", "交换机类型",true, false, false, null);
        return ExchangeBuilder
                // 设置交换机类型(方法名已指定出交换机类型了)+交换机名称
                .directExchange(SIMPLE_DIRECT_EXCHANGE)
                // 设置交换机持久化
                .durable(true)
                // 设置交换机自动删除
                // .autoDelete()
                // 是否为内部交换机(true时,不能被生产者直接发送消息,只能由其他交换机发送)
                // .internal()
                // 额外参数(如alternate-exchange设置备用交换机)
                // .withArguments(new HashMap<>())
                // 构建
                .build();
    }
 
    /***
     * 创建队列信息;
     * 注:Bean注解内不写名称,则Bean名称默认就是方法名
     * @return Exchange
     */
    @Bean(value = "simpleQueueName")
    public Queue createSimpleQueueName() {
        // 这个QueueBuilder创建方式就是我们当初使用的如下方式一样:
        // 这下面注释的原生写法和下面的一样
        //      Map<String, Object> args = new HashMap<>();
        //      args.put("x-message-ttl", 30000);  // 消息存活时间 30 秒
        //      args.put("x-max-length", 100);  // 限制队列最大长度 100 条
        //      args.put("x-dead-letter-exchange", "exchangeName");  // 指定死信交换机
        //      args.put("x-dead-letter-routing-key", "dlxKey");  // 指定死信队列的路由键
        //      //对应参数意思:队列名称,持久化,排他队列,自动删除,额外参数
        //      channel.queueDeclare(SIMPLE_QUEUE_NAME,true,true,true,args)
        // 和上面原生方式一样的
        // return QueueBuilder.durable(SIMPLE_QUEUE_NAME)
        //         .autoDelete().ttl(30000).maxLength(100)
        //         .deadLetterExchange("exchangeName")
        //         .deadLetterRoutingKey("dlxKey")
        //         .withArguments(new HashMap<>()).exclusive().build();
        // 下面创建一个简单的消息队列了
        return QueueBuilder.durable(SIMPLE_QUEUE_NAME).build();
        // 说明:QueueBuilder.nonDurable("xxx") 是非持久化的队列
    }
 
    /***
     * 将队列信息绑定到交换机上
     * @param simpleDirectExchange 简单的直接交换机
     * @param simpleQueueName 简单的队列
     * @return Binding
     */
    @Bean(value = "simpleQueueBindSimpleExchange")
    public Binding createSimpleQueueBindSimpleExchange(
            @Qualifier(value = "simpleDirectExchange") Exchange simpleDirectExchange,
            @Qualifier(value = "simpleQueueName") Queue simpleQueueName) {
        // 这个BindingBuilder就是我们当初使用的如下方式一样:
        // channel.queueBind("队列名称", "交换机名称", "路由key");
        return BindingBuilder
                .bind(simpleQueueName).to(simpleDirectExchange).with(SIMPLE_KEY).noargs();
    }
}

点开查看详情:生产者代码

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class SimpleProducer {
 
    // 注入RabbitTemplate模板对象(集成RabbitMQ时发送是通过这个对象)
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者发送消息的方法
     * @param msg 发送的消息(这里的消息可以是任何类型,最终发送时推荐转成JSON字符串)
     */
    public void productionSimpleMessage(String msg) {
        log.info("生产者接收到任务消息,并发送任务消息到指定交换机中....");
        // 发送消息到Brock的交换机
        // 这一步在日常开发中是有数据处理的逻辑的,校验和转换数据的,最终会转成JSON字符串后发送的
        // 为了简洁,我这里传入的任务消息都是字符串,免去了乱七八糟的流程,直接发送
        byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
        // convertAndSend("交换机名称","路由key","发送消息内容"),其实和下面原生的是一样的:
        // channel.basicPublish("交换机名称","路由key","其它参数","消息");
        // 使用convertAndSend默认消息是持久化的,
        // 就如当初原生设置的其它参数:MessageProperties.PERSISTENT_TEXT_PLAIN
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.SIMPLE_DIRECT_EXCHANGE,
                RabbitMQConfig.SIMPLE_KEY, bytes);
    }
}

点开查看详情:消费者代码

java 复制代码
@Slf4j
@Component
public class SimpleConsumer {
 
    /***
     * 简单消息处理(监听);@RabbitListener注解就是用来监听对应的队列
     * @param msgData 监听消息队列中下消息(生产者发送什么类型数据,这里就用对应类型接收,一般都是JSON字符串)
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     */
    @RabbitListener(queues = {RabbitMQConfig.SIMPLE_QUEUE_NAME}) 
    public void messageSimpleHandle(String msgData, Message message, Channel channel) throws IOException {
        // message可获取消息体(body)和 消息属性(MessageProperties)
        //   message.getBody():
        //      存储消息的具体内容(序列化后的二进制数据)
        //   message.getMessageProperties():
        //      存储RabbitMQ相关的消息属性(如contentType、exchange、routingKey、headers等)
        // 获取到队列消息,因为之前发送是字符串格式,我们也不用将字符串转JSON对象了。
        String receiveMsg = new String(message.getBody(), StandardCharsets.UTF_8);
        // channel参数就是RabbitMQ Java客户端(amqp-client 库)中的核心接口,负责与RabbitMQ服务器进行通信。
        // 它用于创建交换机、声明队列、发布消息、消费消息 等操作。
        // 比如我们下面的消息处理失败时通过channel.basicPublish(...)也是可以的;但是一般不使用
        log.info("消息由消费者消费:{},并消费完成", receiveMsg);
    }
}

点开查看详情:application.yml配置

yml 复制代码
server:
  port: 8081
spring:
  ## 具体RabbitMQ配置请参考:org.springframework.boot.autoconfigure.amqp.RabbitProperties
  rabbitmq:
    host: 111.231.171.158
    port: 5672
    username: rabbit
    password: 1234
    virtual-host: nativeStudy

三:整合RabbitMQ的常用配置信息

按照spring-boot-starter-amqp 3.4.xRabbitProperties源码,可以将他整理成了标准的YAML格式。后期需要啥配置信息我们可以直接找。

yml 复制代码
spring:
  ## 具体RabbitMQ配置请参考:org.springframework.boot.autoconfigure.amqp.RabbitProperties
  rabbitmq:
    # ================= Base =================
    host: 111.231.171.158         # RabbitMQ 服务主机名,默认 localhost
    port: 5672                    # RabbitMQ 端口,默认 5672
    username: rabbit              # 用户名,默认 guest
    password: 1234                # 密码,默认 guest
    virtual-host: nativeStudy     # 虚拟主机路径,默认 /
    addresses: localhost:5672     # 可用地址列表,多个用逗号分隔,优先级高于 host/port
    requested-heartbeat: 60s      # 心跳时间,默认 60s,0 表示不设置
    publisher-confirm-type: correlated  # 发布确认类型:none/simple/correlated(建议用correlated)
    publisher-returns: true       # 启用发布返回,默认 false
    connection-timeout: 3000ms    # 连接超时时间,默认 null(即无限)

点开查看详情:其他属性配置

yml 复制代码
spring:
  rabbitmq:
    # ================= SSL =================
    ssl:
      enabled: false                            # 是否启用 SSL
      key-store: classpath:keystore.jks         # SSL key store 路径
      key-store-password: changeit              # key store 密码
      trust-store: classpath:truststore.jks     # trust store 路径
      trust-store-password: changeit            # trust store 密码
      algorithm: TLSv1.2                        # 默认算法是 TLSv1.2
    # ================= Cache =================
    cache:
      channel:
        size: 25                 # 缓存中的 channel 数量,默认 25
        checkout-timeout: 0ms    # 从缓存中获取channel的超时,0表示始终创建新channel
      connection:
        size: 10                 # 缓存连接数量(仅 CONNECTION 模式下生效)
        mode: CHANNEL            # 可选:CHANNEL 或 CONNECTION,默认 CHANNEL
    # ================= Listener(Simple) =================
    listener:
      simple:
        auto-startup: true       # 是否自动启动监听容器,默认 true
        acknowledge-mode: auto   # 消息确认模式:none/manual/auto,默认 auto
        concurrency: 2           # 最少需要2个消费者来监听同一队列,默认:null
        max-concurrency: 10      # 最多只能拥有10个消费者来监听同一队列,默认:null
        prefetch: 5              # 每个消费者可以处理的未确认消息的最大数量,默认:null;可以设置预取值
        default-requeue-rejected: true      # 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
        idle-event-interval: 60000ms        # 空闲事件间隔,默认:null
        # 重试机制
        retry:
          enabled: true                 # 启用重试,默认 false
          max-attempts: 3               # 最大重试次数
          initial-interval: 1000ms      # 初始重试间隔
          multiplier: 2.0               # 间隔乘数
          max-interval: 10000ms         # 最大重试间隔
          stateless: true               # 是否为无状态重试,默认:true
    # ================= Template =================
    template:
      mandatory: true              # 是否强制消息投递(必须被消费者接收),默认:false
      receive-timeout: 1000ms      # receive 操作的超时时间,默认:null
      reply-timeout: 5000ms        # sendAndReceive 操作的超时时间,默认:5000ms
      retry:
        enabled: true              # 启用发送重试,默认:false
        max-attempts: 3            # 最大重试次数
        initial-interval: 1000ms   # 初始重试间隔
        multiplier: 2.0            # 间隔乘数
        max-interval: 10000ms      # 最大重试间隔

四:工作队列+消息应答+消息分发

生产者(Producer)将消息发送至一个Direct类型的交换机(Direct Exchange),交换机根据指定的路由键(Routing Key)将消息路由至目标队列(Queue)。随后多个消费者(Consumers)并发地从该队列中接收并处理消息,实现工作队列(Work Queue)模型。每条消息仅由其中一个消费者进行处理。

下文各小节将分别介绍该流程中涉及的消息应答机制(Message Acknowledgment)消息分发策略(Message Dispatching),并附有相应的流程图示意。

(一):普通工作队列(轮询)

在以下示例中,消息首先被发送至指定到交换机(Exchange),随后根据交换机与队列之间绑定的路由键(Routing Key),路由至对应的消息队列(Queue)。该队列再按照工作队列(Work Queue)模式,将消息以轮询方式推送给多个消费者,实现消息的并发处理和负载均衡。

java 复制代码
├── java
│ └── cn
│     └── ant
│         ├── config
│         │ └── RabbitMQConfig.java         'RabbitMQ的一些配置'
│         ├── controller
│         │ └── TestController.java         '用来接收信息的Controller'
│         ├── entity
│         │ └── MessageSendDTO.java         '封装的发送消息的实体'
│         ├── mqHandle
│         │ ├── ProducerSend.java           '消息发送(生产者)'
│         │ └── QueueConsumer.java          '消息消费(消费者)'
│         └── WorkQueuesAckDemoApplication.java     
└── resources
    ├── application.yml
    └── log4j2.xml

点开查看详情:封装的发送消息的实体

java 复制代码
/**
 * 用来测试RabbitMQ的生产者发送消息(对象)到消费者中的一系列传输
 * JDK14引入的@Serial注解,标记一个字段或方法是序列化相关的,供编译器进行检查,避免书写错误或遗漏。
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Data
@Builder
public class MessageSendDTO implements Serializable {
 
    @Serial
    private static final long serialVersionUID = 5905249092659173678L;
 
    private Integer msgID;          // 消息ID
    private String msgType;         // 消息类型
    private Object msgBody;         // 消息体
}

点开查看详情:RabbitMQ的一些配置

java 复制代码
/**
 * RabbitMQ配置,工作队列DEMO
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@SpringBootConfiguration
public class RabbitMQConfig {
 
    // 直接交换机名称
    public static final String DIRECT_EXCHANGE = "directExchange";
    // 队列名称A
    public static final String QUEUE_A_NAME = "queueAName";
    // 路由key
    public static final String ROUTE_KEY = "routeKeyName";
 
    /***
     * 创建直接交换机
     * @return Exchange
     */
    @Bean(name = "directExchange")
    public Exchange createDirectExchange() {
        return ExchangeBuilder.directExchange(DIRECT_EXCHANGE).durable(true).build();
    }
 
    /***
     * 创建队列A信息
     * @return Queue
     */
    @Bean("queueAName")
    public Queue createQueueAName() {
        return QueueBuilder.durable(QUEUE_A_NAME).build();
    }
 
    /***
     * 队列绑定到交换机上,通过路由key
     * @param directExchange 交换机信息
     * @param queueAName A队列绑定
     * @return Binding
     */
    @Bean("directExchangeBindAQueue")
    public Binding directExchangeBindQueueAName(
            @Qualifier(value = "directExchange") Exchange directExchange,
            @Qualifier(value = "queueAName") Queue queueAName) {
        return BindingBuilder.bind(queueAName).to(directExchange).with(ROUTE_KEY).noargs();
    }
}

点开查看详情:消息发送(生产者)

java 复制代码
/**
 * 生产者;用来发生消息
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    // 注入rabbitTemplate对象
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者发送方式
     * @param msg 发送的消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        // 消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.DIRECT_EXCHANGE, RabbitMQConfig.ROUTE_KEY, bytes);
    }
}

点开查看详情:消息消费(消费者);2个消费者A和B

java 复制代码
/**
 * 队列的消费者;这是监听队列A的消费者(A、B)
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Slf4j
@Component
public class QueueConsumer {
 
    /***
     * 消费者A(监听)队列queueAName
     * @param msgData 传递的具体消息,最好是生产者发送使用什么类型,这里接收就用什么类型
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     */
    // @RabbitListener监听队列;若监听多个则在queues的{}里面逗号分割;
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_A_NAME})
    public void messageSimpleHandleA(@Payload String msgData, // 这个是生产者发送的JSON消息
                                     Message message,
                                     Channel channel) throws InterruptedException {
        // 获取到队列消息,因为发送是JSON格式,我们要解析对象格式
        // message.getBody():存储消息的具体内容(序列化后的二进制数据)
        String msgJsonStr = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageSendDTO msg = JSONObject.parseObject(msgJsonStr, MessageSendDTO.class);
        //假设消费者A处理消息慢,每8秒处理一条
        Thread.sleep(8000);
        log.info("A:消息由消费者A消费:{},并消费完成", msg);
    }
 
    /***
     * 消费者B(监听)队列queueAName
     * @param msgData 传递的具体消息,最好是生产者发送使用什么类型,这里接收就用什么类型
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     */
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_A_NAME})
    public void messageSimpleHandleB(@Payload String msgData, // 这个是生产者发送的JSON消息
                                     Message message,
                                     Channel channel) throws InterruptedException {
        // 获取到队列消息,因为发送是JSON格式,我们要解析对象格式
        // message.getBody():存储消息的具体内容(序列化后的二进制数据)
        String msgJsonStr = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageSendDTO msg = JSONObject.parseObject(msgJsonStr, MessageSendDTO.class);
        // 假设消费者B处理消息快,每2秒处理一条
        Thread.sleep(2000);
        log.info("B:消息由消费者B消费:{},并消费完成", msg);
    }
}

点开查看详情:消息接收Controller

java 复制代码
@Slf4j  // 使用lombok自带的日志注解,具体实现是slf4j+log4j2
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
 
    // 写了如下就不用写@Slf4j了
    // 使用SLF4J来获取Logger对象;(注意导包:import org.slf4j.Logger; import org.slf4j.LoggerFactory;)
    // Logger log = LoggerFactory.getLogger(this.getClass());
 
    // 注入生产者对象
    private final ProducerSend producerSend;
 
    /***
     * 基本的get请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msgStr 请求消息
     * @return String
     */
    @GetMapping("/produce")
    public String msgSend(String msgStr) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{}", msgStr);
        // 循环发送消息
        for (int i = 48; i gt;= 57; i++) {
            MessageSendDTO build = MessageSendDTO.builder().msgID(i)
                    .msgType("testType")
                    .msgBody(
                            msgStr + "--" + new String(new char[]{(char) i, (char) i})
                    ).build();
            //发送消息
            producerSend.producerSendMsg(build);
        }
        return "请求发送成功,并已接收";
    }
}

执行http://127.0.0.1:8081/test/produce?msgStr=BusinessNewsYES发送消息任务到Controller后,随后Controller会去调用对应的生产者来发送消息任务,具体日志如下:

  从上述日志可以看出,消息队列将任务消息分别分发给了消费者A和消费者B。其中,消费者A处理每条消息大约需要8秒,而消费者B处理同样的业务仅需2秒。由于默认采用的是轮询分发(Round-Robin Dispatch)策略,消息被平均分配,每个消费者各自接收了5条消息,但这也导致了处理速度较慢的消费者A成为系统瓶颈*,从而影响整体的消息消费效率。

这显然并不符合我们在生产环境中的期望:我们更希望根据消费者的处理能力动态分配任务,即处理速度更快的消费者应当承担更多的消息处理任务

为了实现更合理的消息分发机制,我们需要对当前代码进行进一步优化,启用手动消息确认并配置预取数量(prefetchCount),以实现基于消费者处理能力的动态消息调度(又称公平分发,Fair Dispatch)

(二):消息分发(预取值)

我在《Java连接RabbitMQ(原生版)》这篇博客中详细介绍过了什么是预取值;在这里我直接进行设置预取值,需要修改具体如下配置:

yml 复制代码
spring:
  rabbitmq:
    host: 111.231.171.158
    port: 5672
    username: rabbit
    password: 1234
    virtual-host: nativeStudy
    # 消息监听器容器相关配置
    listener:
      simple:
        prefetch: 1   # 设置预取值为1(当为1时也被称为不公平分发;prefetch默认值为250)
        # 此时每个消费者最多只能同时处理1条未确认的消息。
        # 只有在消费者显式确认(ACK)当前消息后,RabbitMQ才会向该消费者分发下一条消息。
java 复制代码
'为什么会设置 prefetch=1 ?'
  这是实现公平分发(Fair Dispatch)的关键参数。
  默认情况下(未设置 prefetch),RabbitMQ会按照轮询方式平均把消息推送给消费者,不管他们是否忙碌。
  如果你设置prefetch=1,RabbitMQ就会等待消费者处理完前一条消息并发送ACK后,再发送下一条。
  这样做可以避免消费者堆积未处理的消息,实现更合理的任务分配。

'假设有两个消费者A和B(共20个任务)'
  A每秒只能处理1条消息;B每秒能处理10条消息;
  如果没有设置prefetch=1,RabbitMQ会直接给A和B各发10条任务,结果A会堆积处理不完,而B处理完了还在等。
  但如果设置了prefetch=1,RabbitMQ就会按消费者的ACK响应速度来继续推送任务,
    B处理得快就能多处理,A处理得慢就少处理,从而实现动态均衡分发。

注意:要实现这个"公平分发"的效果,除了设置prefetch=1,还需要开启手动确认模式。

(三):消息接收应答

完成上面的prefetch=1配置后,我们再来看Spring Boot集成RabbitMQ时的默认消息确认机制:他使用的是acknowledge-mode: auto自动确认模式。具体行为是:消息一旦投递到消费者,如果消费者成功执行完业务逻辑且没有抛出异常,RabbitMQ就会自动发送ACK,并将该消息从队列中移除。但需要注意的是:

①:如果消费者方法在执行过程中抛出了异常 ,Spring会认为消息未成功消费 ,不会自动确认;②:这时消息会被重新投递,或者进入死信队列,具体行为取决于队列是否配置了死信机制或重试策略。

自动确认模式虽然在代码层面无需手动干预,但在业务处理逻辑中一定要注意异常处理,否则可能导致消息重复消费或堆积。

若希望完全控制消息何时被确认或拒绝,建议用acknowledge-mode: manual手动确认模式,如下:

yml 复制代码
spring:
  ## 具体RabbitMQ配置请参考:org.springframework.boot.autoconfigure.amqp.RabbitProperties
  rabbitmq:
    host: 111.231.171.158
    port: 5672
    username: rabbit
    password: 1234
    virtual-host: nativeStudy
    # 消息监听器容器相关配置
    listener:
      simple:
        prefetch: 1   # 设置预取值为1(当为1时也被称为不公平分发;prefetch默认值为250)
        acknowledge-mode: manual  # 设置消息应答模式

'=============== 消息确认模式(acknowledge-mode)说明:=============== '
'acknowledge-mode: none'
    # 表示关闭消息确认机制。消息一旦投递到消费者,即视为已处理完成,无论是否成功执行。
    # ⚠️不推荐用于生产环境,容易导致消息丢失。
'acknowledge-mode: auto'
    # 默认模式。Spring会根据消息监听方法是否抛出异常来判断是否确认:
        # 如果方法成功执行(无异常),则自动发送ACK;
        # 如果方法抛出异常,则不确认,消息将重新投递或进入死信队列。
'acknowledge-mode: manual'
    # 手动确认模式。需要在代码中手动调用channel.basicAck() / basicNack()等方法来确认或拒绝消息。(推荐)
        # 适用于对消息可靠性要求较高的场景;
        # 可配合业务执行结果决定是否确认消息。

改造消费者代码为手动应答模式:
点开查看详情:改造成手动确认的消费者代码

java 复制代码
@Slf4j
@Component
public class QueueConsumer {
 
    /***
     * 消费者A(监听)队列queueAName
     * @param msgData 传递的具体消息,最好是生产者发送使用什么类型,这里接收就用什么类型
     * @param deliveryTag 处理消息的编号(可通过message.getMessageProperties().getDeliveryTag()拿到)
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     */
    // @RabbitListener监听队列;若监听多个则在queues的{}里面逗号分割;
    // ackMode设置应答模式优先级是比yml上配置的acknowledge-mode优先级高
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_A_NAME}, ackMode = "MANUAL")
    public void messageSimpleHandleA(@Payload String msgData, // 这个是生产者发送的JSON消息
                                     @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                                     Message message,
                                     Channel channel) throws InterruptedException, IOException {
        // 获取到队列消息,因为发送是JSON格式,我们要解析对象格式
        // message.getBody():存储消息的具体内容(序列化后的二进制数据)
        String msgJsonStr = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageSendDTO msg = JSONObject.parseObject(msgJsonStr, MessageSendDTO.class);
        //假设消费者A处理消息慢,每8秒处理一条
        Thread.sleep(8000);
        log.info("A:消息由消费者A消费:{},并消费完成", msg);
        // 手动确认,注:这个deliveryTag可以通过message.getMessageProperties().getDeliveryTag()拿到
        channel.basicAck(deliveryTag, false);
    }
 
    /***
     * 消费者B(监听)队列queueAName
     * @param msgData 传递的具体消息,最好是生产者发送使用什么类型,这里接收就用什么类型
     * @param deliveryTag 处理消息的编号(可通过message.getMessageProperties().getDeliveryTag()拿到)
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     */
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_A_NAME}, ackMode = "MANUAL")
    public void messageSimpleHandleB(@Payload String msgData, // 这个是生产者发送的JSON消息
                                     @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                                     Message message,
                                     Channel channel) throws InterruptedException, IOException {
        // 获取到队列消息,因为发送是JSON格式,我们要解析对象格式
        // message.getBody():存储消息的具体内容(序列化后的二进制数据)
        String msgJsonStr = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageSendDTO msg = JSONObject.parseObject(msgJsonStr, MessageSendDTO.class);
        // 假设消费者B处理消息快,每2秒处理一条
        Thread.sleep(2000);
 
        //模拟判断我是否需要手动确认(若随机不是2则确认消费,否则拒绝,继续交由队列)
        if (Math.ceil(Math.random() * 4) != 2) {
            log.info("B:消息由消费者B消费:{},并消费完成", msg);
            //手动确认
            channel.basicAck(deliveryTag, false);
        } else {
            log.info("B:消息由消费者B消费:{},并消费失败,丢回队列", msg);
            // 消息编号我们也可以通过message取出来,不用deliveryTag,在message可以获取更多的信息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

  从上面的日志可以看出,消费者B是每2秒处理一个任务,但任务可能会失败,失败则会拒绝消息被消费,而且会重新投递到队列中;消费者A则就不一样,每8秒执行一个任务,执行完任务后再次等待队列推送任务给消费者A执行。

五:扇出交换机(Fanout 发布订阅)

扇出交换机是RabbitMQ中的一种交换机类型,它会将接收到的消息广播给所有与之绑定的队列,不考虑路由键(routing key)。在上篇博客中已经介绍了过了,这里直接就说明如何在SpringBoot里整合RabbitMQ来实现扇出交换机,具体案例需要实现的示意图如下:

  说明:配置yml文件(剔除预取值和消息确认模式)和MessageSendDTO实体是未改动的。
点开查看详情:RabbitMQ的一些配置

java 复制代码
/**
 * 扇出交换机配置
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Configuration
public class RabbitMQConfig {
 
    // 扇出交换机名称
    public static final String EXCHANGE_NAME = "fanoutDemo";
    // 创建两个消息队列
    public static final String QUEUE_A = "queueA";
    public static final String QUEUE_B = "queueB";
 
    /***
     * 创建扇出交换机信息
     * @return Exchange
     */
    @Bean("fanoutDemo")
    public Exchange createFanoutDemo() {
        return ExchangeBuilder.fanoutExchange(EXCHANGE_NAME).durable(true).build();
    }
 
    /***
     * 创建队列A
     * @return Queue
     */
    @Bean("queueA")
    public Queue createQueueA() {
        return QueueBuilder.durable(QUEUE_A).build();
    }
 
    /***
     * 创建队列B
     * @return Queue
     */
    @Bean("queueB")
    public Queue createQueueB() {
        return QueueBuilder.durable(QUEUE_B).build();
    }
 
    /***
     * 队列A绑定到扇出交换机
     * @param fanoutDemo 交换机名称
     * @param queueA 队列A
     * @return Binding
     */
    @Bean("fanoutBindQueueA")
    public Binding fanoutBindQueueA(@Qualifier(value = "fanoutDemo") Exchange fanoutDemo,
                                    @Qualifier(value = "queueA") Queue queueA) {
        return BindingBuilder.bind(queueA).to(fanoutDemo).with("").noargs();
    }
 
    /***
     * 队列B绑定到扇出交换机
     * @param fanoutDemo 交换机名称
     * @param queueB 队列B
     * @return Binding
     */
    @Bean("fanoutBindQueueB")
    public Binding fanoutBindQueueB(@Qualifier(value = "fanoutDemo") Exchange fanoutDemo,
                                    @Qualifier(value = "queueB") Queue queueB) {
        return BindingBuilder.bind(queueB).to(fanoutDemo).with("").noargs();
    }
}

点开查看详情:生产者代码和消费者代码

java 复制代码
// 消息生产者
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法
     * @param msg 消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        // 消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "", bytes);
        log.info("生产者发送信息完成,已经交由给交换机.....");
    }
}
 
// 消费者A和消费者B
@Slf4j
@Component
@RequiredArgsConstructor
public class QueueConsumer {
 
    /***
     * 消费者A;监听队列:queueA;设置自动确认
     */
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_A}, ackMode = "AUTO")
    public void testConsumerA(@Payload String msgData, // 这个是生产者发送的JSON消息
                              Message message,
                              Channel channel) {
        log.info("接收到队列A信息......;信息为:{}",
            JSONObject.parseObject(msgData, MessageSendDTO.class));
    }
 
 
    /***
     * 消费者B;监听队列:queueB;设置自动确认
     */
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_B}, ackMode = "AUTO")
    public void testConsumerB(@Payload String msgData, // 这个是生产者发送的JSON消息
                              Message message,
                              Channel channel) {
        // 注:若消费失败(报错)会自动手动不确认,并且把消息放到队列中,然后又被这个队列消费,最终死循环
        // 所以在消费消息的时候最好的方式就是手动确认,若失败则投递到死信交换机或者写入日志后直接丢弃。
        // int a = 1 / 0;
        log.info("接收到队列B信息......;信息为:{}", 
            JSONObject.parseObject(msgData, MessageSendDTO.class));
    }
}

点开查看详情:测试消息接收并调用生产者

java 复制代码
/**
 * 测试消息接收并调用生产者
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
 
    // 注入生产者对象
    private final ProducerSend producerSend;
 
    /***
     * 基本的get请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msg 请求消息
     * @return String
     */
    @GetMapping("/produce")
    public String msgSend(MessageSendDTO msg) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{}", msg);
        // 发送消息
        producerSend.producerSendMsg(msg);
        return "请求发送成功,并已接收";
    }
}

到这里我们代码就写完了;扇出交换机主要的关注点在配置方面,其它没啥变化,写完代码后启动服务就会进行交换机和队列A和队列B的创建了。

浏览器执行如下发送到Controller:
http://127.0.0.1:8081/test/produce?msgID=16&msgType=fonoutType&msgBody=YesYesLaLa

六:主题交换机(Topics 匹配模式)

主题交换机是一种强大的交换机类型,支持使用通配符(wildcard)进行模式匹配的路由键,这个路由键可以决定消息该路由到哪个队列里。消息根据routing key与队列绑定时指定的匹配模式进行匹配。他支持两种通配符,分别如下:

*(星号):匹配一个单词 #(井号):匹配零个或多个单词

使用方式:routing key使用.分隔的多个单词组成,如:log.error.system;例如:log.info.*表示订阅所有 info级别的日志,log.#表示订阅所有日志消息。

具体的主题交换机的介绍在上篇博客中已经给出介绍了,这里我只是使用SpringBoot整合RabbitMQ来实现一下,按照上篇的图示实现:


点开查看详情:RabbitMQ的一些配置

java 复制代码
/**
 * RabbitMQ配置主题交换机配置
 * @author Anhui AntLaddie (掘金蚂蚁小哥)
 * @version 1.0
 **/
@Configuration
public class RabbitMQConfig {
 
    // 交换机名称
    public static final String TOPIC_EXCHANGE = "TopicExchange";
    // 队列Q1名称
    public static final String Q1 = "Q1Queue";
    // 队列Q2名称
    public static final String Q2 = "Q2Queue";
    // 路由绑定关系 Routing Key
    public static final String Q1_KEY = "*.orange.*";
    // 路由绑定关系 Routing Key 1
    public static final String Q2_KEY_A = "*.*.rabbit";
    // 路由绑定关系 Routing Key 2
    public static final String Q2_KEY_B = "lazy.#";
 
    /***
     * 主题交换机
     * @return Exchange
     */
    @Bean("topicExchange")
    public Exchange topicExchange() {
        return ExchangeBuilder.topicExchange(TOPIC_EXCHANGE).durable(true).build();
    }
 
    /***
     * 队列1信息
     * @return Queue
     */
    @Bean("q1Queue")
    public Queue q1Queue() {
        return QueueBuilder.durable(Q1).build();
    }
 
    /***
     * 队列2信息
     * @return Queue
     */
    @Bean("q2Queue")
    public Queue q2Queue() {
        return QueueBuilder.durable(Q2).build();
    }
 
    /***
     * 绑定关系,Q1Queue队列绑定的匹配路由为*.orange.*
     * @param topicExchange 交换机
     * @param q1Queue 队列1
     * @return Binding
     */
    @Bean("bindingA")
    public Binding bindingA(@Qualifier("topicExchange") Exchange topicExchange,
                            @Qualifier("q1Queue") Queue q1Queue) {
        return BindingBuilder.bind(q1Queue).to(topicExchange).with(Q1_KEY).noargs();
    }
 
    /***
     * 绑定关系,Q2Queue队列绑定的匹配路由为*.*.rabbit
     * @param topicExchange 交换机
     * @param q2Queue 队列2
     * @return Binding
     */
    @Bean("bindingB1")
    public Binding bindingB1(@Qualifier("topicExchange") Exchange topicExchange,
                             @Qualifier("q2Queue") Queue q2Queue) {
        return BindingBuilder.bind(q2Queue).to(topicExchange).with(Q2_KEY_A).noargs();
    }
 
    /***
     * 绑定关系,Q2Queue队列绑定的匹配路由为lazy.#
     * @param topicExchange 交换机
     * @param q2Queue 队列2
     * @return Binding
     */
    @Bean("bindingB2")
    public Binding bindingB2(@Qualifier("topicExchange") Exchange topicExchange,
                             @Qualifier("q2Queue") Queue q2Queue) {
        return BindingBuilder.bind(q2Queue).to(topicExchange).with(Q2_KEY_B).noargs();
    }lt; gt;
 
//    /***
//     * 这种方式就是一次性创建多个Bing操作;一个方法绑定了Q2_KEY_A和Q2_KEY_B
//     * @param topicExchange 交换机信息
//     * @param q2Queue 队列信息
//     * @return List<Binding>
//     */
//    @Bean("q2Bindings")
//    public List<Binding> q2Bindings(@Qualifier("topicExchange") Exchange topicExchange,
//                                    @Qualifier("q2Queue") Queue q2Queue) {
//        // 将所有的绑定信息以集合的方式返回了,取容器的时候必须以getBean("q2Bindings");
//        // 这种方式取出的类型是List<Binding>集合。
//        // @SuppressWarnings("unchecked") 消除警告。
//        // List<Binding> bindings = (List<Binding>) application.getBean("q2Bindings");
//        return List.of(
//                BindingBuilder.bind(q2Queue).to(topicExchange).with(Q2_KEY_A).noargs(),
//                BindingBuilder.bind(q2Queue).to(topicExchange).with(Q2_KEY_B).noargs()
//        );
//    }
}

点开查看详情:生产者代码和消费者代码

java 复制代码
/**
 * 生产者代码;这里会以各种路由方式发送,然后推送到对应的消息队列中
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法
     */
    public void producerSendMsg() {
        // 消息任务准备
        HashMap<String, String> sendMsg = new HashMap<>();
        sendMsg.put("quick.orange.rabbit", "被队列 Q1 Q2 接收到");
        sendMsg.put("lazy.orange.elephant", "被队列 Q1 Q2 接收到");
        sendMsg.put("quick.orange.fox", "被队列 Q1 接收到");
        sendMsg.put("lazy.brown.fox", "被队列 Q2 接收到");
        sendMsg.put("lazy.pink.rabbit","虽然满足两个绑定规则但两个规则都是在Q2队列,所有只要Q2接收一次");
        sendMsg.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
        sendMsg.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
        sendMsg.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
        // 循环发送消息任务
        for (Map.Entry<String, String> msg : sendMsg.entrySet()) {
            String routKey = msg.getKey();  // 主题路由key
            String message = msg.getValue();// 消息任务
            // 创建对象
            MessageSendDTO build = MessageSendDTO.builder()
                    .msgBody("基本信息:" + message + " 路由信息:" + routKey).build();
            // 消息转换为JSON格式并转为字节数组
            byte[] bytes = JSONObject.toJSONString(build).getBytes(StandardCharsets.UTF_8);
            // 发送消息
            rabbitTemplate.convertAndSend(RabbitMQConfig.TOPIC_EXCHANGE, routKey, bytes);
        }
        log.info("生产者发送信息完成,已经交由给交换机.....");
    }
}
 
// ============================ 
/**
 * 2个消费者分别接收队列Q1Queue和Q2Queue
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class QueueConsumer {
 
    /***
     * 消费者1
     */
    @RabbitListener(queues = {RabbitMQConfig.Q1})
    public void testConsumerA(@Payload String msgData, // 这个是生产者发送的JSON消息
                              Message message,
                              Channel channel) {
        log.info("接收到队列1信息;信息为:{}", 
        JSONObject.parseObject(msgData, MessageSendDTO.class));
    }
 
    /***
     * 消费者2
     */
    @RabbitListener(queues = {RabbitMQConfig.Q2})
    public void testConsumerB(@Payload String msgData, // 这个是生产者发送的JSON消息
                              Message message,
                              Channel channel) {
        log.info("接收到队列2信息...;信息为:{}", 
        JSONObject.parseObject(msgData, MessageSendDTO.class));
    }
}

还是那句话,配置yml文件(剔除预取值和消息确认模式)和MessageSendDTO实体是未改动的。接下来我们需要运行代码,我这里是在启动里加了调用生产者的方法,具体如下:

java 复制代码
    public static void main(String[] args) {
        ConfigurableApplicationContext application = SpringApplication
                .run(TopicExchangeDemoApplication.class, args);
        // 获取Bean容器并调用生产者
        ProducerSend producerSend = application.getBean("producerSend", ProducerSend.class);
        producerSend.producerSendMsg();
    }

  其实运行后就会调用生产者发送任务到交换机,具体运行日志如下:

七:死信队列(重要必看)

"死信队列"(Dead Letter Queue,简称 DLQ)是RabbitMQ中的一种特殊队列,用来接收无法被正常消费的消息。产生死信消息共有三种,具体如下:

1. 队列满了,但还继续有消息投递进来(队列长度限制)。

2. 消息过期,设置了TTL(生存时间),超过时间还没消费。

3. 消息被拒绝,且没有被重新投回队列(basic.rejectbasic.nack,并且requeue=false)。

出现如上的这三种情况后,消息不会被丢弃,而是可以通过死信交换机(Dead Letter Exchange,DLX)重新路由到另一个专门的"死信队列"中。

其实我在之前已经详细介绍了什么是死信队列,这里我将在SpringBoot里面整合RabbitMQ来实现死信队列,具体流程图如下:

  按照如上的示意图,我们编写了具体代码结构如下:

java 复制代码
./main/
├── java
│ └── cn
│     └── ant
│         ├── config
│         │ └── RabbitMQConfig.java             '交换机和队列的创建和绑定关系'
│         ├── controller
│         │ └── TestController.java             '调用Controller,拿到请求数据后调用生产者'
│         ├── DxlQueueDemoApplication.java      '启动类入口'
│         ├── entity
│         │ └── MessageSendDTO.java             '封装的消息对象'
│         └── mqHandle
│             ├── consumer
│             │ ├── DXLConsumer.java            '死信消费者'
│             │ └── QueueMsgConsumer.java       '普通队列消费者'
│             └── producer
│                 └── ProducerSend.java         '生产者'
└── resources
    ├── application.yml
    └── log4j2.xml

点开查看详情:RabbitMQ一些配置信息,交换机、队列等创建和绑定

java 复制代码
/**
 * RabbitMQ一些配置信息,交换机、队列等创建和绑定
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Configuration
public class RabbitMQConfig {
 
    // 直接交换机名称
    public static final String EXCHANGE_NAME = "MsgHandleExchange";
    // 队列名称
    public static final String QUEUE_NAME = "MsgHandleQueue";
    // 路由key
    public static final String ROUTING_KEY = "MsgHandleKey";
    // 声明死信交换机名称
    public static final String DLX_EXCHANGE = "DLXExchange";
    // 声明死信队列名称
    public static final String DLX_QUEUE = "DLXQueue";
    // 声明路由绑定关系 Routing Key 死信交换机到死信队列
    public static final String DLX_KEY = "DLXKey";
 
    //+++++++++++++++++配置了直连交换机和队列的关系
 
    /***
     * 一个普通的直连交换机
     * @return Exchange
     */
    @Bean("msgHandleExchange")
    public Exchange msgHandleExchange() {
        // 第一种方式:使用new的方式,是什么类型交换机我们就创建什么xxxExchange
        //  参数1:exchange: 交换机名称
        //  参数2:durable: 是否需要持久化
        //  参数3:autoDelete: 当最后一个绑定到Exchange上的队列删除后,自动删除该Exchange
        //  参数4:arguments: 扩展参数,用于扩展AMQP协议定制化使用
        // Exchange directExchange = new DirectExchange(EXCHANGE_NAME,true,false,null);
        // 当前Exchange是否用于RabbitMQ内部使用,默认为False
        // 使用默认即可directExchange.isInternal();
 
        //第二种方式:使用Builder方式
        return ExchangeBuilder
                .directExchange(EXCHANGE_NAME)    // 交换机名称
                .durable(true)                    // 是否需要持久化
                .autoDelete()                     // 交换机自动删除
                //.withArguments(null)              // 扩展参数
                .build();
    }
 
    /***
     * 一个普通的队列
     * @return Queue
     */
    @Bean("msgHandleQueue")
    public Queue msgHandleQueue() {
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~设置死信参数Start
        // 绑定死信队列(参数设置)
        Map<String, Object> arguments = new HashMap<>();
        // 正常队列设置死信交换机 参数key是固定值;(就是说死去的消息发送到哪个交换机)
        arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
        // 正常队列设置死信交换机到死信队列绑定Routing Key
        // 参数key是固定值(就是说死去的消息在交换机里通过什么路由发送到死信队列)
        arguments.put("x-dead-letter-routing-key", DLX_KEY);
        // 设置正常队列的长度限制 为3
        // arguments.put("x-max-length",3);
        // 队列设置消息过期时间 60 秒
        // arguments.put("x-message-ttl", 60 * 1000);
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~设置死信参数End
 
        // 第一种方式:使用new的方式;这种和原生创建一样
        //   参数一:队列名称
        //   参数二:队列里的消息是否持久化,默认消息保存在内存中,默认false
        //   参数三:该队列是否只供一个消费者进行消费的独占队列,
        //           为true(仅限于此连接),false(默认,可以多个消费者消费)
        //   参数四:是否自动删除,最后一个消费者断开连接以后,该队列是否自动删除true自动删除,默认false
        //   参数五:构建队列的其它属性,看下面扩展参数
        // Queue queue = new Queue(QUEUE_NAME,true,false,false,arguments);
        // 原生: channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
 
        // 第二种方式:使用Builder方式
        return QueueBuilder.durable(QUEUE_NAME).withArguments(arguments).build();
    }
 
    /***
     * 队列绑定到交换机
     * @param msgHandleExchange 交换机名称
     * @param msgHandleQueue 队列名称
     * @return Binding
     */
    @Bean("queueBindDirectExchange")
    public Binding queueBindDirectExchange(
            @Qualifier("msgHandleExchange") Exchange msgHandleExchange,
            @Qualifier("msgHandleQueue") Queue msgHandleQueue) {
        return BindingBuilder
                .bind(msgHandleQueue).to(msgHandleExchange).with(ROUTING_KEY).noargs();
    }
 
    // +++++++++++++++++配置死信交换机的一系列信息
 
    /***
     * 死信交换机
     * @return Exchange
     */
    @Bean("DLXExchange")
    public Exchange dLXExchange() {
        return ExchangeBuilder.directExchange(DLX_EXCHANGE).durable(true).build();
    }
 
    /***
     * 死信队列
     * @return Queue
     */
    @Bean("DLXQueue")
    public Queue dLXQueue() {
        return QueueBuilder.durable(DLX_QUEUE).build();
    }
 
    /***
     * 死信交换机上面绑定死信队列
     * @return Binding
     */
    @Bean("dlxQueueBindDlxExchange")
    public Binding dlxQueueBindDlxExchange(@Qualifier("DLXExchange") Exchange dLXExchange,
                                           @Qualifier("DLXQueue") Queue dLXQueue) {
        // 死信交换机上面绑定死信队列
        return BindingBuilder.bind(dLXQueue).to(dLXExchange).with(DLX_KEY).noargs();
    }
}

点开查看详情:普消费者、死信消费者

java 复制代码
/**
 * 正常的消费者,监听并处理队列"MsgHandleQueue"的消息,失败时会交由死信交换机
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
public class QueueMsgConsumer {
 
    /***
     * 监听并处理队列"MsgHandleQueue"的消息
     * @param msgData 具体的消息
     * @param deliveryTag 处理消息的编号
     * @param routingKey 当前的路由key
     * @param message message对象
     * @param channel 信道对象
     */
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_NAME}, ackMode = "MANUAL")
    public void testConsumerA(@Payload String msgData, // 这个是生产者发送的JSON消息
                              @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, // 处理消息的编号
                              @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
                              Message message,
                              Channel channel) throws IOException {
        // 把接收JSON的数据转换为对象
        MessageSendDTO messageSendDTO = JSONObject.parseObject(msgData, MessageSendDTO.class);
        // 模拟判断我是否需要手动确认(若随机不是2则确认消费,否则拒绝,交由死信队列)
        if (Math.ceil(Math.random() * 2) != 2) {
            log.info("处理完成接收到的队列信息为:{},从{}路由过来的数据", messageSendDTO, routingKey);
            // 手动确认
            channel.basicAck(deliveryTag, false);
        } else {
            log.info("未处理完成接收到的队列信息为:{},从{}路由过来的数据", messageSendDTO, routingKey);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}
 
// ================================ 
/**
 * 死信消费者
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
public class DXLConsumer {
 
    /***
     * 死信消费者
     */
    @RabbitListener(queues = {RabbitMQConfig.DLX_QUEUE}, ackMode = "MANUAL")
    public void dlxConsumerTest(@Payload String msgData, //这个是生产者发送的JSON消息
                                @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, //处理消息的编号
                                @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
                                Message message,
                                Channel channel) throws IOException {
        // 把接收过来的JSON信息转换为对象
        MessageSendDTO messageSendDTO = JSONObject.parseObject(msgData, MessageSendDTO.class);
        // 死信队列名称
        String consumerQueue = message.getMessageProperties().getConsumerQueue();
        // 死信交换机名称
        String receivedExchange = message.getMessageProperties().getReceivedExchange();
        // 路由key
        String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
        log.info("死信消费者从死信队列:{} 获取死信消息:{},并处理完成手动确认", 
                consumerQueue, messageSendDTO);
        channel.basicAck(deliveryTag, false);
    }
}

点开查看详情:消息生产者

java 复制代码
/**
 * 消息生产者
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法,用来发送消息
     * @param msg 消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        // 消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, 
                RabbitMQConfig.ROUTING_KEY, bytes);
        log.info("生产者发送信息完成,已经交由给交换机.....");
    }
}

点开查看详情:调用Controller,拿到请求数据后调用生产者

java 复制代码
/**
 * 消息接收的Controller
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
 
    //注入生产者对象
    private final ProducerSend producerSend;
 
    /***
     * 基本的get请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msg 请求消息
     * @return String
     */
    @GetMapping("/produce")
    public String msgSend(MessageSendDTO msg) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{}", msg);
        // 发送消息
        producerSend.producerSendMsg(msg);
        return "请求发送成功,并已接收";
    }
}

点开查看详情:application.yml配置文件、MessageSendDTO消息对象封装类

yml 复制代码
# 关于application.yml配置文件
spring:
  ## 具体RabbitMQ配置请参考:org.springframework.boot.autoconfigure.amqp.RabbitProperties
  rabbitmq:
    host: 111.231.171.158
    port: 5672
    username: rabbit
    password: 1234
    virtual-host: nativeStudy
    # 消息监听器容器相关配置
    listener:
      simple:
        prefetch: 1   # 设置预取值为1(当为1时也被称为不公平分发;prefetch默认值为250)
        acknowledge-mode: manual  # 设置消息应答模式
 
# 关于MessageSendDTO消息对象封装类
@Data
@Builder
public class MessageSendDTO implements Serializable {
    @Serial
    private static final long serialVersionUID = 5905249092659173678L;
    private Integer msgID;          // 消息ID
    private String msgType;         // 消息类型
    private Object msgBody;         // 消息体
}

写完代码后可以执行代码查看队列和交换机的创建情况:

  调用Controller:
http://127.0.0.1:8081/test/produce?msgID=16&msgType=DXLType&msgBody=YesYesLaLa11

http://127.0.0.1:8081/test/produce?msgID=16&msgType=fonoutType&msgBody=YesYesLaLa11
http://127.0.0.1:8081/test/produce?msgID=16&msgType=fonoutType&msgBody=YesYesLaLa22

八:延迟队列(基于死信)

(一):队列TTL后延迟消费

针对延迟队列我上篇已经有了基本介绍,其实延迟队列就是基于TTL过期时间来完成的,把消息推送到普通的队列里并不会被消费,而是等待这条消息的TTL时间到期才会被丢弃到死信队列中,其实那个具体的死信队列才是我们将来要真实要处理的消息,消息TTL过期到死信队列,最终有死信消费者完成最终的消费;说白了就是借助普通队列的延迟时间达到延迟消费。


点开查看详情:RabbitMQ一些配置信息,交换机、队列等创建和绑定

java 复制代码
@Configuration
public class RabbitMQConfig {
 
    // 延迟直接交换机
    public static final String DELAY_DIRECT_EXCHANGE = "delayDirectExchange";
    // 延迟队列
    public static final String DELAY_TTL_QUEUE = "delayTTLQueue";
    // 延迟队列连接延迟交换机路由key
    public static final String DELAY_ROUTING_KEY = "delayRoutingKey";
    // 死信交换机
    public static final String DEAD_EXCHANGE = "deadExchange";
    // 死信队列
    public static final String DEAD_QUEUE = "deadQueue";
    // 死信队列绑定死信交换机路由key
    public static final String DEAD_ROUTING_KEY = "deadRoutingKey";
 
    // ################ 编写延迟队列和延迟交换机一些列配置 ################
 
    /***
     * 延迟交换机
     * @return Exchange
     */
    @Bean("delayDirectExchange")
    public Exchange delayDirectExchange() {
        return ExchangeBuilder.directExchange(DELAY_DIRECT_EXCHANGE).durable(true).build();
    }
 
    /***
     * 延迟普通队列
     * @return Queue
     */
    @Bean("delayTTLQueue")
    public Queue delayTTLQueue() {
        // 绑定死信队列(参数设置)
        Map>String, Object< arguments = new HashMap><();
        // 正常队列设置死信交换机 参数key是固定值;(就是说死去的消息发送到哪个交换机)
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        // 正常队列设置死信交换机到死信队列绑定Routing Key 参数key是固定值
        // (就是说死去的消息在交换机里通过什么路由发送到死信队列)
        arguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
        // 设置正常队列的长度限制为3
        // arguments.put("x-max-length",3);
        // 队列设置消息过期时间 20 秒
        arguments.put("x-message-ttl", 20 * 1000);
        return QueueBuilder.durable(DELAY_TTL_QUEUE).withArguments(arguments).build();
    }
 
    /***
     * 延迟队列绑定到延迟交换机上
     * @param delayDirectExchange 延迟交换机
     * @param delayTTLQueue 延迟队列
     * @return Binding
     */
    @Bean("delayQueueBindDelayExchange")
    public Binding delayQueueBindDelayExchange(
            @Qualifier("delayDirectExchange") Exchange delayDirectExchange,
            @Qualifier("delayTTLQueue") Queue delayTTLQueue) {
        return BindingBuilder
                .bind(delayTTLQueue).to(delayDirectExchange).with(DELAY_ROUTING_KEY).noargs();
    }
 
    // ################ 编写死信队列和死信交换机,和它们绑定关系的配置 ################
 
    /***
     * 死信交换机
     * @return Exchange
     */
    @Bean("deadExchange")
    public Exchange deadExchange() {
        return ExchangeBuilder.directExchange(DEAD_EXCHANGE).durable(true).build();
    }
 
    /***
     * 死信队列
     * @return Queue
     */
    @Bean("deadQueue")
    public Queue deadQueue() {
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }
 
    /***
     * 死信队列绑定死信交换机
     * @param deadExchange 死信交换机
     * @param deadQueue 死信队列
     * @return Binding
     */
    @Bean("deadQueueBindDeadExchange")
    public Binding deadQueueBindDeadExchange(
            @Qualifier("deadExchange") Exchange deadExchange,
            @Qualifier("deadQueue") Queue deadQueue) {
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
    }
}

点开查看详情:生产者代码和死信消费者代码

java 复制代码
// 生产者代码
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法
     * @param msg 消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        // 消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_DIRECT_EXCHANGE,
                RabbitMQConfig.DELAY_ROUTING_KEY, bytes);
        log.info("生产者发送信息完成,已经交由给延迟直接交换机.....");
    }
}
 
// 死信消费者代码
@Slf4j
@Component
public class DeadConsumer {
 
    /***
     * 死信消费者
     */
    @RabbitListener(queues = {RabbitMQConfig.DEAD_QUEUE}, ackMode = "MANUAL")
    public void dlxConsumerTest(@Payload String msgData, // 这个是生产者发送的JSON消息
                                @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,//处理消息的编号
                                @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
                                Message message,
                                Channel channel) throws IOException {
        // 把接收过来的JSON信息转换为对象
        MessageSendDTO messageSendDTO = JSONObject.parseObject(msgData, MessageSendDTO.class);
        // 死信队列名称
        String consumerQueue = message.getMessageProperties().getConsumerQueue();
        // 死信交换机名称
        String re = message.getMessageProperties().getReceivedExchange();
        // 路由key
        String rr = message.getMessageProperties().getReceivedRoutingKey();
        log.info("死信消费者从死信队列:{} 获取死信消息:{},并处理完成手动确认",
                consumerQueue, messageSendDTO);
        channel.basicAck(deliveryTag, false);
    }
}

关于TestController、application.yml、MessageSendDTO这三个文件详细参考第七节死信队列代码,都是一样的配置和代码。写好代码后我们看看如下执行效果。

  我们调用Controller打印日志如下:

通过上面的案例我们可以实现延迟的效果,把消息发送到延迟队列中,并不会对这部分消息进行消费,而是等待过期后自动丢到死信交换机中,并由死信交换机转发到死信队列中,并进行消费者消费;之前案例我们设置了一个延迟20秒的延迟队列,但是现在若有一个需求,对一些数据进行10秒的延迟,这时我们就需要手动去创建一个队列,并设置延迟时间为10秒;这时我们就发现并不灵活,每次延迟不同时间都需要添加一个延迟队列,所以在队列级别上设置延迟是不灵活的,并不推荐;下面将进行一个优化。

(二):对每条消息设置TTL延迟

上面说了在队列设置TTL延迟来实现延迟消息不是太灵活,所以我对之前的代码进行一个简单的优化,创建一个全新的没有延迟的队列,只是对发送的每条消息进行一个延迟(在生产者设置消息延迟并发送),这样就可以实现一个队列里面的每条消息的延迟时间不一样,说干就干,参考下图:

  下面我们就将之前的代码改造成如上图的图例一样,具体改造如下:
点开查看详情:将给出的代码增加到RabbitMQConfig类中空白位置即可

java 复制代码
    // ================================ 优化代码 Start ================================
    // 简单队列,无延迟
    public static final String SIMPLE_QUEUE = "simpleQueue";
    // 简单队列连接延迟交换机路由key
    public static final String SIMPLE_ROUTING_KEY = "simpleRoutingKey";
 
    /***
     * 一个简单无延迟的队列
     * @return Queue
     */
    @Bean("simpleQueue")
    public Queue simpleQueue() {
        // 绑定死信队列(参数设置)
        Map>String, Object< arguments = new HashMap><();
        // 正常队列设置死信交换机 参数key是固定值;(就是说死去的消息发送到哪个交换机)
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        // 正常队列设置死信交换机到死信队列绑定Routing Key 参数key是固定值
        // (就是说死去的消息在交换机里通过什么路由发送到死信队列)
        arguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
        // 设置正常队列的长度限制 为3
        // arguments.put("x-max-length",3);
        // 队列设置消息过期时间 20 秒
        // arguments.put("x-message-ttl", 20 * 1000);
        return QueueBuilder.durable(SIMPLE_QUEUE).withArguments(arguments).build();
    }
 
    /***
     * 简单队列绑定到延迟交换机上
     * @param delayDirectExchange 延迟交换机
     * @param simpleQueue 简单队列
     * @return Binding
     */
    @Bean("simpleQueueBindDelayExchange")
    public Binding simpleQueueBindDelayExchange(
            @Qualifier("delayDirectExchange") Exchange delayDirectExchange,
            @Qualifier("simpleQueue") Queue simpleQueue) {
        return BindingBuilder
            .bind(simpleQueue).to(delayDirectExchange).with(SIMPLE_ROUTING_KEY).noargs();
    }
    // ================================ 优化代码 End ================================

点开查看详情:将给出的代码增加到TestController和ProducerSend类空白处即可

java 复制代码
 // 将下面代码增加到TestController空白处
/***
     * 这是优化的代码,用来接收带ttl的时间的方法
     * http://127.0.0.1:8081/test/produceA?msgID=16&msgType=fonoutType&msgBody=YesYesLaLa11&ttl=20
     * 基本的Get请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msg 请求消息
     * @param ttl 过期时间
     * @return String
     */
    @GetMapping("/produceA")
    public String msgSendSimple(MessageSendDTO msg, Integer ttl) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{},
            其中消息的过期时间为:{} s", msg, ttl);
        //发送消息
        producerSend.producerSendMsgSimple(msg, ttl);
        return "请求发送成功,并已接收";
    }
 
// 将下面代码添加到ProducerSend空白处
/***
     * 这是优化后,发送的消息携带ttl过期的生产者
     * 生产者方法
     * @param msg 消息
     * @param ttl 过期时间(秒)
     */
    public void producerSendMsgSimple(MessageSendDTO msg, Integer ttl) {
        //消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        //发送消息
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.DELAY_DIRECT_EXCHANGE,
                RabbitMQConfig.SIMPLE_ROUTING_KEY,
                bytes, message -> {
                    // 这条消息的过期时间也被设置成了ttl秒 , 超过ttl秒未处理则执行到此消息后被丢弃。
                    // 记住是执行到此消息后被丢弃,后面说明
                    message.getMessageProperties().setExpiration(String.valueOf(ttl * 1000));
                    // 设置好了一定要返回
                    return message;
                });
    }

我们调用Controller打印日志如下:
http://127.0.0.1:8081/test/produceA?msgID=16&msgType=DelayType&msgBody=YesYesLaLa11&ttl=20
http://127.0.0.1:8081/test/produceA?msgID=16&msgType=DelayType&msgBody=YesYesLaLa22&ttl=10
  得出结论:上面的程序不是特别完美(有问题) ,虽然设置了消息过期时间,但是在队列中,不管后面入队列的的延迟消息多短,都得等前面的延迟消息过期或消费,否则后面的延迟消息会一直等待,即使延迟消息过期了,他也会等待(最终等到这条消息执行时,会先判断是否过期,没过期继续等待,阻塞后面的消息,过期或者被消费则下一个执行);可以看看队列先进先出的原则。

九:延迟队列(基于插件)

上面基于死信队列实现延迟存在问题,在这我将使用插件的方式来解决上面遗留的问题,这种方式得需要我们下载插件,安装插件后,交换机就会多出来一个 "x-delayed-message" 的类型,我们创建时就可以选择这种类型;点击跳转下载插件(注意:选择合适的版本,具体每个版本他有介绍)。

由于我的RabbitMQ 4.1.2Erlang 27.3.4.1需要下载对应的是v4.1.x版本,直接点击下载即可。下载快捷地址:rabbitmq_delayed_message_exchange-v4.1.0.ez

java 复制代码
安装方式:把下载的插件复制到RabbitMQ的plugins目录里:
    比如我这具体路径是:/usr/local/rabbitmq_server-4.1.2/plugins;复制完成后就可以进行安装操作,
    - '查看RabbitMQ插件列表'
        rabbitmq-plugins list
            // ...
            // [  ] rabbitmq_consistent_hash_exchange 4.1.2
            // [  ] rabbitmq_delayed_message_exchange 4.1.0  【刚才复制到插件目录上的文件】
            // [  ] rabbitmq_event_exchange           4.1.2
    - '安装插件(安装插件时不用指定版本号)'
        rabbitmq-plugins enable rabbitmq_delayed_message_exchange
            // ...
            //   rabbitmq_delayed_message_exchange
            // started 1 plugins.    ## 代表插件安装完成
    - '补充:禁用插件'
        rabbitmq-plugins disable rabbitmq_delayed_message_exchange

现在我们安装完插件后想实现延迟消息则不用那么麻烦了,也不用写死信交换机了,只需要正常的队列创建和交换机创建即可,下面是基本的流程图:


点开查看详情:RabbitMQ一些配置信息,交换机、队列等创建和绑定

java 复制代码
@Configuration
public class RabbitMQConfig {
 
    // 直接延迟交换机名称
    public static final String DELAYED_EXCHANGE = "delayedExchange";
    // 延迟队列名称(但不是在队列中延迟)
    public static final String DELAYED_QUEUE = "delayedQueue";
    // 绑定路由key
    public static final String DELAYED_ROUTING_KEY = "delayedRoutingKey";
 
    /***
     * 创建交换机消息
     * @return Exchange
     */
    @Bean("delayedExchange")
    public CustomExchange delayedExchange() {
        // 因为通过ExchangeBuilder没有那个延迟交换机的类型,所以我们使用其它交换机(自定义创建)
        // 其它参数
        Map>String, Object< args = new HashMap><();
        // 自定义交换机的类型;(虽然设置的是延迟交换机,但是具体四大类型还是得有)
        args.put("x-delayed-type", "direct");
        // 参数:交换机名称、交换机类型、是否持久化交换机、是否断开自动删除、其它参数
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }
 
    /***
     * 队列名称
     * @return Queue
     */
    @Bean("delayedQueue")
    public Queue delayedQueue() {
        return QueueBuilder.durable(DELAYED_QUEUE).build();
    }
 
    /***
     * 绑定关系
     * @param delayedExchange 交换机消息
     * @param delayedQueue 队列消息
     * @return Binding
     */
    @Bean("delayedQueueBindDelayedExchange")
    public Binding delayedQueueBindDelayedExchange(
            @Qualifier(value = "delayedExchange") CustomExchange delayedExchange,
            @Qualifier(value = "delayedQueue") Queue delayedQueue) {
        return BindingBuilder.bind(delayedQueue)
            .to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

点开查看详情:生产者代码和消费者代码

java 复制代码
// ============= 生产者代码 =============
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法
     * @param msg 消息
     * @param delayTime 延迟时间
     */
    public void producerSendMsgDelay(MessageSendDTO msg, Integer delayTime) {
        //消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        //发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.DELAYED_EXCHANGE, 
            RabbitMQConfig.DELAYED_ROUTING_KEY, bytes, message -> {
                    // 这条消息的过期时间被设置过期delayTime秒(在交换机中被延迟,时间一到则被路由到队列)
                    // message.getMessageProperties().setExpiration(String.valueOf(delayTime * 1000));
                    message.getMessageProperties().setHeader("x-delay", delayTime * 1000);
                    // 说明:x-delayed-message 交换机,那你就必须用:setHeader("x-delay", delayTime * 1000)
                    // 不能用 setExpiration(),那是TTL机制的写法。
                    //设置好了一定要返回
                    return message;
                });
    }
}
 
// ============= 消费者代码 =============
@Slf4j
@Component
public class ConsumerA {
 
    /***
     * RabbitListener注解用来监控消息队列
     */
    @RabbitListener(queues = {RabbitMQConfig.DELAYED_QUEUE}, ackMode = "MANUAL")
    public void dlxConsumerTest(@Payload String msgData, // 这个是生产者发送的JSON消息
                                @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, // 处理消息的编号
                                @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
                                Message message,
                                Channel channel) throws IOException {
        // 把接收过来的JSON信息转换为对象
        MessageSendDTO messageSendDTO = JSONObject.parseObject(msgData, MessageSendDTO.class);
        // 队列名称
        String consumerQueue = message.getMessageProperties().getConsumerQueue();
        // 交换机名称
        String receivedExchange = message.getMessageProperties().getReceivedExchange();
        // 路由key
        String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
        log.info("消费者从队列:{} 获取消息:{},并处理完成手动确认", consumerQueue, messageSendDTO);
        channel.basicAck(deliveryTag, false);
    }
}

点开查看详情:TestController用来接收消息并调用生产者

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
 
    //注入生产者对象
    private final ProducerSend producerSend;
 
    /***
     * 基本的POST请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msg 请求消息
     * @param delayTime 延迟时间
     * @return String
     */
    @PostMapping("/produceMsg/{delayTime}")
    public String msgSendSimple(
            @RequestBody MessageSendDTO msg, @PathVariable(value = "delayTime") Integer delayTime) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{},其中消息的过期时间为:{} s", msg, delayTime);
        //发送消息
        producerSend.producerSendMsgDelay(msg, delayTime);
        return "请求发送成功,并已接收";
    }
}

关于application.yml、MessageSendDTO这两个文件详细参考第七节死信队列代码,都是一样的配置和代码。写好代码后我们看看如下执行效果。

调用接口并验证案例代码:

可以看出,虽然第一条消息延迟是50秒,但不影响后一条延迟为10秒的消息出队列,因此解决问题。

十:消息发布确认

在第四节中,我们已经介绍了消息应答机制(Message Acknowledgement),它主要用于保障消费者与队列之间的消息传输可靠性,防止消息在消费过程中丢失。但仅靠消费者端的应答机制,并不能完全覆盖整个消息链路的可靠性保障。要确保消息从生产者成功投递到交换机,以及从交换机成功路由到目标队列的过程中不发生丢失, 我们需要引入消息发布确认机制(Publisher Confirms)。

在上一篇文章中,我们已经基于RabbitMQ原生API实现了三种发布确认策略:单个发布确认、批量发布确认以及异步批量发布确认。本篇文章将进一步通过Spring Boot集成RabbitMQ的方式,实现发布确认机制,以简化开发并提升可维护性。

(一):基本代码准备

这里我准备了一个没有实现消息的发布确认代码,针对这个代码后面做一系列的优化,其实这个基本代码就是直接交换机方式,具体如下:

直接交换机(confirmExchange)<==>路由Key(confirmRoutingKey)<==>基本队列(confirmQueue)
点开查看详情:RabbitMQ一些配置信息,交换机、队列等创建和绑定

java 复制代码
@Configuration
public class RabbitMQConfig {
 
    // 直接交换机
    public static final String CONFIRM_EXCHANGE = "confirmExchange";
    // 队列
    public static final String CONFIRM_QUEUE = "confirmQueue";
    // 绑定路由key
    public static final String CONFIRM_ROUTING_KEY = "confirmRoutingKey";
 
    /***
     * 创建交换机消息
     * @return Exchange
     */
    @Bean("confirmExchange")
    public Exchange createConfirmExchange() {
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true).build();
    }
 
    /***
     * 队列名称
     * @return Queue
     */
    @Bean("confirmQueue")
    public Queue createConfirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE).build();
    }
 
    /***
     * 绑定关系
     * @param confirmExchange 交换机消息
     * @param confirmQueue 队列消息
     * @return Binding
     */
    @Bean("delayedQueueBindDelayedExchange")
    public Binding createDelayedQueueBindDelayedExchange(
            @Qualifier(value = "confirmExchange") Exchange confirmExchange,
            @Qualifier(value = "confirmQueue") Queue confirmQueue) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange)
                .with(CONFIRM_ROUTING_KEY).noargs();
    }
}

点开查看详情:生产者代码和消费者代码

java 复制代码
// ============== 消费者代码 ==============
@Slf4j
@Component
public class ConsumerA {
 
    /***
     * 消费者
     */
    @RabbitListener(queues = {RabbitMQConfig.CONFIRM_QUEUE}, ackMode = "MANUAL")
    public void consumerATest(@Payload String msgData, //这个是生产者发送的JSON消息
                              @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, //处理消息的编号
                              @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
                              Message message,
                              Channel channel) throws IOException {
        //把接收过来的JSON信息转换为对象
        try {
            MessageSendDTO messageSendDTO = JSONObject.parseObject(msgData, MessageSendDTO.class);
            //队列名称
            String consumerQueue = message.getMessageProperties().getConsumerQueue();
            log.info("消费者从队列:{} 获取消息:{},并处理完成手动确认", consumerQueue, messageSendDTO);
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            log.info("消费失败:{}", msgData);
            channel.basicNack(deliveryTag, false, true);
        }
    }
}
 
// ============== 生产者代码 ==============
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    private final RabbitTemplate rabbitTemplate;
 
    /***
     * 生产者方法
     * @param msg 消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        //消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        //发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE,
                RabbitMQConfig.CONFIRM_ROUTING_KEY, bytes);
    }
}

点开查看详情:TestController用来接收消息并调用生产者

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
 
    //注入生产者对象
    private final ProducerSend producerSend;
 
    /***
     * 基本的GET请求,用来接收消息,并把消息交给生产者,并由生产者推送到指定交换机,由交换机分发消息
     * @param msg 请求消息
     * @return String
     */
    @GetMapping("/produce")
    public String msgSendSimple(MessageSendDTO msg) {
        log.info("Controller接收到请求并把请求的信息交由生产者:{}", msg);
        //发送消息
        producerSend.producerSendMsg(msg);
        return "请求发送成功,并已接收";
    }
}

关于application.yml、MessageSendDTO这两个文件详细参考第七节死信队列代码,都是一样的配置和代码。写好代码后测试是否能创建和接收并投递消息即可。

(二):消息发布确认

说到消息的发布确认,我们就会使用到RabbitTemplate里的ConfirmCallback内部类接口,就是确认消息是否真实发送到交换机里了,我们得编写一个RabbitMQMyCallBack的回调类,具体说明如下:

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RabbitMQMyCallBack implements RabbitTemplate.ConfirmCallback {

    // 注入rabbitTemplate对象
    private final RabbitTemplate rabbitTemplate;

    /***
     * 对象实例化完成(对象创建和属性注入)后调用此方法
     */
    @PostConstruct
    public void init() {
        // 设置发布确认信息回调类RabbitTemplate.ConfirmCallback confirmCallback;
        rabbitTemplate.setConfirmCallback(this);
    }

    /***
     * 是ConfirmCallback的抽象方法,用来确认消息是否到达exchange,不保证消息是否可以路由到正确的queue;
     * 它的抽象方法机制只确认,但需要配置部分参数:
     *      spring.rabbitmq.publisher-confirm-type: correlated
     *      对于部分Springboot老版本需要设置:publisher-confirms: true
     * 交换机确认回调方法(成功和失败):
     *      发送消息 --> 交换机接收到消息 --> 回调
     *          1:correlationData 保存回调消息的ID及相关信息
     *          2:交换机收到消息 ack = true
     *          3:调用回调confirm方法,对应ack=true , cause=null
     *      发送消息 --> 交换机接收消息失败 --> 回调
     *          1:correlationData 保存回调消息的ID及相关信息
     *          2:交换机收到消息 ack = false
     *          3:调用回调confirm方法,对应ack=false , cause="异常信息"
     * @param correlationData 回调的相关数据
     * @param ack 消息是否成功发送给交换机,true成功,false失败
     * @param cause 对于ack为false时会有对应的失败原因,否则为空
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        // 获取对应的ID消息,因为不确认是否有ID被传入,所以取值需要判空
        String id = correlationData == null ? "" : correlationData.getId();
        //校验是否成功发送
        if (ack) {
            log.info("消息已经成功交给了交换机,对应消息ID为:{}", id);
        } else {
            log.info("消息未能成功发送给交换机,对应消息ID为:{},异常原因:{}", id, cause);
        }
    }
}

编写完成过后,我们需要在发送者ProducerSend来设置一些消息,用来做回调操作:

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {

    private final RabbitTemplate rabbitTemplate;

    // 生产者方法
    public void producerSendMsg(MessageSendDTO msg) {
        // 消息转换为JSON格式并转为字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
        // 其它的一些信息,用来回调用处
        CorrelationData correlationData = new CorrelationData();
        // 设置id信息,其实默认就是UUID,我们其实可以根据自己设置指定ID信息
        // correlationData.setId("MSG-UID-" + UUID.randomUUID());
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE,
                RabbitMQConfig.CONFIRM_ROUTING_KEY, bytes, correlationData);
    }
}

设置必要配置,在application.yml中加这个配置:spring.rabbitmq.publisher-confirm-type: correlated
点开查看详情:关于spring.rabbitmq.publisher-confirm-type配置说明

java 复制代码
'RabbitTemplate.ConfirmCallback: 用于确认消息是否成功到达交换机(Exchange)'
    触发时机:当消息从生产者发送到交换机后,无论是否成功,都会触发这个回调。
    使用场景:用于确认消息是否成功投递到交换机,避免消息在传输过程(如网络故障)中丢失。
    增加必要的application.yml配置:
        spring.rabbitmq.publisher-confirm-type:none/simple/correlated
            - none(默认值):关闭发布确认功能。
                效果:RabbitTemplate不会触发ConfirmCallback回调。
                适用场景:对消息可靠性没有特别要求的简单应用。
            - correlated:启用相关性发布确认机制。'(推荐)'
                效果:推荐使用。支持传入CorrelationData,可以精确追踪每一条消息的状态。是高级发布确认模式。
                适用场景:生产级项目或对消息可靠性要求高的系统。
                回调中可获得:消息 ID(来自 correlationData.getId())、成功状态(ack)、失败原因(cause)
            - simple:启用简单的发布确认机制。
 
'关于simple模式的详细说明:当配置为simple模式时,RabbitMQ会提供两种方式进行消息发布确认:'
    第一种:回调方式(ConfirmCallback):
        支持通过RabbitTemplate.setConfirmCallback()注册回调函数来监听消息是否成功到达交换机。
    第二种:同步阻塞方式:开发者可以通过RabbitTemplate.execute()方法获取底层Channel对象,
        并显式调用waitForConfirms()或waitForConfirmsOrDie() 方法,以同步方式等待broker
        返回发布确认结果,从而根据结果决定后续逻辑。
            waitForConfirms():阻塞当前线程,直到消息被确认或超时,返回true表示确认成功,false 表示失败。
            waitForConfirmsOrDie():功能与waitForConfirms()相似,但如果确认失败,则会主动关闭当前Channel,
                从而导致后续消息无法通过该Channel发送,需谨慎使用。
        就是投递消息时使用原生方式,如下:
            rabbitTemplate.execute(channel -> {
                channel.confirmSelect(); // 开启发布确认模式
                channel.basicPublish(RabbitMQConfig.CONFIRM_EXCHANGE,
                        RabbitMQConfig.CONFIRM_ROUTING_KEY, null, bytes);
                // 等待 broker 返回确认结果(同步阻塞)
                boolean confirmed = channel.waitForConfirms(); // 或waitForConfirmsOrDie()
                if (confirmed) {
                    System.out.println("消息发送成功!");
                } else {
                    System.out.println("消息发送失败!");
                }
                return "";
            });
        '原生方式发布确认,一旦投递失败就会关闭Channel,后面无法继续投递了。'
        '推荐使用官方封装的:rabbitTemplate.convertAndSend(xxxx);不要使用这种底层API'

测试调用失败和成功现状(故意写错发送到不存在的交换机):

java 复制代码
// 生产者一次性发送2个消息:
// 发送消息A(可以成功发送)
rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE,
        RabbitMQConfig.CONFIRM_ROUTING_KEY, bytes, correlationData);
// 发送消息B(不可以成功发送,交换机不存在)
rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE + "test",
        RabbitMQConfig.CONFIRM_ROUTING_KEY, bytes, new CorrelationData());

(三):路由失败回退机制

在RabbitMQ中,当交换机接收到来自生产者的消息后,会尝试根据指定的路由键将消息投递到匹配的队列。如果未找到符合条件的队列,消息将被视为不可路由 。在默认情况下(未设置回退机制时),不可路由的消息会被交换机直接丢弃,且生产者不会收到任何通知 ,这可能导致消息静默丢失,无法被感知和处理。为了解决这一问题,RabbitMQ提供了mandatory参数及相应的回调机制,用于增强消息的可靠性:

java 复制代码
'Mandatory 参数:'
    启用方式:在使用RabbitTemplate发送消息时,设置mandatory = true。
    功能描述:当设置为true时,如果消息无法被交换机路由到任何队列,RabbitMQ将不会直接丢弃消息,
        而是将该消息退回给生产者,并触发生产者端的ReturnsCallback回调方法。
    实际作用:确保生产者能够感知消息是否成功路由至目标队列,避免消息在交换机阶段悄然丢失。

'RabbitTemplate.ReturnsCallback: 用于确认消息是否成功从交换机路由到队列(Queue)。'
    触发时机:消息到达交换机,但无法路由到任何队列时,才会触发。
    前提条件:RabbitTemplate必须设置mandatory=true,否则消息路由失败后会被丢弃,回调不会触发。
    使用场景:用于检测路由失败的情况,例如routing key设置错误、没有绑定队列等。

通过设置mandatory=true并实现ReturnsCallback接口,开发者可以对路由失败的消息进行日志记录、报警、补发等处理,进一步提升系统的可靠性与可观测性。下面我将对上面的代码进行一个简单的修改:

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RabbitMQMyCallBack 
        implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    // 注入rabbitTemplate对象
    private final RabbitTemplate rabbitTemplate;

    // 对象实例化完成(对象创建和属性注入)后调用此方法
    @PostConstruct
    public void init() {
        // 设置发布确认信息回调类RabbitTemplate.ConfirmCallback confirmCallback;
        rabbitTemplate.setConfirmCallback(this);
        // 设置回退消息回调类ReturnsCallback.ReturnsCallback returnsCallback;
        rabbitTemplate.setReturnsCallback(this);
        // true:交换机无法将消息进行路由时,会将该消息返回给生产者;
        // false:如果发现消息无法进行路由,则直接丢弃。
        rabbitTemplate.setMandatory(true);  // 或使用配置 spring.rabbitmq.template.mandatory: true
    }

    /***
     * 当消息无法被路由时执行当前回调(增加的路由失败回退机制)
     * @param returned 被退回的消息信息
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        // 发送的消息
        Message message = returned.getMessage();
        // 发送到哪个交换机
        String exchange = returned.getExchange();
        // 交换机到队列的路由key
        String routingKey = returned.getRoutingKey();
        // 退回原因
        String replyText = returned.getReplyText();
        // 退回原因状态码
        int replyCode = returned.getReplyCode();
        //消息打印
        log.info("信息被回退,从交换机:{} 路由:{}发送到队列失败,发送信息为:{},退回状态码:{}和原因:{}",
        exchange, routingKey, message, replyCode, replyText);
        // 我们可以在这后面对发送失败的消息进行处理...;比如放到一个统一的队列,或者打印日志记录
    }
    
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {...}
}

设置必要配置,在application.yml中加这个配置:

yml 复制代码
spring:
  rabbitmq:
    ...省略
    publisher-confirm-type: correlated   # 消息发布确认(生产者->交换机是否成功)
    publisher-returns: true              # 消息回退(交换机路由->队列是否成功)
    template:
      mandatory: true       # 消息路由发送失败返回到队列中, 相当手动设置rabbitTemplate.setMandatory(true);

编写完成过后,我们需要在发送者ProducerSend来设置一些测试代码:

java 复制代码
//省略....  
//发送消息一次(不可以成功发送,路由key不存在)  
rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE,  
        RabbitMQConfig.CONFIRM_ROUTING_KEY + "test", bytes, correlationData);

十一:发布确认&路由失败解决方案

在生产环境中,消息投递的可靠性是系统稳定性的重要保障。RabbitMQ 提供的发布确认机制和回退机制可以帮助我们在消息未正确到达交换机或队列时,及时捕捉并进行补偿。

(一):发布确认机制(ConfirmCallback)

java 复制代码
目标:确认消息是否被RabbitMQ交换机成功接收。
关键流程说明:
    ConfirmCallback 会在消息从 生产者发送到交换机之后,异步回调;
        当ack=true 表示 RabbitMQ 交换机已接收;
        当ack=false 表示交换机拒收,需要重试或报警处理。

@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    // 通过 CorrelationData 返回消息体
    String msg = new String(correlationData.getReturned()
        .getMessage().getBody(), StandardCharsets.UTF_8);
    MessageSendDTO messageSendDTO = JSONObject.parseObject(msg, MessageSendDTO.class);
    // 关于幂等性Key;在生产者发送消息时设置幂等性Key
    String redisKey = "rabbit:confirm:" + messageSendDTO.getMessageId();
    if (ack) {
        // 交换机已接收,清除 Redis 中的缓存记录
        redisTemplate.delete(MQConstant.REDIS_PREFIX_KEY + messageSendDTO.getMessageId());
        redisTemplate.delete(redisKey);
        log.info("消息已成功发送到交换机,messageId={}", messageSendDTO.getMessageId());
    } else {
        // 交换机未接收,记录重试次数
        Long retryCount = redisTemplate.opsForValue().increment(redisKey);
        if (retryCount >= 4) {
            redisTemplate.delete(redisKey);
            log.error("消息发送失败已重试 {} 次,终止重试,messageId={},消息内容={}",
                retryCount, messageSendDTO.getMessageId(), messageSendDTO);
            // TODO:可落库,发告警,或者投递到"异常处理队列"
        } else {
            log.warn("消息发送失败,准备第 {} 次重试,
                messageId={}",retryCount, messageSendDTO.getMessageId());
            // 重新投递消息
            rabbitTemplate.convertAndSend(
                correlationData.getReturned().getExchange(),
                correlationData.getReturned().getRoutingKey(),
                correlationData.getReturned().getMessage(),
                correlationData);
        }
    }
}

注意事项:
    重试次数采用 Redis 控制,避免重复消费;
    超过最大重试次数时,不可再盲目投递,应落库记录或转人工介入;
    建议对发送失败的消息,写入独立表做"失败补偿"处理。

(二):路由失败回退机制(ReturnsCallback)

关于无法被路由的消息我们可以使用下小节的备份交换机来兜底。

java 复制代码
目标:确保消息能正确从交换机路由到对应队列。
典型原因:
    routingKey 错误;
    没有任何队列绑定到目标交换机;
    队列被意外删除或配置异常;
    使用了Direct/Topic交换机但未匹配成功。
    回退机制处理逻辑如下:

@Override
public void returnedMessage(ReturnedMessage returned) {
    String exchange = returned.getExchange();
    String routingKey = returned.getRoutingKey();
    String replyText = returned.getReplyText();
    int replyCode = returned.getReplyCode();
    String msg = new String(returned.getMessage().getBody(), StandardCharsets.UTF_8);

    log.error("消息回退:消息未能路由至队列,exchange={},
                routingKey={},replyCode={},原因={},消息体={}",
                exchange, routingKey, replyCode, replyText, msg);

    // TODO:生产环境建议如下处理:
    // 1. 将回退的消息记录数据库(如 message_failed 表);
    // 2. 写入专用失败队列,供后续人工处理或定时任务重试;
    // 3. 实时触发告警(如钉钉、邮件、短信通知)

    // 示例:持久化失败消息
    // failedMessageRepository.save(new FailedMessage(exchange, routingKey, msg, replyText));
}

十二:备份交换机

在 RabbitMQ 中,通过设置 mandatory=true 并实现生产者端的 ReturnsCallback,可以对不可路由的消息进行回退处理。这种机制使得生产者能够感知消息未能成功路由至目标队列的情况,并根据业务需求进行相应的日志记录、告警或补偿处理。尽管这种方式提供了一种对消息丢失风险的控制手段,但它也引入了以下问题:

  1. 处理方式分散且不统一:通常仅通过记录日志和触发告警来应对回退消息,不具备系统化的存储与处理能力。
  2. 运维复杂性增加:在分布式部署场景中,多个生产者节点分布在不同服务器上,若依赖本地日志进行问题排查,将导致排查流程复杂、效率低,且易漏报或误判。
  3. 代码侵入性增强 :启用回退机制后,生产者需要额外编写处理逻辑来消费 ReturnsCallback,增加了系统开发与维护成本。

此外,由于不可路由的消息并未进入任何队列 ,因此无法像"死信消息"那样通过死信队列(DLX)进行统一收集与后续处理,这就使得这类消息的可靠存储和集中处理面临更大挑战。

✅ 解决方案:备份交换机(Alternate Exchange)

为了解决以上问题,RabbitMQ 提供了 备份交换机(Alternate Exchange, 简称 AE) 机制。该机制允许在声明主交换机时指定一个备份交换机,用于接收所有无法被路由的消息。其工作原理如下:

  • 当主交换机无法将消息路由至任何队列时,RabbitMQ 会自动将该消息转发至事先指定的备份交换机;
  • 备份交换机通常设置为 Fanout 类型,以实现对所有绑定队列的广播分发;
  • 应用可在备份交换机下绑定一个或多个队列,用于集中存储这些"不可路由消息",实现日志归集、消息分析或统一处理;
  • 此外,可为备份交换机额外绑定一个专用的报警队列,由独立消费者监听该队列,实时触发系统告警。

相比传统的mandatory + ReturnsCallback回退机制,备份交换机提供了一种更优雅、更系统化的消息兜底处理方案 。它适用于对可靠性和运维效率要求较高的生产环境,特别是在需要保障消息不丢失、降低生产者复杂度的场景下,是非常推荐的架构模式。
<math xmlns="http://www.w3.org/1998/Math/MathML"> 下面案例的基本流程图: \color{#f00}{下面案例的基本流程图:} </math>下面案例的基本流程图:

具体代码目录如下:

java 复制代码
'目录结构:'
    ./main/
    ├── java
    │ └── cn
    │     └── ant
    │         ├── BackupExchangeDemoApplication.java        // 启动类
    │         ├── config
    │         │ ├── RabbitMQConfig.java                     // RabbitMQ配置
    │         │ └── RabbitMQMyCallBack.java                 // 关于发布确认的回调方法编写
    │         ├── controller
    │         │ └── TestController.java                     // 接收前端发送的任务消息
    │         ├── entity
    │         │ └── MessageSendDTO.java                     // 消息封装类
    │         └── mqHandle
    │             ├── consumer
    │             │ ├── BackupConsumer.java                 // 备份消费者
    │             │ ├── ConsumerA.java                      // 普通消费者
    │             │ └── WarningConsumer.java                // 告警消费者
    │             └── producer
    │                 └── ProducerSend.java                 // 消息生产者
    └── resources
        ├── application.yml
        └── log4j2.xml

这里将不再给出示例代码,具体请参考远程仓库;编写完代码后启动,会创建如下一些交换机和队列:

发送请求到Controller后调用发送消息会发现直接到备份交换机(故意写错的路由Key);具体日志如下:

需要注意的是,在使用RabbitMQ发送消息时,我们常会设置 mandatory=true,用于确保当消息无法被路由到任何队列时,能够触发生产者端的 ReturnsCallback 回调方法,从而感知消息未成功投递的情况。然而,当交换机同时配置了备份交换机(Alternate Exchange) 时,即使消息无法被主交换机路由到目标队列,RabbitMQ 也不会触发 ReturnsCallback,而是将该消息转发给预先设置的备份交换机进行兜底处理。在这种机制下,RabbitMQ 认为消息已被"成功处理",不会将其视为投递失败。因此,当主交换机配置了备份交换机时,即便设置了 mandatory=true,也不会触发 returnedMessage() 回调方法。这也是 RabbitMQ 的一个重要设计原则,需在实际开发中注意取舍和选择适合的异常消息处理机制。

关于消息的幂等性和优先级队列请看下篇。

相关推荐
这里有鱼汤几秒前
“三角收敛”战法全解析:我靠这一招实现了年化35%
后端·python
小鱼人爱编程27 分钟前
Java基石--Java发动机ClassLoader
java·spring boot·后端
一只叫煤球的猫42 分钟前
从屎山说起:支付流程重构实战,三种设计模式灵活运用
java·后端·架构
xiezhr1 小时前
那些年我们一起追过的Java技术,现在真的别再追了!
java·后端·编程语言
Victor3561 小时前
MySQL(155)什么是MySQL的事件调度器?
后端
Victor3561 小时前
MySQL(156)如何使用MySQL的事件调度器?
后端
程序员爱钓鱼2 小时前
Go语言实战案例-使用map实现学生成绩管理
后端·google·go
程序员爱钓鱼2 小时前
Go语言实战案例-合并多个文本文件为一个
后端·google·go
Microsoft Word6 小时前
用户中心项目实战(springboot+vue快速开发管理系统)
vue.js·spring boot·后端
不写八个9 小时前
GoLang教程005:switch分支
开发语言·后端·golang