• 消息队列-RabbitMQ(二)


    接上文《消息队列-RabbitMQ(一)

    在这里插入图片描述

    4、RabbitMQ与springboot整合

    4.1、依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>org.example</groupId>
      <artifactId>RabbitMQDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
    
      <name>RabbitMQDemo</name>
      <!-- FIXME change it to the project's website -->
      <url>http://www.example.com</url>
    
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath />
      </parent>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <fastjson.version>1.2.70</fastjson.version>
      </properties>
    
      <dependencies>
        <!-- SpringBoot 核心包 -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
        </dependency>
    
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <!-- AMQP客户端 -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    
        <!-- 阿里JSON解析器 -->
        <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>${fastjson.version}</version>
        </dependency>
    
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.10</version>
        </dependency>
    
      </dependencies>
    
      <repositories>
        <!--阿里云镜像仓库-->
        <repository>
          <id>public</id>
          <name>aliyun nexus</name>
          <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
          <releases>
            <enabled>true</enabled>
          </releases>
        </repository>
        <!--oracle驱动没有发布到中央仓库,只能从此仓库下载-->
        <repository>
          <id>jeecg</id>
          <name>jeecg Repository</name>
          <url>http://maven.jeewx.com/nexus/content/repositories/jeecg</url>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
    
    </project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    4.2、配置类

    @Configuration
    public class RabbitMqConfig {
        // 消息的消费方json数据的反序列化
        @Bean
        public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
            factory.setConnectionFactory(connectionFactory);
            factory.setMessageConverter(new Jackson2JsonMessageConverter());
            return factory;
        }
    
        // 定义使用json的方式转换数据
        @Bean
        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
            RabbitTemplate amqpTemplate = new RabbitTemplate();
            amqpTemplate.setConnectionFactory(connectionFactory);
            amqpTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
            return amqpTemplate;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4.3、启动类

    @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
    public class App
    {
        public static void main(String[] args)
        {
            SpringApplication.run(App.class, args);
            System.out.println("(♥◠‿◠)ノ゙  启动成功   ლ(´ڡ`ლ)゙ ");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4.4、application.yml

    # 开发环境配置
    server:
      # 服务器的HTTP端口,默认为8080
      port: 8899
      servlet:
        # 应用的访问路径
        context-path: /
      tomcat:
        # tomcat的URI编码
        uri-encoding: UTF-8
        # tomcat最大线程数,默认为200
        max-threads: 800
        # Tomcat启动初始化的线程数,默认值25
        min-spare-threads: 30
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    📌简单模型

    1、生产者

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        public void simpleMessageSend() {
            System.out.println("simpleMessageSend");
            User user = new User();
            user.setUserId("1001");
            user.setUserName("张三");
            rabbitTemplate.convertAndSend("simpleQueue", user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    User:

    @Data
    public class User {
        private String userId; //用户编号
        private String userName; //用户姓名
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、消费者

    @Service
    public class TestConsumer {
    
        @RabbitListener(queuesToDeclare = {@Queue("simpleQueue")})
        public void simpleModel(User user) {
            System.out.println("接收消息message=" + user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、控制器

    @RestController
    @RequestMapping("/rabbitMqReq")
    public class RabbitMqController {
        @Autowired
        private TestProducer testProducer;
    
        @PostMapping("/simple")
        public void simple(@RequestBody User user) {
            testProducer.simpleMessageSend();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    💦 postman 请求 http://localhost:8899/rabbitMqReq/simple:
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    📌工作模型

    工作模式只需要在简单模式的基础上,添加一个消息的消费方。

    📌发布订阅模型

    1、生产者

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        // fanout模型
        public void fanoutMessageSend() {
            for (int i = 0; i < 5; i++) {
                User user = new User();
                user.setUserId("1001");
                user.setUserName("张三");
                rabbitTemplate.convertAndSend("fanout-ex", "", user);
            }
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2、消费者

    @Service
    public class TestConsumer {
    
        // value=@Queue 创建临时队列
        // exchange创建交换机
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        exchange = @Exchange(value = "fanout-ex", type = ExchangeTypes.FANOUT))
        })
        public void receiveMessage1(User user) {
            System.out.println(String.format("消费者 【one】: %s", user));
        }
    
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        exchange = @Exchange(value = "fanout-ex", type = ExchangeTypes.FANOUT))
        })
        public void receiveMessage2(User user) {
            System.out.println(String.format("消费者 【two】: %s", user));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3、控制器

    @RestController
    @RequestMapping("/rabbitMqReq")
    public class RabbitMqController {
        @Autowired
        private TestProducer testProducer;
    
        @PostMapping("/fanoutMessageSend")
        public void fanoutMessageSend(@RequestBody User user) {
            testProducer.fanoutMessageSend();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    💦 postman 请求 http://localhost:8899/rabbitMqReq/fanoutMessageSend:

    在这里插入图片描述

    📌direct模型

    1、生产者

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        // direct模型
        public void directMessageSend() {
            User user = new User();
            user.setUserId("1001");
            user.setUserName("张三");
            //rabbitTemplate.convertAndSend("direct-ex", "success", user);
            rabbitTemplate.convertAndSend("direct-ex", "error", user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2、消费者

    @Service
    public class TestConsumer {
    
        // 直连模式(direct)
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        key = {"error", "success"},
                        exchange = @Exchange(value = "direct-ex", type = ExchangeTypes.DIRECT))
        })
        public void receiveMessage3(User user, Message message, Channel channel) throws IOException {
            System.out.println(String.format("消费者 【one】: %s", user));
        }
    
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        key = {"error"},
                        exchange = @Exchange(value = "direct-ex", type = ExchangeTypes.DIRECT))
        })
        public void receiveMessage4(Message message, Channel channel, User user) throws IOException {
            System.out.println(String.format("消费者 【two】: %s", user));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3、控制器

    @RestController
    @RequestMapping("/rabbitMqReq")
    public class RabbitMqController {
        @Autowired
        private TestProducer testProducer;
    
        @PostMapping("/directMessageSend")
        public void directMessageSend(@RequestBody User user) {
            testProducer.directMessageSend();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    💦 postman 请求 http://localhost:8899/rabbitMqReq/directMessageSend:

    在这里插入图片描述

    📌topic模型

    1、生产者

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        // topic模型
        public void topicMessageSend() {
            User user = new User();
            user.setUserId("1001");
            user.setUserName("张三");
            rabbitTemplate.convertAndSend("topic-ex", "B1.TH", user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2、消费者

    @Service
    public class TestConsumer {
    
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        key = {"B1.#"},
                        exchange = @Exchange(value = "topic-ex", type = ExchangeTypes.TOPIC))
        })
        public void receiveMessage5(User user) {
            System.out.println(String.format("消费者 【one】: %s", user));
        }
    
        @RabbitListener(bindings = {
                @QueueBinding(value = @Queue,
                        key = {"B1.T2.*"},
                        exchange = @Exchange(value = "topic-ex", type = ExchangeTypes.TOPIC))
        })
        public void receiveMessage6(User user) {
            System.out.println(String.format("消费者 【two】: %s", user));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3、控制器

    @RestController
    @RequestMapping("/rabbitMqReq")
    public class RabbitMqController {
        @Autowired
        private TestProducer testProducer;
    
        @PostMapping("/topicMessageSend")
        public void topicMessageSend(@RequestBody User user) {
            testProducer.topicMessageSend();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    💦 postman 请求 http://localhost:8899/rabbitMqReq/topicMessageSend:

    在这里插入图片描述

    5、消息确认机制

    参考:📖 RabbitMq 消息确认机制详解

    消息从发送,到消费者接收,会经理多个过程:

    在这里插入图片描述

    其中的每一步都可能导致消息丢失,常见的丢失原因包括:

    • 生产者发送时丢失:
      • 生产者发送的消息未送达exchange
      • 消息到达exchange后未到达queue
    • MQ宕机,queue将消息丢失。
    • 消费者接收到消息后未消费就宕机。

    针对这些问题,RabbitMQ分别给出了解决方案

    • 生产者确认机制
    • mq持久化
    • 消费者确认机制
    • 失败重试机制

    5.1、生产者消息确认

    RabbitMQ 提供了生产者确认机制来避免消息发送到 MQ 过程中丢失。这种机制 必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

    • publisher-confirm发送者确认

      • 消息成功投递到交换机,返回 ack。
      • 消息未投递到交换机,返回 nack。
    • publisher-return发送者回执

      • 消息投递到交换机了,但是没有路由到队列。返回 ack,及路由失败原因。

    在这里插入图片描述

    5.1.1、修改配置

    application.yml:

    spring:
      rabbitmq:
        publisher-confirm-type: correlated
        publisher-returns: true
        template:
          mandatory: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    说明:

    • publish-confirm-type:开启 publisher-confirm,这里支持两种类型:
      • NONE:禁用发布确认模式,是默认值
      • simple:同步等待 confirm 结果,直到超时
      • correlated:异步回调,定义 ConfirmCallback,MQ返回结果时会回调这个 ConfirmCallback。
    • publish-returns:开启 publish-return 功能,同样是基于 callback机 制,不过是定义 ReturnCallback
    • template.mandatory:定义消息路由失败时的策略。true:则调用 ReturnCallback;false:则直接丢弃消息。

    ⚠️注意⚠️:如果rabbitmq的版本低,publisher-confirm-type: correlated可能不生效,配置publisher-confirms: true

    5.1.2、定义ReturnCallback、ConfirmCallback

    • 每个 RabbitTemplate 只能配置一个 ReturnCallback;
    • ConfirmCallback 可以在发送消息时指定,因为每个业务处理 confirm 成功或失败的逻辑不一定相同。也可以在 RabbitTemplate 统一配置。

    RabbitMqConfig

    @Configuration
    public class RabbitMqConfig {
        // 消息的消费方json数据的反序列化
        @Bean
        public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
            factory.setConnectionFactory(connectionFactory);
            factory.setMessageConverter(new Jackson2JsonMessageConverter());
            //消息的手动确认
            //factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
            return factory;
        }
    
        // 定义使用json的方式转换数据
        @Bean
        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
            RabbitTemplate amqpTemplate = new RabbitTemplate();
            amqpTemplate.setConnectionFactory(connectionFactory);
            // 定义转换器
            amqpTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    
            // 开启 setReturnCallback
            amqpTemplate.setMandatory(true);
    
            // 定义ConfirmCallback exchange告诉程序是否以及到达交换机,该方法无论成功都会回调。如果到达ack为true; 否则为false;
            amqpTemplate.setConfirmCallback((correlationData, ack, cause) -> {
                if(ack) {
                    System.out.println("成功到达交换机, ID:" + correlationData.getId());
                }else {
                    System.out.println("没有到达交换机, ID:" + correlationData.getId() + ", 原因:" + cause);
                }
            });
    
            // 定义ReturnCallback
            amqpTemplate.setReturnCallback((message, replyCode, replyText, ex, rk) -> {
                System.out.println("消息没有到达队列");
            });
    
            return amqpTemplate;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    生产者-TestProducer

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        // direct模型
        public void directMessageSend() {
            // 1、定义发送消息
            User user = new User();
            user.setUserId("1001");
            user.setUserName("张三");
    
            // 2.全局唯一的消息ID,需要封装到CorrelationData中
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    
            // 3.发送消息
            rabbitTemplate.convertAndSend("direct-ex", "error", user, correlationData);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    💦 postman 请求 http://localhost:8899/rabbitMqReq/directMessageSend:

    在这里插入图片描述

    📌 ConfirmCallback 还可以发送消息时指定:

    生产者-TestProducer

    @Service
    public class TestProducer {
        @Resource
        private RabbitTemplate rabbitTemplate;
    
        // direct模型
        public void directMessageSend() {
            // 1、定义发送消息
            User user = new User();
            user.setUserId("1001");
            user.setUserName("张三");
    
            // 2.全局唯一的消息ID,需要封装到CorrelationData中
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    
            // 3.添加callback
            correlationData.getFuture().addCallback(
                    result -> {
                        if(result.isAck()){
                            // 3.1.ack,消息成功
                            System.out.println("消息发送成功, ID:" + correlationData.getId());
                        }else{
                            // 3.2.nack,消息失败
                            System.out.println("消息发送失败, ID:" + correlationData.getId() + ", 原因:" + result.getReason());
                        }
                    },
                    ex -> System.out.println("消息发送异常, ID:" + correlationData.getId() + ", 原因:" + ex.getMessage())
            );
            // 4.发送消息
            rabbitTemplate.convertAndSend("direct-ex", "error", user, correlationData);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    💦 postman 请求 http://localhost:8899/rabbitMqReq/directMessageSend:

    在这里插入图片描述

    5.2、消息持久化

    生产者确认可以确保消息投递到 RabbitMQ 的队列中,但是消息发送到 RabbitMQ 以后,如果突然宕机,也可能导致消息丢失。

    要想确保消息在 RabbitMQ 中安全保存,必须开启消息持久化机制。

    • 交换机持久化
    • 队列持久化
    • 消息持久化

    5.2.1、交换机持久化

    RabbitMQ中交换机默认是非持久化的,mq重启后就丢失。SpringAMQP中可以通过代码指定交换机持久化:

    @Bean
    public DirectExchange simpleExchange(){
        // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
        return new DirectExchange("simple.direct", true, false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.2.2、队列持久化

    RabbitMQ中队列默认是非持久化的,mq重启后就丢失。SpringAMQP中可以通过代码指定交换机持久化:

    @Bean
    public Queue simpleQueue(){
        // 使用QueueBuilder构建队列,durable就是持久化的
        return QueueBuilder.durable("simple.queue").build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.2.3、消息持久化

    利用SpringAMQP发送消息时,可以设置消息的属性(MessageProperties),指定delivery-mode。

    5.3、消费者消息确认

    RabbitMQ 是 阅后即焚 机制,RabbitMQ 确认消息被消费者消费后会立刻删除。而 RabbitMQ 是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向 RabbitMQ 发送 ACK 回执,表明自己已经处理消息。

    ⚠️ 假如 RabbitMQ 投递消息给消费者,消费者获取消息后,返回 ACK 给 RabbitMQ,RabbitMQ 收到 ACK 后删除消息,突然,消费者宕机,消息尚未处理。这样,消息就丢失了。因此消费者返回 ACK 的时机非常重要

    SpringAMQP 则允许消费者配置三种确认模式:

    • none:关闭 ack,MQ 假定消费者获取消息后会成功处理,因此消息投递后立即被删除。
    • manual:手动 ack,需要在业务代码结束后,调用 api 发送 ack。
    • auto:自动 ack,由 spring 监测 listener 代码是否出现异常,没有异常则返回 ack;抛出异常则返回 nack。

    由此可知:

    • none模式下,消息投递是不可靠的,可能丢失。
    • manual:自己根据业务情况,判断什么时候该ack。
    • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack。默认

    一般,重要消息用manual,其他,使用默认的auto即可。

    5.3.1、none模式

    application.yml:

    spring:
      rabbitmq:
        listener:
          simple:
            acknowledge-mode: none # 关闭ack
    
    • 1
    • 2
    • 3
    • 4
    • 5

    说明:当 RabbitMqConfig 配置了json反序列化,代码中实例化了SimpleRabbitListenerContainerFactory,会默认覆盖application.yml文件中的配置,需要在代码层面手动的设置提交的方式:

    factory.setAcknowledgeMode(AcknowledgeMode.NONE);
    
    • 1

    完整 RabbitMqConfig

    @Configuration
    public class RabbitMqConfig {
        // 消息的消费方json数据的反序列化
        @Bean
        public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
            factory.setConnectionFactory(connectionFactory);
            factory.setMessageConverter(new Jackson2JsonMessageConverter());
            //消息确定模式
            factory.setAcknowledgeMode(AcknowledgeMode.NONE);
            return factory;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    💦 修改 TestConsumer 类中的方法,模拟一个消息处理异常:

    @RabbitListener(queuesToDeclare = {@Queue("simpleQueue")})
    public void simpleModel(User user) {
        int i = 1/0;
        System.out.println("接收消息message=" + user);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    在这里插入图片描述

    测试可以发现,当消息处理抛异常时,消息依然被RabbitMQ删除了。

    5.3.2、auto模式

    factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
    
    • 1

    在这里插入图片描述

    抛出异常后,因为Spring会自动返回nack,所以消息恢复至Ready状态,并且没有被RabbitMQ删除。

    5.3.3、manual模式

    //消息的手动确认
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    
    • 1
    • 2

    在这里插入图片描述

    抛出异常后,没有手动返回 ack,所以消息恢复至Unacked状态,并且没有被RabbitMQ删除。

    📌 消费者手动返回 ack:

    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    
    • 1
    @RabbitListener(queuesToDeclare = {@Queue("simpleQueue")})
    public void simpleModel(User user, Message message, Channel channel) throws IOException {
        //int i = 1/0;
        System.out.println("接收消息message=" + user);
    
        // 消费者手动返回 ack
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    消费者手动返回 ack,RabbitMQ 收到 ACK 回执,删除消息。

    5.4、失败重试机制

    当消费者出现异常后,消息会不断 requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。

    5.4.1、本地重试

    我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

    application.yml

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

    重启consumer服务,重复之前的测试。可以发现:

    • 在重试3次后,SpringAMQP会抛出异常AmqpRejectAndDontRequeueException,说明本地重试触发了。
    • 查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是ack,mq删除消息了。

    结论:

    • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试。
    • 重试达到最大次数后,Spring会返回ack,消息会被丢弃。

    5.4.2、失败策略

    达到最大重试次数后,消息会被丢弃,这是由Spring内部机制决定的。

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式。
    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。

    比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

    @Configuration
    public class ErrorMessageConfig {
    
        // 在consumer服务中定义处理失败消息的交换机和队列
        @Bean
        public DirectExchange errorMessageExchange(){
            return new DirectExchange("error.direct");
        }
        @Bean
        public Queue errorQueue(){
            return new Queue("error.queue", true);
        }
        @Bean
        public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
            return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
        }
     
        // 定义一个 RepublishMessageRecoverer,关联队列和交换机
        @Bean
        public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
            return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    5.5、小结

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

    • 开启生产者确认机制,确保生产者的消息能到达队列。
    • 开启持久化功能,确保消息未消费前在队列中不会丢失。
    • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack。
    • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。

    6、幂等性

    所有的消息中间件都会存在这样一个问题,那就是消息的重复消费问题,例如说记录用户的积分信息,消息每次消费都会生成一条记录,这会队我们的业务带来致命的问题,所以我们必须做幂等性设计,所谓幂等设计就是,一条消息无论消费多少次所产生的结果都是相同的

    6.1 方案一

    为每条消息生成全局唯一ID,每次消费消息之后都将ID在表中插入一条数据,每次消费之前先查询ID是否存在,如果不存在就执行对应的逻辑;如果存在则直接确认。

    6.2 方案二

    利用 redis+数据库 的方案来实现幂等性的设计,给消息分配一个全局唯一ID,只要消费过该消息,将以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

    7、消息的重试机制

    消息的重试是发生在消息的消费端。详见 5.4

    8、面试

    📖 面试百问:使用MQ的优势、劣势以及问题

    8.1、如何保证消息不丢失?

    首先,RabbitMQ由生产者、交换机、队列、消费者组成。

    • 开启生产者确认机制,确保生产者的消息能到达队列。
    • 开启持久化功能,确保消息未消费前在队列中不会丢失。
    • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack。
    • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。

    8.2、如何保证消息的不重复消费?

    利用 redis 的方案来实现幂等性的设计,给消息分配一个全局唯一ID,只要消费过该消息,将以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

    8.3、在工作中mq用在哪里?

    发送第三方支付系统,交易状态推送通知。支付回调。

    8.4、使用RabbitMQ注意哪些?

    1. 确认模式的选择: 根据具体业务需求选择合适的确认模式。如果对消息传递的可靠性要求较高,建议使用手动确认模式或批量确认模式。
    2. 设置消息持久化: 在发布消息时,通过设置消息的持久化属性,可以确保即使RabbitMQ服务器意外关闭或重启,消息仍然能够被保存下来。
    3. 处理未确认消息: 如果消费者在处理消息时发生异常,导致无法发送确认信号给RabbitMQ,那么这些消息将保持在队列中,并可能被重新投递。需要设定适当的重试策略和错误处理机制来处理这些未确认的消息。
    4. 监听确认超时: 在使用批量确认模式时,如果长时间没有收到确认信号,需要设置合理的超时时间,并采取相应措施来处理超时的情况。

    8.5、如何使用mq来是实现分布式事务?

    📌 什么是事务?
    数据库事务。原子性、一致性、隔离性、持久性。

    📌 本地事务?
    整个服务操作只能涉及一个单一的数据库资源。

    📌 分布式事务?
    分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。分布式事务就是为了保证不同数据库的数据一致性。

    📌 分布式事务应用架构?
    微服务架构的分布式应用环境下,越来越多的应用要求对多个数据库资源,多个服务的访问都能纳入到同一个事务当中,分布式事务应运而生。消息中间件来处理分布式事务。

    📌 RocketMQ事务消息发送步骤如下:
    1、发送方将半事务消息发送至消息队列RocketMQ。
    2、消息队列 RocketMQ 将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。
    3、发送方开始执行本地事务逻辑。
    4、发送方根据本地事务执行结果向服务端提交二次确认(Commit或是Rollback),服务端收到Commit状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半事务消息,订阅方将不会接受该消息。

    事务消息回查步骤如下:
    1、在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
    2、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
    3、发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。

    📌 如何用 RabbitMQ 解决分布式事务?
    在消息驱动的微服务中,服务之间不再互相直接调用,当服务之间需要通信时,就把通信内容发送到消息中间件上,另一个服务则通过监听消息中间件中的消息队列,来完成相应的业务逻辑调用。

    用户购票的案例:
    在这里插入图片描述

    1. 向新订单队列中写入一条数据。
    2. Order Service 负责消费这个队列中的消息,完成订单的创建,然后再向新订单缴费队列中写入一条消息。
    3. User Service 负责消费新订单缴费队列中的消息,在 User Service 中完成对用户账户余额的划扣,然后向新订单转移票队列中写入一条消息。
    4. Ticket Service 负责消费新订单转移票队列,在 Ticket Service 中完成票的转移,然后发送一条消息给订单完成队列。
    5. 最后 Order Service 中负责监听订单完成队列,处理完成后的订单。

    扣款失败:

    1. 撤销票的转移,也就是把票的 owner 字段重新置为 null。
    2. 撤销锁票,也就是把票的 lock_user 字段重新置为 null。
    3. 向 order:fail 队列发送订单失败的消息。

    这就是一个典型的消息驱动微服务,也是一个典型的响应式系统。

    互联网核心,通过轮询查询、消息推送来实现分布式事务,查询的结果或者推送的结果修改原先交易的状态,失败回退还是成功。

    9、结语

    目前市面上消息中间件还有阿里开源的 RocketMQ,大数据生态的不可或缺的Kafka,Kafka对日志进行收集分析,数据处理一般接入Hadoop分析处理,或者是Logstash+Elasticsearch。

    如何技术选型,首先,先有业务场景,然后再有适配这种场景的技术。什么样的场景选择什么样的技术。

    📚参考文章

    📖 RabbitMQ使用详解
    📖 RabbitMq 消息确认机制详解
    📖 rabbitmq是什么,主要用于哪些方面?
    📖 用了8年MQ!聊聊消息队列的技术选型,哪个最香!
    📖 绝对详细的 RabbitMQ入门,看完本系列就够了

  • 相关阅读:
    数据库视图解析[普通视图、物化视图以及通过修改视图修改数据]
    C++ Reference: Standard C++ Library reference: C Library: cstdio: ferror
    Android App内存泄漏原理、检测及修改方案
    MVC三层架构
    好的架构是进化来的,不是设计来的
    Vue源码篇
    膜拜,华为18级工程师用349页构建高可用Linux服务器,其实并不难
    每日一题——Java编程练习题
    Pandas数据分析31——全国城市房价分析及样式可视化
    element ui中el-form-item的属性rules的用法
  • 原文地址:https://blog.csdn.net/weixin_40017062/article/details/133359418