消息队列RabbitMQ

189 阅读10分钟

消息队列

核心概念已经应用场景

消息队列中间件 (Message Queue Middleware,简称 MQ) 是指利用高效可靠的消息传递机制进行与平台无关的数据交流,它可以在分布式环境下扩展进程间的数据通信,并基于数据通信来进行分布式系统的集成。它主要适用于以下场景:

项目解耦不同的项目或模块可以使用消息中间件进行数据的传递,从而可以保证模块的相对独立性,实现解耦。
流量削峰:可以将突发的流量 (如秒杀数据) 写入消息中间件,然后由多个消费者进行异步处理。
弹性伸缩:可以通过对消息中间件进行横向扩展来提高系统的处理能力和吞吐量。
发布订阅可以用于任意的发布订阅模式中。
异步处理当我们不需要对数据进行立即处理,或者不关心数据的处理结果时,可以使用中间件进行异步处理。
冗余存储消息中间件可以对数据进行持久化存储,直到你消费完成后再进行删除。

消息队列模式

1.点对点模式

消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。

2872467b19a72be03ef2f65ce41f7a80.png

2.发布/订阅:Topic,可以重复消费

消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。

45dfee8a36636c2c67b4b28043bdd22c.png


支持订阅组的发布订阅模式:发布订阅模式下,当发布者消息量很大时,显然单个订阅者的处理能力是不足的。实际上现实场景中是多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能力线性扩展。可以看成是一个topic下有多个Queue,每个Queue是点对点的方式,Queue之间是发布订阅方式。


RabbitMQ

1.RabbitMQ简介

RabbitMQ 完全实现了 AMQP 协议,并基于相同的模型架构。RabbitMQ 在实现 AMQP 0-9-1 的基础上还进行了额外拓展,并可以通过插件来支持 AMQP 1.0。所以在某种程度上而言, RabbitMQ 就是 AMQP 在 Erlang 语言上的实现。RabbitMQ 基于众多优秀的特性成为了目前最为广泛使用的消息中间件,它的主要特性如下:

支持多种消息传递协议,除了 AMQP 外,还可以通过插件支持所有版本的 STOMP 协议和 MQTT 3.1 协议; 拥有丰富的交换器类型,可以满足绝大部分的使用需求; 支持多种部署方式,易于部署; 支持跨语言开发,如:Java,.NET,PHP,Python,JavaScript,Ruby,Go; 可以通过集群来实现高可用性和高吞吐,还可以通过 Federation 插件来连接跨机房跨区域的不同版本的服务节点; 插拔式的身份验证和授权,支持 TLS 和 LDAP; 支持持续集成,能够使用各种插件进行灵活地扩展; 能够使用多种方式进行监控和管理,如 HTTP API,命令行工具和 UI 界面。


2.RabbitMQ特点

消息路由(支持) RabbitMQ可以通过不同的交换器支持不同种类的消息路由
消息有序(不支持) 当消费消息时,如果消费失败,消息会被放回队列,然后重新消费,这样会导致消息无序
消息时序(非常好) 通过延时队列,可以指定消息的延时时间,过期时间TTL等
容错处理(非常好) 通过交付重试和死信交换器(DX)来处理消息处理故障
伸缩(一般) 伸缩其实没有非常智能,因为即使伸缩了,master queue还是只有一个,负载还是只有这一个master queue:去抗
持久化(不太好) 没有消费的消息,可以支持持久化,这个是为了保证机器宕机时消息可以恢复,但是消费过的消息,就会被马上删除,因为RabbitMQ设计时,就不是为了去存储历史数据的
消息回溯(不支持) 因为消息不支持永久保存,所以自然就不支特回溯
高吞吐(中等) 因为所有的请求的执行,最后都是在master queue,它的这个设计,导致单机性能达不到十万级的标准


3.RabbitMQ环境搭建

官网:下载连接

下载安装elang环境

elang安装完成后需要再环境变量中path变量添加你的安装路径

9b744933c2e9f076371d5ec3a07a9075.png

下载rabbitmq


9cfb2853eaafdab362468c015c0f1d0c.png


安装完成后进入rabbitmq的安装目录下面的sbin目录执行如下命令


rabbitmq-plugins enable rabbitmq_management

618a7e2705373a526fdb14039993fd37.png


618eb0ae4b1783a4c462c3f8c346f57a.png

c13be3d786cfcffadd07c72736fc0417.png



打开任务管理器找到rabbitmq的服务启动

36c0c54f04e82e557bd3129c11adc909.png

访问http://localhost:15672/#/

d0c62077e02f4d657ff68b370a89a717.png

登录账号密码: guest

springboot整合RabbitMQ

1.在pom.xml中引入Spring AMQP依赖:

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

2.在application.properties中配置RabbitMQ相关信息

spring.rabbitmq.host=localhost
spring.rabbitmq.port=15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

3.配置ConnectionFactory并设置连接池

@Bean
public CachingConnectionFactory connectionFactory(RabbitProperties rabbitProperties) {
    CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(rabbitProperties.determineHost());
    cachingConnectionFactory.setPort(rabbitProperties.getPort());
    cachingConnectionFactory.setUsername(rabbitProperties.getUsername());
    cachingConnectionFactory.setPassword(rabbitProperties.getPassword());
    // 设置其他属性,如连接池大小
    cachingConnectionFactory.setChannelCacheSize(25);
    // 设置连接超时(单位:毫秒)
    cachingConnectionFactory.setConnectionTimeout(60000);
    // 设置确认模式(publisher confirms 和 transactional channels 支持)
    cachingConnectionFactory.setPublisherConfirms(true);
    cachingConnectionFactory.setPublisherReturns(true);
    return cachingConnectionFactory;
}


4.RabbitMQ提供了四种主要类型的Exchange

1.Direct Exchange

场景:适用于一对一的直接路由。当消息携带的routing key与队列绑定时指定的routing key完全匹配时,消息会被路由到对应的队列。 Spring Boot示例:

@Configuration
 public class RabbitMQConfig {

     @Bean
     public Declarables directExchangeConfig() {
         DirectExchange directExchange = new DirectExchange("directExchange");
         Queue queue = new Queue("directQueue");
         Binding binding = BindingBuilder.bind(queue).to(directExchange).with("direct.routing.key");
         
         return new Declarables(directExchange, queue, binding);
     }

     @Service
     public class DirectProducer {

         @Autowired
         private RabbitTemplate rabbitTemplate;

         public void sendMessage(String message) {
             rabbitTemplate.convertAndSend("directExchange", "direct.routing.key", message);
         }
     }
      
     @Service
     @RabbitListener(queues = "directQueue")
     public class DirectConsumer {

         @RabbitHandler
         public void process(String message) {
             System.out.println("Received message on direct exchange: " + message);
         }
     }
 }

在此示例中,我们首先声明了一个Direct Exchange和一个Queue,并将它们通过特定的routing key绑定起来。然后,生产者发送消息时指定Exchange和routing key,消费者则监听对应Queue上的消息。


2.Fanout Exchange

场景:广播模式,所有与该Exchange绑定的队列都会收到消息,无需routing key。 Spring Boot示例:

@Configuration
 public class RabbitMQConfig {

     @Bean
     public Declarables fanoutExchangeConfig() {
         FanoutExchange fanoutExchange = new FanoutExchange("fanoutExchange");
         Queue queue1 = new Queue("fanoutQueue1");
         Queue queue2 = new Queue("fanoutQueue2");
         Binding binding1 = BindingBuilder.bind(queue1).to(fanoutExchange);
         Binding binding2 = BindingBuilder.bind(queue2).to(fanoutExchange);
         
         return new Declarables(fanoutExchange, queue1, queue2, binding1, binding2);
     }

     @Service
     public class FanoutProducer {

         @Autowired
         private RabbitTemplate rabbitTemplate;

         public void sendMessage(String message) {
             rabbitTemplate.convertAndSend("fanoutExchange", "", message);
         }
     }

     @RabbitListener(queues = "fanoutQueue1")
     public class FanoutConsumer1 {

         @RabbitHandler
         public void process(String message) {
             System.out.println("Received message on fanout queue 1: " + message);
         }
     }
     @Service
     @RabbitListener(queues = "fanoutQueue2")
     public class FanoutConsumer2 {

         @RabbitHandler
         public void process(String message) {
             System.out.println("Received message on fanout queue 2: " + message);
         }
     }
 }

在Fanout Exchange的示例中,无论何时发送消息到这个Exchange,它都会被广播到所有绑定的队列


3.Topic Exchange 场景:多对多模式,通过模糊匹配routing key规则。routing key是一个包含.分隔符的主题,队列通过订阅带有通配符(*代表一个单词,#代表零个或多个单词)的binding key来接收消息。 Spring Boot示例

https://gitee.com/ma-jinwen2/learning-notes/tree/master/springboot%E7%B3%BB%E5%88%97/springboot-rabbitmq@Configuration
public class RabbitMQConfig {

 @Bean
  public Declarables topicExchangeConfig() {
      TopicExchange topicExchange = new TopicExchange("topicExchange");
      Queue queue1 = new Queue("topicQueue1");
      Queue queue2 = new Queue("topicQueue2");
      Binding binding1 = BindingBuilder.bind(queue1).to(topicExchange).with("*.news.*");
      Binding binding2 = BindingBuilder.bind(queue2).to(topicExchange).with("*.*.news");
      Binding binding3 = BindingBuilder.bind(queue2).to(topicExchange).with("news.#");
      return new Declarables(topicExchange, queue1, queue2, binding1, binding2, binding3);
  }

 @Service
public class TopicProducer {

  @Autowired
  RabbitTemplate rabbitTemplate;

  public void sendMessage(String routingKey, String message) {
      rabbitTemplate.convertAndSend("topicExchange", routingKey, message);
  }

}
@Service
public class TopicConsumer {
  @RabbitListener(queues = "topicQueue1")
  @RabbitHandler
  public void process1(String message){
      System.out.println("Received message on topic queue 1: " + message);
  }


  @RabbitListener(queues = "topicQueue2")
  @RabbitHandler
  public void process2(String message){
      System.out.println("Received message on topic queue 2: " + message);
  }
}
}
     

在Topic Exchange的例子中,不同的消费者可以根据自己的binding key订阅特定主题的消息。

// routingKey为a.news.news 能匹配到队列topicQueue1和topicQueue2

Received message on topic queue 1: 你真帅
Received message on topic queue 2: 你真帅

// routingKey为a.news.b 只能匹配到队列topicQueue1

Received message on topic queue 1: 你真帅

// routingKey为news.a或者news.a.b 只能匹配到队列topicQueue2

Received message on topic queue 2: 你真帅

// 注意:如果在发送消息的时候没有匹配到符合条件的binding key,那么这条消息将会被废弃。如routingKey为a.news时匹配不到队列

4.Headers Exchange

场景:根据消息头部(headers)而不是routing key进行路由。headers是一组键值对,队列通过一组匹配规则与Exchange关联,只有消息头满足这些规则时,消息才会被路由到相应的队列。

RabbitMQ的Headers Exchange是一种特殊的交换机类型,它不基于路由键(routing key)进行消息路由,而是基于消息头部(headers)中的键值对来进行匹配。这种类型的交换机会根据消息发送时附带的headers与队列绑定时设定的匹配条件来进行消息路由。

Spring Boot示例:

@Configuration
public class RabbitMQConfig {
    @Bean
    public Declarables headersExchangeConfig() {
        HeadersExchange headersExchange = new HeadersExchange("headersExchange");
        // 声明两个Queue
        Queue queue1 = new Queue("headersQueue1");
        Queue queue2 = new Queue("headersQueue2");
        // 队列queue1的绑定条件,只要消息头包含键'header1'并且值等于'value1'就会路由过来
        Map<String, Object> bindings1 = new HashMap<>();
        bindings1.put("header1", "value1");
        Binding binding1 = BindingBuilder.bind(queue1).to(headersExchange).whereAll(bindings1).match();


        // 队列queue2的绑定条件,消息头包含键'header2'并且值等于'value2'就会路由过来
        Map<String, Object> bindings2 = new HashMap<>();
        bindings2.put("header2", "value2");
        Binding binding2 = BindingBuilder.bind(queue2).to(headersExchange).whereAll(bindings2).match();

        return new Declarables(headersExchange, queue1, queue2, binding1, binding2);
    }


    @Service
    public class HeadersConsumer {
        @RabbitHandler
        @RabbitListener(queues = "headersQueue1")
        public void process1(String message) {
            System.out.println("Received message on headers queue1: " + message);
        }

        @RabbitHandler
        @RabbitListener(queues = "headersQueue2")
        public void process2(String message) {
            System.out.println("Received message on headers queue2: " + message);
        }
    }

    @Service
    @RabbitListener(queues = "headersQueue1")
    public class HeadersConsumer {

        @RabbitHandler
        public void process(String message) {
            System.out.println("Received message on headers queue: " + message);
        }
    }
//在这个例子中,当发送消息时,我们需要创建一个包含所需键值对的消息头,并将其与消息一起发送。如果消息头中的键值对满足某个Queue的绑定条件,则该消息会被路由到该Queue上。
    @Service
    public class HeadersProducer {

        @Autowired
        RabbitTemplate rabbitTemplate;

        public void sendMessage(Map<String, Object> headers, String message) {
            final MessageBuilder messageBuilder = MessageBuilder.withBody(message.getBytes());
            for (String s : headers.keySet()) {
                messageBuilder.setHeader(s, headers.get(s));
            }
            final Message msg = messageBuilder.build();
            rabbitTemplate.convertAndSend("headersExchange", "", msg);
        }
    }
}

 

在Headers Exchange的示例中,生产者发送消息时需要设置headers,消费者则根据预设的headers匹配规则接收消息。 以上每个示例中都包含了声明Exchange、Queue和Binding的配置,以及生产者和消费者的实现。在Spring Boot项目中,可以方便地使用@RabbitListener注解来定义消息消费者。

在消费者端,我们可以定义针对不同Queue的消费者,当消息被正确路由到Queue时,相应的消费者会接收到消息并进行处理。 注意,Headers Exchange的匹配规则可以相当复杂,包括但不限于精确匹配、范围匹配、正则表达式匹配等,但它并不适合所有场景,因为它比其他Exchange类型(如Direct、Fanout和Topic)效率更低,且不适合做大规模的消息路由。

代码仓库

gitee.com/ma-jinwen2/…