rabbitmq学习笔记

rabbitmq学习笔记

安装rabbitmq环境

一 简介

01 特点

  • 开发语言: erlang

  • 单机吞吐量: 万级

  • 消息延迟: 微妙级

  • 功能特性: 并发能力强,性能极其好,时延低,社区活跃,管理界面丰富

 

02 消息队列的应用场景

应用解耦,异步处理,流量削峰,消息通知

应用解耦:

之前的做法通过feign调用,现在的做法通过消息队列

异步处理:

示例:由之前的串行发短信发邮件,变成发短信和发邮件订阅消息队列

流量削峰

数据库的处理并发能力有限,由消息队列平稳的去处理消息

 

03 AMQP协议

AMQP( Advanced Message Queueing Protocol)协议是一个高级抽象层消息通信协议,RabbitMQ是AMQP协议的实现。架构模型如下

 

 

04 名词解释

  • broker: 一个中间件实例,及一个RabbitMQ 服务器

  • message: 消息,就是要传输的数据

  • producer: 消息的生产者,就是将数据发送给RabbitMQ的客户端

  • consumer: 消息的消费者,用户获取并处理生产者发送的数据

  • exchange: 交换器,消息需要从交换器按照不同的规则分发到指定的目的地(队列)

  • queue: 队列, 消息最终要存放到队列,然后由对应的消费者消费队列里的消息

  • routing key: 路由key,交换器可以根据路由规则,决定消息该发往哪里

  • binding key: 将路由key和队列进行绑定,(在exchange配置队列时完成)

 

05 四种交换机类型

4种交换器类型
Direct Exchange: 直连交换器
  小结:直连交换器发送消息会根据路由和交换机的绑定关系发送到队列
  Fanout Exchange: 分发交换器(扇出交换器)
小结: 分发交换器发送消息会分发至所有和其有绑定的队列中,这样消息会被多个消费者处理
  Topic Exchange: 主题交换器
小结:主题交换器可以让每个队列只接收它关注的信息
可以根据路由key 进行模式匹配 有两种方式: 分别是 # 和 *
# 表示支持多个单词 单词的长度不限制 多个单词间使用.分隔
* 表示只能匹配一个单词 单词的长度不限制
Headers Exchange: 头信息交换器(了解)
头信息交换器可以实现更为复杂的匹配但性能不好,
      不推荐使用,了解即可

注释

Direct Exchange: 直连交换器,如果交换器名称为"",将使用默认交换器,默认交换器不会绑定任何队列,mq会直接把route_key当做queue名称去查找
Fanout Exchange 只要和该交换机绑定的队列都会接收到消息
Topic Exchange: 主题交换器
   例如exchange的 A队列的builder routing key 为a.*
   符合那么发送消息的路由key如果是a.2356 或者a.456 都是可以发送到A队列,而a.456.456不可以
   例如exchange的 B队列的builder routing key 为B.#
   符合那么发送消息的路由key如果是b.2356 或者b.456.45564 都是可以发送到B队列

 

06 五种工作模式

  • 简单模式: 消息只有一个消费者 使用默认交换器即可(direct)

  • 工作队列模式: 消息有多个消费者,消息只可以被消费一次 使用默认交换器即可(direct)

  • 发布订阅模式: 消息有多个消费者,而且消息会被多个消费者同时消费 使用分发交换器即可(fanout)

  • 路由模式: 根据路由的key,将消息发送到指定的队列 使用直接交换器即可(direct)

  • 通配符(主题)模式: 根据路由的key,进行通配符匹配,发送到指定的队列(topic) 使用主题交换器即可

 

07 项目中MQ的应用

 

  1. 商品搜索功能 数据库及索引库的一致性 商品上架 --> 发送上架消息 --> es搜索服务 --> 将上架商品存入索引库 商品下架 --> 发送下架消息 --> es搜索服务 --> 将下架商品删除

  2. 商品详情静态化,静态页面的生成延迟队列: 商品上架 --> 发送上架消息 --> 静态化服务 --> 将上架商品生成静态页

  3. 利用rabbitmq的死信队列功能实现订单延时处理

  4. 用户注册成功后向MQ中发送消息, 短信服务监听队列消息实现短信发送

  5. 监控数据的采集可以使用rabbitmq异步采集 日志信息的采集 链路追踪信息的采集

 

二 入门demo

demo地址

01 添加父工程rabbitmq-demo

目录结构

 

 

    <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.2.8.RELEASE</version>
       <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <properties>
       <java.version>1.8</java.version>
   </properties>
   <dependencies>
       <!-- rabbitmq起步依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-amqp</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
           <exclusions>
               <exclusion>
                   <groupId>org.junit.vintage</groupId>
                   <artifactId>junit-vintage-engine</artifactId>
               </exclusion>
           </exclusions>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>

 

02 消息的生产者Producer

创建发送消息工程 maven工程 rabbit_producer,添加依赖

<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
</dependencies>

配置application.yml

spring:
rabbitmq:
  host: 192.168.85.131
  port: 5672
  username: guest
  password: guest

springboot启动类(略)

发送消息

@RestController
@RequestMapping("/direct")
public class DirectController {

   @Autowired
   private RabbitTemplate rabbitTemplate;

   /**
    * 使用直连交换机
    *
    * @param message
    * @return
    */
   @GetMapping("/send")
   public String send(String message) {
       // 需求,根据用户的id发送消息到不同用户的队列
       // 发送消息到RabbitMQ
       //定义路由key
       String routingKey = "a";
       String queueName = "test.a";
       // 定义交换机名称
       String exchangeName = "test.direct.1";

       // 定义交换机队列绑定关系
      //添加rabbitmq管理工具
       RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate);
       //创建交换机
       Exchange exchange = new DirectExchange(exchangeName);
       // 定义交换机
       rabbitAdmin.declareExchange(exchange);

       // 创建队列
       //参数明细
       //1、queue 队列名称
       //2、durable 是否持久化,如果持久化,mq重启后队列还在
       //3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
       //4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
       //5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
       Queue queue = new Queue(queueName);
       //定义队列
       rabbitAdmin.declareQueue(queue);

       //定义交换机和队列的绑定关系
       // 绑定参数 1 队列名称 2 绑定类型 3 交换机名称 4 路由key 5 自定义参数
       Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, exchangeName, routingKey, null);
       rabbitAdmin.declareBinding(binding);

       // 使用RabbitTemplate 发送消息
       rabbitTemplate.convertAndSend(exchangeName, routingKey, message);

       return "发送成功! " + new Date();
  }

 

03 消息的消费者 Consumer

创建消息消费者 maven工程

设置pom 依赖,同生产者

配置yml文件

server:
port: 9008
spring:
rabbitmq:
  host: 192.168.85.131
  listener:
    simple:
      acknowledge-mode: auto  #自动确认   manual #手动确认
       # 重试策略相关配置
      retry:
        enabled: true # 是否开启重试功能
        max-attempts: 3 # 最大重试次数
         # 时间策略乘数因子   0 1 2 4 8
         # 时间策略乘数因子
        multiplier: 1.0
        initial-interval: 1000ms # 第一次调用后的等待时间
        max-interval: 20000ms # 最大等待的时间值

配置启动类

配置listen监听器

/**
* 消息监听者
* @author lyn
*/
@Service
public class MyListener {

   @RabbitListener(queues = "test.a")
   public void handleMessage(String msg) {
       System.out.println("接收到消息: " + msg);
       // 模拟异常
       if (msg.equals("1")) {
           int i = 1 / 0;
      }
       //没有绑定死信队列的消息,出现异常后会丢失
  }

   @RabbitListener(queues = "test.b")
   public void handleMsg(Message message, Channel channel) throws IOException {
       String msg = new String(message.getBody(), Charset.forName("utf-8"));
       System.out.println("接收到消息: " + msg);
       // 模拟异常
       if (msg.equals("1")) {
           int i = 1 / 0;
      }
       MessageProperties messageProperties = message.getMessageProperties();
       // 获取消息标识
       long deliveryTag = messageProperties.getDeliveryTag();
       // 确认消息
       channel.basicAck(deliveryTag, true);
      // 绑定死信队列的消息,会进入死信队列
  }
}

 

三 MQ面试热点

mq常见问题

消息丢失,消息积压,重复消费

01 如何保证消费的可靠性传输?

主要从三个角度来分析:

  • 生产者的发消息的可靠性

  • 消息队列数据的可靠性

  • 消费者消费数据的可靠性

1> 生产者发消息的可靠性

从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction事务机制(了解)

transaction机制就是说,发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降。

confirm确认机制

一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了。如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。处理Ack和Nack的代码如下所示

更改发送者配置

spring:
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  publisher-confirm-type: correlated  # 开启确认机制回调 必须配置这个才会确认回调
  publisher-returns: true # 开启return机制回调

增加rabbitmq配置类

package com.hjxr.producer.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.SerializerMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
* RabbitMq配置类
*
* @author lyn
*/
@Configuration
public class RabbitMqConfig {

   /**
    * RabbitTemplate是RabbitMQ在与SpringAMQP整合的时,Spring提供的即时消息模板
    * RabbitTemplate提供了可靠性消息投递方法、回调监听消息接口ConfirmCallback、返回值确认接口ReturnCallback等等
    */

   @Bean
   public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
       RabbitTemplate template = new RabbitTemplate(connectionFactory);
       // 设置开启 Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
       template.setMandatory(true);
       template.setMessageConverter(new SerializerMessageConverter());
       //发送消息确认机制监听
       template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
           /**
            * @param correlationData 回调数据
            * @param ack true: 确认 false: 未确认
            * @param cause 原因
            */
           @Override
           public void confirm(CorrelationData correlationData, boolean ack, String cause) {
               System.out.println("回调数据==>" + correlationData);
               System.out.println("是否确认==>" + ack);
               System.out.println("原因==>" + cause);
               if (!ack) {
                   System.out.println("确认失败");
                   //可以钉钉告警,也可以记录错误日志
              }
          }
      });
       //消费者在消息没有被路由到合适队列情况下会被return监听,(此处值只会在没投递到队列时候回调)
       template.setReturnCallback(new RabbitTemplate.ReturnCallback() {
           @Override
           public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
               System.out.println("返还消息==>" + message);
               System.out.println("回复编码==>" + replyCode);
               System.out.println("回复消息==>" + replyText);
               System.out.println("交换器信息==>" + exchange);
               System.out.println("路由key==>" + routingKey);

               System.out.println("推送失败");
               //可以钉钉告警,也可以记录错误日志
          }
      });
       return template;
  }

}

 

2> 消息队列数据的可靠性

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

  1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列

  2. 发送消息的时候将deliveryMode=2

这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据

我们可以查看下,之前配置中创建队列的源码,如果你什么都不设置,实际上默认值都是持久化的。

    /**
* 默认队列是持久化的,非自动删除的
* The queue is durable, non-exclusive and non auto-delete.
*
* @param name the name of the queue.
*/
public Queue(String name) {
this(name, true, false, false);
}

默认的消息也是持久化的:

3> 消费者消费数据的可靠性(未测试)

消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rabbitMQ会立即将消息删除,这种情况下,如果消费者出现异常而未能处理消息,就会丢失该消息。至于解决方案,采用手动确认消息即可。

spring:
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  listener:
    simple:
       # acknowledge-mode: manual #手动确认
      acknowledge-mode: auto #自动确认   manual #手动确认

手动确认

@RabbitListener(queues = "green_queue")
public void onMessage(Message message, Channel channel) throws IOException {
   String msg = new String( message.getBody(), Charset.forName("utf-8"));
   System.out.println(msg);
   long deliveryTag = message.getMessageProperties().getDeliveryTag();
   channel.basicAck(deliveryTag,true);
}

不过springboot的项目中,在spring的层面使用了aop实现了消息处理失败的自动重试功能

// 在监听消息的方法中 加入抛异常的逻辑

if(null!=msg && "1".equals(msg)){
    throw new RuntimeException("出错了");
}

因为自动重试功能,所以监听方法出现问题了 会不断的重试,并且在消息队列中该消息属于未消费的状态,而不是未确认的状态。这是spring帮我们提供的消息自动补偿机制, 不过持续的重试对系统带来的压力非常大,我们可以对重试的相关参数进行设置来改善。

 

02 消息重试机制(自动补偿)及幂等性

底层使用Aop拦截,如果程序(消费者)没有抛出异常,自动提交事务 如果Aop使用异常通知拦截获取到异常后,自动实现补偿机制

1> 重试机制的设置
 RabbitMQ自动补偿机制触发:(多用于调用第三方接口)
  1. 当我们的消费者在处理我们的消息的时候,程序抛出异常情况下触发自动补偿(默认无限次数重试)
  2. 应该对我们的消息重试设置间隔重试时间,比如消费失败最多只能重试5次,间隔3秒(防止重复消费,幂等问题)
  3. 如果重试后还未消费默认丢弃,如果配置了死信队列,会发送到死信队列中
spring:
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  listener:
    simple:
      acknowledge-mode: manual #手动确认
# acknowledge-mode: auto #自动确认   manual #手动确认
       # 重试策略相关配置
      retry:
        enabled: true # 是否开启重试功能
        max-attempts: 5 # 最大重试次数
         # 时间策略乘数因子   0 1 2 4 8
         # 时间策略乘数因子
        multiplier: 2.0
        initial-interval: 1000ms # 第一次调用后的等待时间
        max-interval: 20000ms # 最大等待的时间值

注意: 虽然重试机制可以对称对消息消费方法进行重试,不过重试结束后仍未消费的消息还可能造成消息丢失,这里可以通过配置死信队列来存放暂时无法消费的消息,或者过期失效未处理的消息。

2> 死信队列的设置

如果使用 auto 重试次数完毕后 消息会丢失

如果使用 手动确认 重试次数完毕后 消息会存放在unacked影响性能

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

 

 

用于存放过期未消费的或重试后还未消费的消息:

  1. 死信交换机和死信队列和普通的没有区别

  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

  3. 消息成为死信的三种情况:

    1. 队列消息长度到达限制;

    2. 消费者拒绝消费消息,并且不重回队列;

    3. 原队列存在消息过期设置,消息到达超时时间未被消费;

添加一个死信队列

普通队列绑定死信交换机:

给队列设置参数:x-dead-letter-exchange 和x-dead-letter-routing-key

 /**
    * 使用直连交换机并且配置死信队列
    *
    * @param message
    * @return
    */
   @GetMapping("/sendDead")
   public String sendAndDead(String message) {

       //定义工作队列信息
       String routingKey = "b";
       String queueName = "test.b";
       String exchangeName = "test.direct.1";
       //定义死信队列信息
       String deadExchange = "dead.exchange";
       String deadRoutingKey = "dead";
       String deadQueueName = "dead.queue";
       //定义死信交换机和队列的关系
       declareRelation(deadRoutingKey,deadQueueName,deadExchange);
       // 定义交换机队列绑定关系
       declareDeadRelation(routingKey, queueName, exchangeName, deadExchange, deadRoutingKey);

       // 使用RabbitTemplate 发送消息
       rabbitTemplate.convertAndSend(exchangeName, routingKey, message);

       return "发送成功! " + new Date();
  }

   /**
    * 动态交换机和队列的绑定关系
    *
    * @param routingKey   路由key
    * @param queueName   队列名称
    * @param exchangeName 交换机名称
    */
   private void declareRelation(String routingKey, String queueName, String exchangeName) {
       //添加rabbitmq管理工具
       RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate);
       //创建交换机
       Exchange exchange = new DirectExchange(exchangeName);
       // 定义交换机
       rabbitAdmin.declareExchange(exchange);

       // 创建队列
       //参数明细
       //1、queue 队列名称
       //2、durable 是否持久化,如果持久化,mq重启后队列还在
       //3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
       //4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
       //5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
       Queue queue = new Queue(queueName);
       //定义队列
       rabbitAdmin.declareQueue(queue);

       //定义交换机和队列的绑定关系
       // 绑定参数 1 队列名称 2 绑定类型 3 交换机名称 4 路由key 5 自定义参数
       Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, exchangeName, routingKey, null);
       rabbitAdmin.declareBinding(binding);
  }

   /**
    * 动态direct交换机和队列的绑定关系(死信队列)
    *
    * @param routingKey     路由key
    * @param queueName     队列名称
    * @param exchangeName   交换机名称
    * @param deadExchange   死信交换机
    * @param deadRoutingKey 死信路由key
    */
   private void declareDeadRelation(String routingKey, String queueName, String exchangeName, String deadExchange, String deadRoutingKey) {
       //添加rabbitmq管理工具
       RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate);
       //创建交换机
       Exchange exchange = new DirectExchange(exchangeName);
       // 定义交换机
       rabbitAdmin.declareExchange(exchange);

       // 创建队列
       //参数明细
       //1、queue 队列名称
       //2、durable 是否持久化,如果持久化,mq重启后队列还在
       //3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
       //4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
       //5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
       //设置死信队列信息
       Map<String, Object> map = new HashMap<>();
       map.put("x-dead-letter-exchange", deadExchange);
       map.put("x-dead-letter-routing-key", deadRoutingKey);
       Queue queue = new Queue(queueName, true, false, false, map);
       //定义队列
       rabbitAdmin.declareQueue(queue);

       //定义交换机和队列的绑定关系
       // 绑定参数 1 队列名称 2 绑定类型 3 交换机名称 4 路由key 5 自定义参数
       Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, exchangeName, routingKey, null);
       rabbitAdmin.declareBinding(binding);
  }

将确认模式改成自动

spring:
rabbitmq:
  host: 192.168.85.131
  listener:
    simple:
      acknowledge-mode: auto  #自动确认   manual #手动确认

重试后仍然报错,该消息被转发到死信队列 dead_queue中

if(null!=msg && "1".equals(msg)){
    throw new RuntimeException("出错了");
}
3> 幂等性的处理

消息重试可能造成消费方法的多次调用,所以在消费方法中一定要处理消息的重复消费(幂等性)

(1). 使用全局MessageID判断消费方是否消费

在消息生产时,我们可以生成一个全局的消息ID

(2).使用业务ID+逻辑保证唯一

在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重的依据,避免同一条消息被重复消费。

演示伪代码:

发送者测试方法:

@Test
   void sendMsg2(){
       // 自己构建消息
       Message message = MessageBuilder.withBody("我是消息内容".getBytes()).build();
       // 设置消息的全局ID
       message.getMessageProperties().setMessageId(UUID.randomUUID().toString()); // 设置自定义的消息ID
       // 发送消息
       rabbitTemplate.send("my_topic_exchange","xx.yellow.xx",message);
  }

消费者代码:

Map cacheMap = new HashMap(); // 用map模拟redis中的key:value结构
   @RabbitListener(
           queues = "yellow_queue"
  )
   public void yellowMsg(Message message){
       // 商品数据   商品ID
       byte[] body = message.getBody();
       // 获取消息ID (mq生成的)
       String messageId = message.getMessageProperties().getMessageId();
       System.out.println("消息ID messageId==>"+messageId);
       String msg = new String(body);
       // 查看记录
       Object o = cacheMap.get(messageId);
       if(o!=null){
           System.out.println("已处理");
      }else {
           System.out.println("yellow队列收到消息==>" + msg);
      }
       // 处理该消息将处理记录保存
       cacheMap.put(messageId,msg);
       // 模拟出现异常
       throw new RuntimeException();
  }

在第一次消费时,将消费记录存入到redis中,后面的消费通过redis判断是否消费 来防止重复消费。

 

03 扩展MQ场景面试(未测试)

如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?

问题分析 你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办?

所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。

关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。 大量消息在 mq 里积压了几个小时了还没解决W

几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。

一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

正常情况

 

 

紧急扩容

 

 

伪代码

@Test
   public void sendMsg2(){
       // 每秒发送 1秒 10条消息
       // 消费端 1秒消费 2消息
       for(int i=1;;i++){
           try {
               Thread.sleep(100);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
        rabbitTemplate.convertAndSend(exchangeName,"##.green","第"+i+"条消息");
      }
  }

停掉原有服务

//    @RabbitListener(queues = "green_queue")
   public void greenQueue(String msg){
       try {
           Thread.sleep(500);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("成功消费==>"+msg);
  }

紧急开通临时主题交换器及临时队列

创建 : temp_topic 主题交换器

准备若干临时扩容队列

ToRouting keyArguments 
temp_green01 #.green1    
temp_green02 #.green2    
temp_green03 #.green3    

使用临时程序将积压消息 全部转发到临时队列中,并临时扩容对应临时队列数量的消费者

@Component
public class MqListener3 {
   @Autowired
   RabbitTemplate rabbitTemplate;
   volatile static int num = 0;
   @RabbitListener(queues = "green_queue")
   public void greenQueue(String msg){
       rabbitTemplate.convertAndSend("temp_topic","#.green"+(num%3+1),msg);
       num++;
  }
   @RabbitListener(queues = "temp_green01")
   public void green01(String msg){
       try {
           Thread.sleep(200);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("green01临时程序处理消息==>"+msg);
  }
   @RabbitListener(queues = "temp_green02")
   public void green02(String msg){
       try {
           Thread.sleep(200);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("green02临时程序处理消息==>"+msg);
  }
   @RabbitListener(queues = "temp_green03")
   public void green03(String msg){
       try {
           Thread.sleep(200);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("green03临时程序处理消息==>"+msg);
  }
}

mq 中的消息过期失效了 假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。

这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

消息追踪之rabbitmq_tracing插件
作用: 将发消息 消费消息记录到日志中
https://www.cnblogs.com/Harriet/p/10149144.html

04 如何设计一个消息队列?(未测试)

面试官主要考察的是你对消息队列产品的架构有没有一个系统的理解,是一道开放性的面试题。

1. 消息队列的基本消息模型
(首先是设计软件内部的架构,因为我之前用的是Rabbitmq,所以我来设计也会参照AMQP协议模型,其中要有exchange交换器,用于接收消息,还需要有queue 队列结构用于存储消息,并提供绑定和路由的功能,能让exchange和queue关联在一起,这样用户在发送消息时可以根据exchange 和 路由key将消息投递到指定队列中)
broker --> exchange --> binding --> routing --> queue

2. 可靠性消息的设计
(其次,一款成熟的消息队列肯定要保证消息的可靠性,如果在某个环节丢消息就不好了,我可以这样来处理,当客户端发送消息到消息队列时,如果我收到消息并有对应的exchange处理,给客户端返回一个确认状态为true,如果收到消息没有对应的exchange处理给客户端返回一个确认状态为false,来保证 消息能正确的到达消息队列)

发送消息 --> 消息队列:   提供confirm机制   return机制

(消息到达消息队列后,肯定是在内存当中处理更快了,不过在内存处理如果消息队列宕机可能会造成消息丢失,那我们也要提供一个持久化方案,如 让用户可以设置 是否使用持久化,如果开启将相关数据存储到磁盘中)
消息队列: 队列持久化   消息持久化

(最后消费者在消费消息的时候,需要消费者消费后给一个确认反馈,才算消息正式消费,因为消费者有可能刚获取到我们的消息就出错了 那消息也算是丢失了, 像rabbitmq就提供自动确认和手动确认两种模式,为被确认的消息还会存在队列中 状态为unacked 消费者重连时会重新投递消息)

消费者消费: 确认机制

3. 高可用的MQ
(保证这些后,那最后就是MQ本身的高可用方案,通过集群来解决)

05 Rabbitmq的高可用方案(集群)(未测试)

在使用RabbitMQ的过程中,如果只有一个节点,但是一旦单机版宕机,服务不可用,影响比较严重,通过集群就能避免单点故障的问题。 RabbitMQ 集群分为两种 普通集群 和 镜像集群

普通集群

以两个节点(rabbit01、rabbit02)为例来进行说明。
rabbit01和rabbit02两个节点仅有相同的元数据,即队列的结构,但消息实体只存在于其中一个节点
rabbit01(或者rabbit02)中。 当消息进入rabbit01节点的Queue后,consumer从rabbit02节点消费时,RabbitMQ会临时在rabbit01、rabbit02间进行消息传输,把A中的消息实体取出并经过B发送给consumer。
所以consumer应尽量连接每一个节点,从中取消息,即对于同一个逻辑队列,要在多个节点建立物理Queue;否则无论consumer连rabbit01或rabbit02,出口总在rabbit01,会产生瓶颈。
当rabbit01节点故障后,rabbit02节点无法取到rabbit01节点中还未消费的消息实体。如果做了消息持久化,那么得等rabbit01节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。

 

 

镜像集群

在普通集群的基础上,把需要的队列做成镜像队列,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取,也就是说多少节点消息就会备份多少份。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用
由于镜像队列之间消息自动同步,且内部有选举master机制,即使master节点宕机也不会影响整个集群的使用,达到去中心化的目的,从而有效的防止消息丢失及服务不可用等问题。

 

 

 

 

 

posted @   进击的小蔡鸟  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示