• RabbitMq深度学习


    什么是RabbitMq?

    RabbitMQ是一个开源的消息队列中间件,它实现了高级消息队列协议(AMQP)。它被广泛用于分布式系统中的消息传递和异步通信。RabbitMQ提供了一种可靠的、可扩展的机制来传递消息,使不同的应用程序能够相互之间进行通信。它支持多种编程语言和平台,并且具有灵活的路由和队列配置选项。

    同步调用 

    同步调用的优点:

    • 时效性较强,可以立即得到结果

    同步调用的问题:

    • 耦合度高

    • 性能和吞吐能力下降

    • 有额外的资源消耗

    • 有级联失败问题

    异步调用

    好处:

    • 吞吐量提升:无需等待订阅者处理完成,响应更快速

    • 故障隔离:服务没有直接调用,不存在级联失败问题

    • 调用间没有阻塞,不会造成无效的资源占用

    • 耦合度极低,每个服务都可以灵活插拔,可替换

    • 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件

    缺点:

    • 架构复杂了,业务没有明显的流程线,不好管理

    • 需要依赖于Broker的可靠、安全、性能

    MQ的种类 

     RabbitMq安装和使用 

     云服务器安装Rabbitmq。

     在docker 中拉去Ribbitmq镜像。

    在docker 中运行ribbitmq。

    docker run -d -p 5672:5672 -p 15672:15672 -p 25672:25672 --name rabbitmq rabbitmq

     查看rabbitmq的状态。

    rabbitmqctl status

    接着我们还可以将Rabbitmq的管理面板开启,这样就可以在浏览器上进行实时访问和监控了。 

    我们需要先进入rabbitmq容器。

    docker exec -it [在docker中对应的ID] [进入容器的路径] #路径一般为/bin/bash

    开启rabbitmq的控制面板设置。

    rabbitmq-plugins enable rabbitmq_management

    打开rabbitmq的控制面板,就是对应的控制面板端口为15672。

    账号和密码都是:guest

     消息队列模型

     SpringAMQP

     什么是springAMQP?

    Spring AMQP 是一个基于 Spring 框架的 AMQP(高级消息队列协议)的开发框架。它提供了一种简化和抽象化的方式来使用 AMQP,使得在应用程序中使用消息队列变得更加容易。

    springAMQP的使用

    导入依赖

    1. <!--AMQP依赖,包含RabbitMQ-->
    2. <dependency>
    3. <groupId>org.springframework.boot</groupId>
    4. <artifactId>spring-boot-starter-amqp</artifactId>
    5. </dependency>

    编写发送者

    编写applcation.yml文件

    1. spring:
    2. rabbitmq:
    3. host: 119.9.212.171 # 主机名
    4. port: 5672 # 端口
    5. virtual-host: / # 虚拟主机
    6. username: guest # 用户名
    7. password: guest # 密码

    进行测试

    1. import com.rabbitmq.client.Channel;
    2. import com.rabbitmq.client.Connection;
    3. import com.rabbitmq.client.ConnectionFactory;
    4. import org.junit.Test;
    5. import org.junit.runner.RunWith;
    6. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.boot.test.context.SpringBootTest;
    9. import org.springframework.test.context.junit4.SpringRunner;
    10. import java.io.IOException;
    11. import java.util.concurrent.TimeoutException;
    12. @RunWith(SpringRunner.class) #如果不加此注解,spring容器无法自动注入RabbitTemplate
    13. @SpringBootTest
    14. public class PublisherTest {
    15. @Autowired
    16. RabbitTemplate rabbitTemplate;
    17. @Test
    18. public void tess1() {
    19. String queueName = "queueName";
    20. String message = "hello, tolen";
    21. rabbitTemplate.convertAndSend(queueName, message);
    22. }
    23. }

    测试结果为下:

     可能会出现没有队列生成的情况,这是因为@Test无法自动一个 queue,我们手动创建一个即可。

    编写消费者

    编辑application.yml文件

    1. spring:
    2. rabbitmq:
    3. host: 192.168.150.101 # 主机名
    4. port: 5672 # 端口
    5. virtual-host: / # 虚拟主机
    6. username: test # 用户名
    7. password: 123456 # 密码

    创建消息监听者

    1. import org.springframework.amqp.rabbit.annotation.RabbitListener;
    2. import org.springframework.stereotype.Component;
    3. @Component
    4. public class RabbitMqListener {
    5. @RabbitListener(queues = "queueName")
    6. public void getMessage(String message) {
    7. System.out.println("获取的消息是:" + message);
    8. }
    9. }

    直接配置即可,在后续的项目中消费者会监听对应的消息进行操作。

    WorkQueue

    我们可以对一个消息标签设置多个监听者,并且默认的设置是预取,也就是即使服务模块处理能力差的情况也会分配到相同个数的信息,不能达到能者多劳的效果,为了到达此效果,我们可以在application.yml中进行设置。

    1. spring:
    2. rabbitmq:
    3. listener:
    4. simple:
    5. prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

    发布与订阅

    FanoutExchange的使用

    在消费者模块编写:新建交换机,新建队列,交换机和队列绑定操作。

    在配置类中完成上述操作

    1. import org.springframework.amqp.core.Binding;
    2. import org.springframework.amqp.core.BindingBuilder;
    3. import org.springframework.amqp.core.FanoutExchange;
    4. import org.springframework.amqp.core.Queue;
    5. import org.springframework.context.annotation.Bean;
    6. import org.springframework.context.annotation.Configuration;
    7. @Configuration
    8. public class MQConfiguration {
    9. //声明交换机FanoutExchange
    10. @Bean
    11. public FanoutExchange fanoutExchange() {
    12. // 设置交换机的名字
    13. return new FanoutExchange("tolen.fanout");
    14. }
    15. // 创建一个信息队列1
    16. @Bean
    17. public Queue fanoutQueue1() {
    18. return new Queue("fanout.queue1");
    19. }
    20. // 创建信息队列2
    21. @Bean
    22. public Queue fanoutQueue2() {
    23. return new Queue("fanout.queue2");
    24. }
    25. //将交换机和队列1进行绑定
    26. @Bean
    27. public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
    28. //绑定队列给对应的交换机
    29. return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    30. }
    31. //将交换机和队列2进行绑定
    32. @Bean
    33. public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
    34. return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    35. }
    36. }

    在消费者模块中创建两个队列的监听器

    1. import org.springframework.amqp.rabbit.annotation.RabbitListener;
    2. import org.springframework.stereotype.Component;
    3. @Component
    4. public class RabbitMqListener {
    5. @RabbitListener(queues = "fanout.queue1")
    6. public void getMessage1(String message) {
    7. System.out.println("消息队列1中获取的消息是:" + message);
    8. }
    9. @RabbitListener(queues = "fanout.queue2")
    10. public void getMessage2(String message) {
    11. System.out.println("消息队列2中获取的消息是:" + message);
    12. }
    13. }

    接下来不信消息发送模块,这里需要注意的是,此时我们是向对应的交换机发送消息,通过交换机发送消息给两个消息队列。

    发送消息的代码为下:

    1. import com.rabbitmq.client.Channel;
    2. import com.rabbitmq.client.Connection;
    3. import com.rabbitmq.client.ConnectionFactory;
    4. import org.junit.Test;
    5. import org.junit.runner.RunWith;
    6. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.boot.test.context.SpringBootTest;
    9. import org.springframework.test.context.junit4.SpringRunner;
    10. import java.io.IOException;
    11. import java.util.concurrent.TimeoutException;
    12. @RunWith(SpringRunner.class)
    13. @SpringBootTest
    14. public class PublisherTest {
    15. @Autowired
    16. RabbitTemplate rabbitTemplate;
    17. @Test
    18. public void tess1() {
    19. String queueName = "queueName";
    20. String message = "hello, tolen";
    21. rabbitTemplate.convertAndSend(queueName, message);
    22. }
    23. @Test
    24. public void fanoutTest() {
    25. String exchangeName = "tolen.fanout";
    26. String message = "hi, tolen!";
    27. //routingKey不进行设置
    28. rabbitTemplate.convertAndSend(exchangeName, "", message);
    29. }
    30. }

    如果不设置routingKey的话,就会默认将消息发送到使用绑定的消息队列上。 

    测试结果为下:

    交换机状态

    监听器接收到的消息 

     DirectExchange

    可以设置routingKey,交换机可以向指定的队列发送消息。

    配置监听器

    1. import org.springframework.amqp.rabbit.annotation.Exchange;
    2. import org.springframework.amqp.rabbit.annotation.Queue;
    3. import org.springframework.amqp.rabbit.annotation.QueueBinding;
    4. import org.springframework.amqp.rabbit.annotation.RabbitListener;
    5. import org.springframework.stereotype.Component;
    6. @Component
    7. public class RabbitMqListener {
    8. //使用注解进行绑定, 不再需要configuration配置
    9. @RabbitListener(bindings = @QueueBinding(
    10. value = @Queue(name = "directQueue1"),
    11. exchange = @Exchange(name = "direct"), //默认使用的交换机类型就是directExchange
    12. key = {"red", "blue"}
    13. ))
    14. public void directQueue1(String message) {
    15. System.out.println("directQueue2:" + message);
    16. }
    17. //使用注解进行绑定, 不再需要configuration配置
    18. @RabbitListener(bindings = @QueueBinding(
    19. value = @Queue(name = "directQueue2"),
    20. exchange = @Exchange(name = "direct"), //默认使用的交换机类型就是directExchange
    21. key = {"red"}
    22. ))
    23. public void directQueue2(String message) {
    24. System.out.println("directQueue2:" + message);
    25. }
    26. }

    编写消息发布模块

    1. import org.junit.Test;
    2. import org.junit.runner.RunWith;
    3. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.boot.test.context.SpringBootTest;
    6. import org.springframework.test.context.junit4.SpringRunner;
    7. @RunWith(SpringRunner.class)
    8. @SpringBootTest
    9. public class PublisherTest {
    10. @Autowired
    11. RabbitTemplate rabbitTemplate;
    12. @Test
    13. public void fanoutTest() {
    14. String exchangeName = "direct";
    15. String message = "hi, tolen!";
    16. //设置routingKey
    17. rabbitTemplate.convertAndSend(exchangeName, "blue", message);
    18. }
    19. }

    测试结果为下:

    此时就只有routingKey=blue的监听器才会接收到消息。

    TopicExchage

    Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

    Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

    通配符规则:

    #:匹配一个或多个词

    *:匹配不多不少恰好1个词

    修改编写监听器的配置

    1. //使用注解进行绑定, 不再需要configuration配置
    2. @RabbitListener(bindings = @QueueBinding(
    3. value = @Queue(name = "directQueue2"),
    4. exchange = @Exchange(name = "direct", type = ExchangeTypes.TOPIC), //默认使用的交换机类型就是directExchange
    5. key = {"#.new"}
    6. ))
    7. public void directQueue2(String message) {
    8. System.out.println("directQueue2:" + message);
    9. }

    只要发送的消息中的routingKey中尾部为新闻的消息全部会被监听。(routingKey使用"."作间隔)

    消息转换器

    在springboot中默认使用JDK的序列化,为了提高使用性,我们可以使用json转换器。

    在消费者和发送者中都导入对应的依赖。

    1. com.fasterxml.jackson.dataformat
    2. jackson-dataformat-xml
    3. 2.9.10

    在configuration中配置信息转换器。(消费者和发布者都需要配置)

    1. import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    2. import org.springframework.amqp.support.converter.MessageConverter;
    3. import org.springframework.context.annotation.Bean;
    4. import org.springframework.context.annotation.Configuration;
    5. @Configuration
    6. public class MQConfiguration {
    7. @Bean
    8. public MessageConverter jsonMessageConverter(){
    9. return new Jackson2JsonMessageConverter();
    10. }
    11. }

    进行测试,在发送一个对象类型的消息。

    对应的监听器

    1. import org.springframework.amqp.rabbit.annotation.Exchange;
    2. import org.springframework.amqp.rabbit.annotation.Queue;
    3. import org.springframework.amqp.rabbit.annotation.QueueBinding;
    4. import org.springframework.amqp.rabbit.annotation.RabbitListener;
    5. import org.springframework.stereotype.Component;
    6. import java.util.Map;
    7. import java.util.Objects;
    8. @Component
    9. public class RabbitMqListener {
    10. //使用注解进行绑定, 不再需要configuration配置
    11. @RabbitListener(bindings = @QueueBinding(
    12. value = @Queue(name = "directQueue2"),
    13. exchange = @Exchange(name = "direct"), //默认使用的交换机类型就是directExchange
    14. key = {"blue"}
    15. ))
    16. public void directQueue2(Map message) {
    17. System.out.println("directQueue2:" + message);
    18. }
    19. }

    对应的发送代码

    1. import org.junit.Test;
    2. import org.junit.runner.RunWith;
    3. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.boot.test.context.SpringBootTest;
    6. import org.springframework.test.context.junit4.SpringRunner;
    7. import java.util.LinkedHashMap;
    8. import java.util.Map;
    9. @RunWith(SpringRunner.class)
    10. @SpringBootTest
    11. public class PublisherTest {
    12. @Autowired
    13. RabbitTemplate rabbitTemplate;
    14. @Test
    15. public void fanoutTest() {
    16. String exchangeName = "direct";
    17. Map message = new LinkedHashMap<>();
    18. message.put("name", "tolen");
    19. message.put("age", "19");
    20. //设置routingKey
    21. rabbitTemplate.convertAndSend(exchangeName, "blue", message);
    22. }
    23. }

    测试效果为下:

    接收到的数据 。

     消息队列中的数据。

    mq存在的几个问题

    1.消息可靠性  

    在mq中我们没有办法直接知道消息发送和消息被消费者消费是否成功,所以需要使用方法来进行判断。

    消息发送的过程为下: 

     在该过程中可能会出现的情况: 发送消息到交换机失败, 交换机发送消息到消息队列失败,消费者消费信息失败。

    为此rabbitmq通过了publisher confirm 机制用于判断是否失败。

    返回的方式有两种

    1.pulish-confirm (该返回出现在 发送消息到交换机, 消费者消费消息)

            ack(acknowledge):执行成功。 

            nack(no-acknowledge) :执行失败。

    2.publish-reutrn (该返回出现在交换机将消息路由到消息队列)

            ack:交换机路由消息到消息队列失败,并且返回错误信息。

    实现pulish-confirm和publish-return (在生产者中设置)

    在信息发送模块publisher的application.yml中配置上述机制。

    1. spring:
    2. rabbitmq:
    3. host: 139.9.4.176 # rabbitMQ的ip地址
    4. port: 5672 # 端口
    5. username: test
    6. password: 123456
    7. virtual-host: /test
    8. publisher-confirm-type: correlated #开启publisher-confirm并设置confirm的方法
    9. publisher-returns: true #开启publisher-returns
    10. template:
    11. mandatory: false #自定义失败策略,如果为false则直接丢弃失败消息
    • publish-confirm-type:开启publisher-confirm,这里支持两种类型:

      • simple:同步等待confirm结果,直到超时

      • correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback

    • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback

    • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

    自定义publisher-return回调

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    3. import org.springframework.beans.BeansException;
    4. import org.springframework.context.ApplicationContext;
    5. import org.springframework.context.ApplicationContextAware;
    6. import org.springframework.context.annotation.Configuration;
    7. //自定义return回调
    8. @Slf4j
    9. @Configuration
    10. public class CommonConfig implements ApplicationContextAware {
    11. @Override
    12. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    13. //获取RabbitTemple在ioc中的bean实例
    14. RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
    15. //设置returnCallback
    16. rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
    17. //设置return机制错误后的操作
    18. //exchange路由消息到消息队列失败
    19. log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
    20. replyCode, replyText, exchange, routingKey, message.toString());
    21. //一般在最好会进行重试操作
    22. });
    23. }
    24. }

     自定义ConfirmCallback, 在触发publisher-confirm时自定义成功后操作和失败后的操作。(在测试类中设置)

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.junit.Test;
    3. import org.junit.runner.RunWith;
    4. import org.springframework.amqp.rabbit.connection.CorrelationData;
    5. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.boot.test.context.SpringBootTest;
    8. import org.springframework.test.context.junit4.SpringRunner;
    9. import java.util.UUID;
    10. @Slf4j
    11. @RunWith(SpringRunner.class)
    12. @SpringBootTest
    13. public class SpringAmqpTest {
    14. @Autowired
    15. private RabbitTemplate rabbitTemplate;
    16. @Test
    17. public void testSendMessage2SimpleQueue() throws InterruptedException {
    18. String message = "hello, spring amqp!";
    19. //创建CorrelationData,设置全局ID,并将其封装到CorrelationData中
    20. CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    21. //设置publisher-confirm回调方法,分别是: 成功后的方法,失败后的方法
    22. correlationData.getFuture().addCallback(
    23. result -> {
    24. //confirm成功,也就是ack
    25. if(result.isAck()) {
    26. log.debug("消息发送成功, ID:{}", correlationData.getId());
    27. } else {
    28. //confirm失败,也就是nack
    29. log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), result.getReason());
    30. }
    31. },
    32. ex -> {
    33. log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
    34. //一般会在此处重试发送
    35. });
    36. rabbitTemplate.convertAndSend("amp.topic", "simple.test", message, correlationData);
    37. // 休眠一会儿,等待ack回执,需要在rabbitmq连接断开之前消费消息,否则就会报错
    38. Thread.sleep(2000);
    39. }
    40. }

    进行测试

    如果出现下图情况,是以为channel和connection已经被关闭了,肯定就消费不了消息了。我们需要晚一点关闭这两个东西就行。
    也就是多休眠几秒(Thread.sleep(2000))。

    最终解决消息的可靠性问题。 

    2.消息持久化

    交换机的持久化

    实现代码为下:

    1. import org.springframework.amqp.core.DirectExchange;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. @Configuration
    5. public class MqConfig {
    6. //创建持久化交换机
    7. @Bean
    8. public DirectExchange getExchange() {
    9. //参数1:设置交换机的名字
    10. //参数2:设置是否持久化
    11. //参数3:设置是否在交换机中的消息队列被删除后自动删除该交换机
    12. return new DirectExchange("simple.direct", true, false);
    13. }
    14. }

    其实在源码的direct交换机中的构造函数默认参数2和参数3就是true和false。

    构造函数的源码为下:

    消息队列持久化

     实现代码为下:

    1. @Bean
    2. public Queue getQueue() {
    3. //通过QueueBuilder可以设置消息队列的属性
    4. //这里通过QueueBuilder的方法durable来设置持久化
    5. return QueueBuilder.durable("simple.queue").build();
    6. }

    其实在默认的queue创建的时候就是设置为持久化。

     消息的持久化 

    实现代码为下(需要在消息进行发送时设置其属性):

    1. @Test
    2. public void send2DurableMessage() {
    3. //将消息的编码设置为UTF-8
    4. //setDeliveryMode用于设置消息的存储方式,这里的PERSISTENT为持久化设置
    5. Message message = MessageBuilder.withBody("hello, tolen!".getBytes(StandardCharsets.UTF_8))
    6. .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
    7. .build();
    8. rabbitTemplate.convertAndSend("directQueue1", message);
    9. }

    消息持久化的效果图:

    delivery_mode=2就是表示消息为持久化消息。

    观察其他未手动设置持久化的信息。我们会发现delivery_mode都为2,说明默认消息都是持久化的

     消费者消息确认

    在上面我们实现了发送端的消息确认,此时存在的问题就是:我们无法确定消费者是否消费了,所以我们需要解决消费者的消息确认。

    在springAMQP中有三种消费者消息确认的模式,分别为下:

    1.manual:手动返回ack,在消息被消费者接收到消息后通过调用对应的api返回ack。

    在消费者模块中的application.yml中进行设置:

    1. spring:
    2. rabbitmq:
    3. listener:
    4. simple:
    5. acknowledge-mode: manual

    2. auto:自动返回ack,在消费者接收到消息后,没异常则直接返回ack,如果有异常的化就返回nack(消息会被重新发送,如果消息存在问题就会出现无限重新发送消息的情况)

     在消费者模块中的application.yml中进行设置:

    1. spring:
    2. rabbitmq:
    3. listener:
    4. simple:
    5. acknowledge-mode: auto

    3.none:发送者在发送消息后,就直接认为消息成功被接收了,不考虑消费者是否接收到了,消息在被发送后就直接被删除了。

    在消费者模块中的application.yml中进行设置:

    1. spring:
    2. rabbitmq:
    3. listener:
    4. simple:
    5. acknowledge-mode: none

    消费者消息确认如果使用auto模式的话,会出现无限重发消息的情况,这是我们就可以使用spring的retry机制进行本地重试的方式。

     本地重试的配置代码为下(在消费者中的application.ymnl中配置)

    1. spring:
    2. rabbitmq:
    3. listener:
    4. simple:
    5. retry:
    6. enabled: true # 开启消费者失败重试
    7. initial-interval: 1000 # 初识的失败等待时长为1
    8. multiplier: 2 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
    9. max-attempts: 4 # 最大重试次数
    10. stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

    initial-interval:初始的失败等待时长。下次等待时长就是 multiplier*last-interval

    multipliter:失败的等待时长倍数,最终这种情况的重试时间就是 1秒,  2秒, 4秒, 8秒,结束重试。

    max-attempts:最大重试的次数。

    stateless:在有事务的情况下开启,防止事务丢失。(正常情况就使用false)

    失败策略

    默认在使用auto模式时,多次重新发送消息失败后会直接丢弃,为了避免消息的丢失,spring提供三种失败策略。

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

     使用代码实现 RepublishMessageRecoverer(在消费者模块中实现)

    1. import org.springframework.amqp.core.Binding;
    2. import org.springframework.amqp.core.BindingBuilder;
    3. import org.springframework.amqp.core.DirectExchange;
    4. import org.springframework.amqp.core.Queue;
    5. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    6. import org.springframework.amqp.rabbit.retry.MessageRecoverer;
    7. import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
    8. import org.springframework.context.annotation.Bean;
    9. import org.springframework.context.annotation.Configuration;
    10. @Configuration
    11. public class CommonConfig {
    12. //创建error交换机
    13. @Bean
    14. public DirectExchange getErrorExchange() {
    15. return new DirectExchange("error.direct");
    16. }
    17. //创建消息队列
    18. @Bean
    19. public Queue getQueue() {
    20. return new Queue("error.queue");
    21. }
    22. //将二者进行绑定
    23. @Bean
    24. public Binding errorBinding() {
    25. //with方法就是设置routingKey,这里routingKey的值为:error
    26. return BindingBuilder.bind(getQueue()).to(getErrorExchange()).with("error");
    27. }
    28. //设置失败策略
    29. @Bean
    30. public MessageRecoverer getRepublishMessageRecoverer(RabbitTemplate rabbitTemplate) {
    31. return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    32. }
    33. }

    最终当消息的重试次数达到上限,就会将消息发送给error.direct交换机,该交换机会将对应的消息发送给error.queue,管理员就可以在该消息队列中查看错误消息的信息。(最终确保消息不会丢失)

    如果确保信息的可靠性

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

    2.开启消息队列和消息和交换机的持久化。

    3.开启消费者的消息确认机制,开启auto模式,在消息被接收到消息后返回ack。

    4.开启消费者失败重试机制,配置RepublishMessageRecoverer类,保证失败后的消息不会丢失。

    死信交换机 

    什么是死信?

    1.消费者使用reject或返回nack声明导致的失败的消息,并且此消息设置requeue为false。

    2.消息是个过期消息,且超时没有被消费。

    3.要投递的消息队列满了,无法被投递的消息。

    为了防止这些死信丢失,我们就需要创建一个死信交换机,用于接收死信。

    创建死信交换机的代码为下:

    1. import org.springframework.amqp.core.*;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. @Configuration
    5. public class CommonConfig {
    6. //创建普通的交换机
    7. @Bean
    8. public DirectExchange getDirectExchange() {
    9. return new DirectExchange("simple.direct");
    10. }
    11. //创建普通的消息队列
    12. @Bean
    13. public Queue getQueue() {
    14. return QueueBuilder.durable("simple.queue")
    15. .deadLetterExchange("dl.direct") //设置死信交换机的名字
    16. .build();
    17. }
    18. //将普通交换机和普通消息队列进行绑定
    19. @Bean
    20. public Binding DirectBinding() {
    21. return BindingBuilder.bind(getQueue()).to(getDirectExchange()).with("simple");//普通消息队列的routingKey为:simple
    22. }
    23. //创建死信交换机
    24. @Bean
    25. public DirectExchange getDlExchange() {
    26. return new DirectExchange("dl.direct");
    27. }
    28. //创建存放死信的消息队列
    29. @Bean
    30. public Queue getDLQueue() {
    31. return new Queue("dl.queue");
    32. }
    33. //将死信交换机和死信消息队列进行绑定
    34. @Bean
    35. public Binding DLBinding() {
    36. return BindingBuilder.bind(getDlExchange()).to(getDlExchange()).with("dl");
    37. }
    38. }

    只要在普通队列中出现的死信都会进入死信队列中,只需要提供该死信队列就可以获取对应的死信。

    死信队列和RepublishMessageRecoverer策略的区别

    死信队列的流程图为下:

    RepublishMessageRecoverer策略的流程图为下:

    死信队列中的死信先退回到消息队列中,提供消息队列发送给死信交换机,提供死信交换机将死信发送到死信消息队列中。(在发送端的消息队列中操作)

    而在RepublishMessageRecoverer策略中,异常的消息直接由消费者直接发送给异常交换机。(在消费端进行操作)

    TTL(Time-to-Live) 

    消息队列中的消息超时未被消费,则会成为死信,超时分为两种情况:

    1.消息所在的消息队列设置了超时时间。

    2.消息本身设置了过期时间。

    提供设置对应的过期时间,将死信传入死信队列,从而完成实现延迟队列。(用于解决需要延迟处理的业务)

    延迟队列的实现效果的步骤为下:

    1. 创建普通交换机和消息队列,死信交换机和消息队列,提供设置对应的routingKey进行发送消息。(这里直接使用注解创建对应的交换机和消息队列)

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.amqp.rabbit.annotation.*;
    3. import org.springframework.stereotype.Component;
    4. @Slf4j
    5. @Component
    6. public class SpringRabbitListener {
    7. @RabbitListener(bindings = @QueueBinding(
    8. value = @Queue(name = "dl.queue", durable = "true"),
    9. exchange = @Exchange(name = "dl.direct"),
    10. key = "dl"
    11. ))
    12. public void listenDLQueue(String message) {
    13. System.out.println("DL2message:" + message);
    14. log.info("DL2message:" + message);
    15. }
    16. //正常消费测试
    17. @RabbitListener(bindings = @QueueBinding(
    18. value = @Queue(name = "simple.queue", durable = "true",
    19. arguments = {
    20. @Argument(name = "x-dead-letter-exchange",value = "dl.direct"), //指定一下死信交换机
    21. @Argument(name = "x-dead-letter-routing-key",value = "dl"), //指定死信交换机的路由key
    22. @Argument(name = "x-message-ttl",value = "10000", type = "java.lang.Long") //指定过期时间,并设置时间的类型
    23. }),
    24. exchange = @Exchange(name = "simple.direct"),
    25. key = "simple"
    26. ))
    27. public void listenQueue(String message) {
    28. System.out.println("正常的message:" + message);
    29. log.info("正常的message:" + message);
    30. }
    31. }

    2.进行测试(让消息在普通的消息队列中过期,让其进入死信队列,并被消费)

    代码为下:

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.junit.Test;
    3. import org.junit.runner.RunWith;
    4. import org.springframework.amqp.rabbit.connection.CorrelationData;
    5. import org.springframework.amqp.rabbit.core.RabbitTemplate;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.boot.test.context.SpringBootTest;
    8. import org.springframework.test.context.junit4.SpringRunner;
    9. import java.util.UUID;
    10. @Slf4j
    11. @RunWith(SpringRunner.class)
    12. @SpringBootTest
    13. public class tests {
    14. @Autowired
    15. RabbitTemplate rabbitTemplate;
    16. @Test
    17. public void DLTest() {
    18. String message = "接收到死信!";
    19. CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    20. rabbitTemplate.convertAndSend("simple.direct", "simple", message, correlationData);
    21. log.info("消息发送了!");
    22. }
    23. }

    因为普通消息队列中的消息会被正常正常消费所以不会进入死信消息队列中。

     为了让消息过期,我们需要设置消息队列不进行自动监听,这里在@RabbitListener注解中使用autoStartup = "false"

    所以监听类的修改后的代码为下:

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.amqp.rabbit.annotation.*;
    3. import org.springframework.stereotype.Component;
    4. @Slf4j
    5. @Component
    6. public class SpringRabbitListener {
    7. @RabbitListener(bindings = @QueueBinding(
    8. value = @Queue(name = "dl.queue", durable = "true"),
    9. exchange = @Exchange(name = "dl.direct"),
    10. key = "dl"
    11. ))
    12. public void listenDLQueue(String message) {
    13. System.out.println("DL2message:" + message);
    14. log.info("DL2message:" + message);
    15. }
    16. //正常消费测试
    17. @RabbitListener(bindings = @QueueBinding(
    18. value = @Queue(name = "simple.queue", durable = "true",
    19. arguments = {
    20. @Argument(name = "x-dead-letter-exchange",value = "dl.direct"), //指定一下死信交换机
    21. @Argument(name = "x-dead-letter-routing-key",value = "dl"), //指定死信交换机的路由key
    22. @Argument(name = "x-message-ttl",value = "10000", type = "java.lang.Long") //指定过期时间,并设置时间的类型
    23. }),
    24. exchange = @Exchange(name = "simple.direct"),
    25. key = "simple"
    26. ),
    27. autoStartup = "false") //关闭自动监听,这里要保证消息过期,使得该死信进入死信队列
    28. public void listenQueue(String message) {
    29. System.out.println("正常的message:" + message);
    30. log.info("正常的message:" + message);
    31. }
    32. }

    就比上面的监听类多了个属性 autoStartup = "false"。

    测试结果为下:

    测试类的效果图

     死信队列的消费者监听到的结果图

    我们也可以设置消息的有效时间,使得消息过期,这样也可以达到死信的效果。

    设置消息有效时间的代码为下:

    1. //设置消息的有效性
    2. @Test
    3. public void setMessageValidity() {
    4. Message message = MessageBuilder.withBody("此消息为死信!".getBytes(StandardCharsets.UTF_8))
    5. .setDeliveryMode(MessageDeliveryMode.PERSISTENT) //把消息设置为持久化
    6. .setExpiration("5000") // 设置有效时间 (该时间要小于普通消息队列的有效时间, 这里就是要小于5000)
    7. .build();
    8. rabbitTemplate.convertAndSend("simple.direct", "simple", message);
    9. log.info("消息发送成功!");
    10. }

     这里设置信息的有效时间为:5秒。

    如果信息的有效时间和消息队列的有效时间都设置了,那么就会以那个更短的时间将信息变为死信。 

    惰性队列

    消息堆积问题:消息的消费速度小于消息的接收速度,最终导致消息堆积。

    解决方案:

    1.增加更多的消费者。

    2.使用多线程进行消费。

    3.扩大消息队列的容量。

    惰性队列

    就是基于方案三的思想进行实现的。

    惰性队列的优点:

    1.容量大,基于磁盘的大小。

    2.消费者要消费消息时才会到磁盘中查找消息。

    惰性队列的实现代码为下

    基于ioc的实现

    1. import org.springframework.amqp.core.QueueBuilder;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.amqp.core.Queue;
    5. @Configuration
    6. public class rabbitMQConfig {
    7. @Bean
    8. public Queue getLazyQueue() {
    9. return QueueBuilder
    10. .durable("lazy.queue")
    11. .lazy()
    12. .build();
    13. }
    14. }

    基于注解的实现

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.amqp.rabbit.annotation.*;
    3. import org.springframework.stereotype.Component;
    4. @Slf4j
    5. @Component
    6. public class SpringRabbitListener {
    7. @RabbitListener(bindings = @QueueBinding(
    8. value = @Queue(name = "lazy.queue", durable = "true",
    9. arguments = {
    10. @Argument(name = "x-queue-mode", value = "lazy")
    11. }),
    12. exchange = @Exchange(name = "lazy.direct"),
    13. key = "lazy"
    14. ),
    15. autoStartup = "false")
    16. public void LazyQueue(String message) {
    17. System.out.println(message);
    18. log.info("惰性队列!");
    19. }
    20. }

    进行测试,惰性队列和普通队列进行对比。

    测试代码为下:

    1. @Test //普通消息队列
    2. public void normalQueue() {
    3. String message = "欢迎!";
    4. for (int i = 0; i < 100000; i++) {
    5. rabbitTemplate.convertAndSend("queueName", message);
    6. }
    7. }
    8. @Test //惰性消息队列
    9. public void lazyQueue() {
    10. String message = "欢迎!";
    11. for (int i = 0; i < 100000; i++) {
    12. rabbitTemplate.convertAndSend("lazy.queue", message);
    13. }
    14. }

    惰性队列的优缺点

    优点:

    • 基于磁盘存储,消息上限高

    • 没有间歇性的page-out,性能比较稳定

    缺点:

    • 基于磁盘存储,消息时效性会降低

    • 性能受限于磁盘的IO

  • 相关阅读:
    kubeadm 部署方式修改kube-proxy为 ipvs模式
    【java筑基】IO流进阶之文件随机访问、序列化与反序列化
    路由算法(凑够五个字)
    基于单片机的自动循迹小车(论文+源码)
    树莓派4B_OpenCv学习笔记10:调整视频帧大小
    我的十年编程路 尾声
    IDEA中字符串怎么自动转义,双引号自动转义的小技巧
    卷积神经网络特征融合,两个神经网络同时训练
    vue 项目内vue指令常用
    Redis 学习笔记 3:黑马点评
  • 原文地址:https://blog.csdn.net/Ostkakah/article/details/132233583