第五章 RabbitMQ高级

1. 生产者消息确认机制

1.1 配置文件

yaml 复制代码
spring:
  rabbitmq:
    host: 192.168.137.110 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: itcast
    password: 123456
    virtual-host: /
    publisher-confirm-type: correlated #异步
    publisher-returns: true
    template:
      mandatory: true

1.2 编写配置类

java 复制代码
package cn.itcast.mq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName CommonConfig
 * @Description rabbitMQ生产者消息确认配置类
 * @Author 孙克旭
 * @Date 2024/11/24 14:47
 */
@Configuration
@Slf4j
public class CommonConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //获取RabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        //配置ReturnCallback
        rabbitTemplate.setReturnCallback(((message, replyCode, replyText, exchange, routingKey) -> {
            //记录日志
            log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",
                    replyCode, replyText, exchange, routingKey, message.toString());
        }));
    }
}

1.3 发送消息时携带关联数据

java 复制代码
package cn.itcast.mq.spring;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.UUID;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue() throws InterruptedException {
        String routingKey = "simple.test";
        String message = "hello, spring amqp!";
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        correlationData.getFuture().addCallback(result -> {
            //判断结果
            if (result.isAck()) {
                //ACK
                log.debug("消息成功投递到交换机,消息ID:{}", correlationData.getId());
            } else {
                //NACK
                log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
            }
        }, ex -> {
            //记录日志
            log.error("消息发送失败!", ex);
        });
        rabbitTemplate.convertAndSend("amq.topic", routingKey, message, correlationData);
    }
}

1.4 SpringAMQP中处理消息确认的几种情况:

(1)publisher-comfirm:

消息成功发送到exchange,返回ack

消息发送失败,没有到达交换机,返回nack

消息发送过程中出现异常,没有收到回执

(2)消息成功发送到exchange,但没有路由到queue,调用ReturnCallback

2. 消息持久化

直接创建交换机或队列、发送消息时,数据都是持久的。

2.1 交换机和队列持久化

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName CommonConfig
 * @Description 消息持久化配置类
 * @Author 孙克旭
 * @Date 2024/11/24 15:34
 */
@Configuration
public class CommonConfig {
    /**
     * 交换机持久化
     *
     * @return
     */
    @Bean
    public DirectExchange simpleDirect() {
        return new DirectExchange("simple.direct", true, false);
    }

    /**
     * 队列持久化
     *
     * @return
     */
    @Bean
    public Queue simpleQueue() {
        return QueueBuilder.durable("simple.queue").build();
    }
}

2.2 数据持久化

发送消息是指定数据发布的模式:

java 复制代码
   @Test
    public void testDurableMessage() {
        //1.准备消息
        Message message = MessageBuilder.withBody("hello RabbitMQ".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT) //消息持久化
                .build();
        //2.发送消息
        rabbitTemplate.convertAndSend("simple.queue", message); //没有指定交换机名称,会使用默认交换机
    }

3. 消息重发机制

RabbitMQ的消息没有被消费者读取后,会自动返回队列,再次发送至消费者,并一直循环,直到该消息被消费。这样会极大的浪费mq的性能,所以需要手动设置消息重发机制。

yaml 复制代码
spring:
  rabbitmq:
    host: 192.168.137.110 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: itcast
    password: 123456
    virtual-host: /
    listener:
      simple:
        prefetch: 1 #消费者每次只能读取一个消息
        acknowledge-mode: auto #消费者自动确认消息,表明消息已经被成功处理,然后RabbitMQ会从队列中移除这条消息
        retry:
          enabled: true #开启消费者失败重试
          initial-interval: 1000 #初始的失败等待时长为1秒
          multiplier: 3 #下次失败的等待时长倍数,下次等待时长=multiplier * last-interval
          max-attempts: 4 #最大重试次数
          stateless: true #ture 无状态;false 有状态。如果业务中包含事务,这里改为false

3.1 重发失败消息应对策略

消费者将错误消息发送至指定的交换机。

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName ErrorMessageConfig
 * @Description 重发失败消息应对策略配置类
 * @Author 孙克旭
 * @Date 2024/11/24 16:28
 */
@Configuration
public class ErrorMessageConfig {
    /**
     * 接受错误消息的交换机
     *
     * @return
     */
    @Bean
    public DirectExchange errorMessageExchange() {
        return new DirectExchange("error.direct");
    }

    /**
     * 接收错误消息的队列
     *
     * @return
     */
    @Bean
    public Queue errorQueue() {
        return new Queue("error.queue");
    }

    /**
     * 将队列绑定到交换机
     *
     * @return
     */
    @Bean
    public Binding errorMessageBinding() {
        return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
    }

    /**
     * 错误消息应对策略
     * 在消息处理失败时重新发布消息到指定的交换机和队列
     *
     * @param rabbitTemplate
     * @return
     */
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}

3.2 如何确保RabbitMQ消息的可靠性?

1.开启生产者确认机制,确保生产者的消息能到达队列

2.开启持久化功能,确保消息未消费前在队列中不会丢失

3.开启消费者确认机制为auto,由spring确认消息处理成功后完成ack

4.开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

4. 死信交换机

死信会被队列发送给死信交换机

4.1 什么样的消息会成为死信?

(1)消息被消费者reject或者返回nack

(2)消息超时未消费

(3)队列满了,早期的消息被丢弃

4.2 如何给队列绑定死信交换机?

(1)给队列设置dead-letter-exchange属性,指定一个交换机

(2)给队列设置dead-letter-routing-key属性,设置死信交换机与死信队列的RoutingKey

4.2.1 代码实现

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName TtlMessageConfig
 * @Description 死信消息路由规则配置类
 * @Author 孙克旭
 * @Date 2024/11/25 10:02
 */
@Configuration
public class TtlMessageConfig {
    /**
     * 交换机
     *
     * @return
     */
    @Bean
    public DirectExchange ttlDirectExchange() {
        return new DirectExchange("ttl.direct");
    }

    /**
     * 定义队列,指定死信交换机和routing key
     * 并设置消息存活时间
     *
     * @return
     */
    @Bean
    public Queue ttlQueue() {
        return QueueBuilder.durable("ttl.queue")
                .ttl(10000)
                .deadLetterExchange("dl.direct")
                .deadLetterRoutingKey("dl")
                .build();
    }

    /**
     * 定义绑定规则
     *
     * @return
     */
    @Bean
    public Binding ttlBinding() {
        return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
    }

}

监听器:

java 复制代码
package cn.itcast.mq.listener;

import lombok.extern.slf4j.Slf4j;
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;

/**
 * @ClassName SpringRabbitListener
 * @Description 队列监听器
 * @Author 孙克旭
 * @Date 2024/11/25 9:53
 */
@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
    }

    /**
     * 监听死信队列消息
     *
     * @param msg
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true"),
            exchange = @Exchange(name = "dl.direct"),
            key = "dl"
    ))
    public void listenDlQueue(String msg) {
        log.info("消费者接收到了dl.queue的延迟消息:{}", msg);
    }


}

测试代码:

java 复制代码
   @Test
    public void testTTLMessage() {
        //1.准备消息
        Message message = MessageBuilder.withBody("hello RabbitMQ".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT) //消息持久化
                .setExpiration("5000") //定义消息存活时间
                .build();
        //2.发送消息
        rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
    }

4.3 消息超时的两种方式是?

(1)给队列设置ttl属性

(2)给消息设置ttl属性

(3)两者共存时,以时间短的ttl为准

5. 延迟消息

5.1 安装rabbitmq插件

进入容器内部后,执行下面命令开启插件:

bash 复制代码
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

5.2 代码实现

消费者:

java 复制代码
    /**
     * 监听延迟消息
     * 创建队列
     * 创建延迟交换机,指定交换机类型为direct
     * 指定routing key
     *
     * @param msg
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue", durable = "true"),
            exchange = @Exchange(name = "delay.direct", type = "direct", delayed = "true"),
            key = "delay"))
    public void listenDelayExchange(String msg) {
        log.info("消费者接收到了delay.queue的延迟消息:{}", msg);
    }

生产者测试代码:

java 复制代码
    /**
     * 测试延迟消息
     */
    @Test
    public void testDelayMessage() {
        //1.准备消息
        Message message = MessageBuilder
                .withBody("hello RabbitMQ".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT) //消息持久化
                .setHeader("x-delay", 5000) //设置延迟时间
                .build();
        //2.发送消息
        rabbitTemplate.convertAndSend("delay.direct", "delay", message);
    }

5.3 延迟插件的使用步骤有哪些?

(1)声明一个交换机,添加delayed属性为true

(2)发送消息时,添加x-delay头,值为延时时间

6. 惰性队列

6.1 消息堆积问题

6.1.1 解决方案

(1)增加更多的消费者,提高消费速度

(2)在消费者内开启线程池加快消息处理速度

(3)扩大队列容积,提高堆积上限

6.2 惰性队列特征

(1)接收到消息直接存入磁盘而非内存

(2)消费者要消费消息时才会从磁盘读取并加载到内存

(3)支持数百万条的消息存储

6.2.1 优点

(1)基于磁盘存储,消息上限高

(2)没有间歇性的page-out(页面置换),性能比较稳定

plaintext 复制代码
这句话的意思是,在传统的队列模式下,RabbitMQ 会将消息存储在内存中,当内存不足时,会将内存中的消息换页至磁盘中,
这个操作会耗费较长的时间,并且可能会阻塞队列的操作,导致无法接收新的消息。
这种由于内存不足而导致的消息换页操作就是所谓的间歇性page-out。

而惰性队列由于一开始就将消息存储在磁盘上,避免了在内存和磁盘之间频繁地进行页面置换,因此减少了因页面置换导致的性能波动和
延迟,使得性能更加稳定。这意味着,即使在高负载或者消费者处理能力不足导致消息积压的情况下,惰性队列也能保持较好的性能表现,
不会因为内存压力而导致处理能力下降。

6.2.2 缺点

(1)基于磁盘存储,消息时效性会降低

(2)性能受限于磁盘IO

6.3 代码实现

在消费者中定义惰性队列:

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * @ClassName LazyConfig
 * @Description 惰性队列配置类
 * @Author 孙克旭
 * @Date 2024/11/25 13:23
 */
@Configuration
public class LazyConfig {

    /**
     * 声明一个惰性队列
     *
     * @return
     */
    @Bean
    public Queue LazyQueue() {
        return QueueBuilder.durable("lazy.queue")
                .lazy().build();
    }
}

7. RabbitMQ集群

在配置文件中添加集群地址:

7.1 普通集群

普通分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。每个节点有其他节点的元数据信息,但是没有其他节点的数据;当消费者读取一个节点时,若请求的队列不在该节点,该节点会自动根据元数据查找指定的队列,并返回给消费者。

7.2 镜像集群

镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。

7.3 仲裁队列

Raft协议是一种分布式一致性协议,它用于在分布式系统中的多个节点之间达成一致性。

8. 踩坑记录

1.在学习死信交换机时,在配置类中手动定义了ttl.exchange和ttl.queue,但是没有定义死信交换机和队列,测试也能正常通过。为啥死信交换机和队列能被自动创建?

原来是定义监听器时,该交换机和队列可以被持久化

java 复制代码
   /**
     * 监听死信队列消息
     *
     * @param msg
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true"),
            exchange = @Exchange(name = "dl.direct"),
            key = "dl"
    ))
    public void listenDlQueue(String msg) {
        log.info("消费者接收到了dl.queue的延迟消息:{}", msg);
    }

当你使用 @Queue@Exchange 注解定义队列和交换机时,Spring AMQP 会自动声明这些队列和交换机。这意味着,如果这些队列和交换机在 RabbitMQ 中不存在,Spring AMQP 会在应用启动时自动创建它们。这大大简化了配置,因为你不需要手动创建这些资源。

2.docker创建mq容器时指定了目录,但是不能运行容器。

后来发现是缺少参数:

bash 复制代码
docker run \
 -e RABBITMQ_DEFAULT_USER=itcast \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 -v $PWD/mq-plugins:/plugins \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 --privileged=true
 -d \
 rabbitmq:3.8-management

--privileged=true 表示开放权限,即当运行这个实例以后,完成容器内部和宿主机中指定的绝对路径实现了信息的共享。

相关推荐
大新新大浩浩3 小时前
arm64适配系列文章-第六章-arm64环境上rabbitmq-management的部署,构建cluster-operator
rabbitmq·arm
躺不平的理查德4 小时前
General Spark Operations(Spark 基础操作)
大数据·分布式·spark
talle20214 小时前
Zeppelin在spark环境导出dataframe
大数据·分布式·spark
渣渣盟4 小时前
大数据开发环境的安装,配置(Hadoop)
大数据·hadoop·分布式
Angindem5 小时前
SpringClound 微服务分布式Nacos学习笔记
分布式·学习·微服务
电脑玩家粉色男孩8 小时前
2、Ubuntu 环境下安装RabbitMQ
linux·rabbitmq
龙仔72513 小时前
离线安装rabbitmq全流程
分布式·rabbitmq·ruby
〆、风神16 小时前
Spring Boot 整合 Lock4j + Redisson 实现分布式锁实战
spring boot·分布式·后端
胡萝卜糊了Ohh17 小时前
kafka
分布式·kafka