Spring Boot学习笔记:RabbitMQ

179 阅读5分钟

本文内容来自SpringBoot相关书籍和项目学习总结

1 RabbitMQ入门

1.1 RabbitMQ简介

什么是消息队列?

消息队列实现系统之间的双向解耦,生产者往消息队列中发送消息,消费者从队列中拿取消息并处理,生产者不用关心是谁来消费,消费者不用关心谁在生产消息,从而达到系统解耦的目的,也大大提高了系统的高可用性和高并发能力。

什么是RabbitMQ?

RabbitMQ基于开源的AMQP协议实现,服务器端用Erlang语言编写,支持多种客户端,如Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP、AJAX等。

主要应用场景:

  • 异步任务
  • 提速
  • 接口解耦
  • 削峰

什么是AMQP协议?

AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是应用层协议的开放标准,是为面向消息的中间件设计。基于此协议的客户端可与消息中间件传递消息,从而不受产品、开发语言等条件限制。消息中间件主要用于组件之间的解耦,消息发送者无须知道消息使用者的存在,反之亦然。

1.2 RabbitMQ组件功能

RabbitMQ中有几个非常重要的组件:服务实体(Broker)、虚拟主机(Virtual Host)、交换机(Exchange)、队列(Queue)和绑定(Binging)等。

RabbitMQ模型.png

1.3 交换机

交换机(Exchange)的功能主要是接收消息并且转发到绑定的队列,交换机不存储消息,只是把消息分发给各自的队列。

当消息发送到交换机(Exchange)时,通过消息携带的RoutingKey与当前交换机所有绑定的BindingKey进行匹配,如果满足匹配规则,则往BindingKey所绑定的消息队列发送消息,这样就解决了向RabbitMQ发送一次消息,可以分发到不同的消息队列,实现消息路由分发的功能。

交换机有Direct、Topic、Headers和Fanout四种消息分发类型:

  1. Direct:其类型的行为是“先匹配,再发送”,即在绑定时设置一个BindingKey,当消息的RoutingKey匹配队列绑定的BindingKey时,才会被交换机发送到绑定的队列中。
  2. Topic:按规则转发消息(最灵活)。支持用*#的模式进行绑定。*表示匹配一个单词,#表示匹配0个或者多个单词。
  3. Headers:设置header attribute参数类型的交换机。根据应用程序消息的特定属性进行匹配,这些消息可能在绑定key中标记为可选或者必选。
  4. Fanout:转发消息到所有绑定队列(广播)。将消息广播到所有绑定到它的队列中,而不考虑队列绑定的BindingKey的值。

2 SpringBoot集成RabbitMQ

2.1 准备工作

1、添加pom依赖

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

2、修改配置文件

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: 123456
    virtual-host: imooc-news-dev

3、定义交换机和队列

@Configuration
public class RabbitMQConfig {
    //定义交换机的名字
    public static final String EXCHANGE_ARTICLE = "exchange_article";

    //定义队列的名字
    public static final String QUEUE_DOWNLOAD_ARTICLE = "queue_download_article";

    /**
     * 创建交换机
     *
     * @return
     */
    @Bean(EXCHANGE_ARTICLE)
    public TopicExchange topicExchange() {
        return new TopicExchange(EXCHANGE_ARTICLE);
    }

    /**
     * 创建队列
     *
     * @return
     */
    @Bean(QUEUE_DOWNLOAD_ARTICLE)
    public Queue queue() {
        return new Queue(QUEUE_DOWNLOAD_ARTICLE);
    }

    /**
     * 队列绑定到交换机,指定路由键(RoutingKey)
     *
     * @return
     */
    @Bean
    public Binding binding() {
        return BindingBuilder
                .bind(queue())
                .to(topicExchange())
                .with("article.#.do");
    }
}

2.2 创建生产者和消费者

1、创建生产者发送消息

rabbitTemplate.convertAndSend(
        RabbitMQConfig.EXCHANGE_ARTICLE,
        "article.hello.do",
        "这是从生产者发送的消息~");

RabbitTemplate提供了convertAndSend方法发送消息。convertAndSend方法有routingKey和message两个参数:

1)routingKey为要匹配的路由键。

2)message为具体的消息内容。

2、创建消费者消费消息

@Component
public class RabbitMQConsumer {
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})
    @RabbitHandler
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);
    }
}

1)@RabbitListener注解提供了@QueueBinding、@Queue、@Exchange等对象,通过这个组合注解配置交换机、绑定路由并且配置监听功能等。

2)@RabbitHandler注解为具体接收的方法。

3 实战案例:异步解耦

1、创建生产者

通过rabbitTemplate向指定的交换机(Exchange)发送消息,通过RoutingKey与交换机绑定的BindingKey进行匹配,对符合匹配规则的消息队列发送消息。

@Autowired
private RabbitTemplate rabbitTemplate;

private void doDownloadArticleHTMLByMQ(String articleId, String articleMongoId) {
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
            "article.download.do",
            articleId + "," + articleMongoId);
}

2、创建消费者

消费者在监听到消息后,根据RoutingKey进行判断,然后对消息进行解析,执行具体的操作。

@Component
public class RabbitMQConsumer {
    @Autowired
    private ArticleHTMLComponent articleHTMLComponent;

    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})
    @RabbitHandler
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);
        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.download.do")) {
            String articleId = payload.split(",")[0];
            String articleMongoId = payload.split(",")[1];

            try {
                articleHTMLComponent.download(articleId, articleMongoId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

4 实战案例:延迟队列

注:需要安装rabbitmq-delayed-message-exchange插件

1、开启延迟消息支持

@Configuration
public class RabbitMQDelayConfig {
    //定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    //定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    /**
     * 创建交换机
     *
     * @return
     */
    @Bean(EXCHANGE_DELAY)
    public TopicExchange topicExchange() {
        TopicExchange exchange = new TopicExchange(EXCHANGE_DELAY);
        exchange.setDelayed(true); //开启支持延迟消息
        return exchange;
    }

    /**
     * 创建队列
     *
     * @return
     */
    @Bean(QUEUE_DELAY)
    public Queue queue() {
        return new Queue(QUEUE_DELAY);
    }

    /**
     * 队列绑定到交换机,指定路由键(RoutingKey)
     *
     * @return
     */
    @Bean
    public Binding delayBinding() {
        return BindingBuilder
                .bind(queue())
                .to(topicExchange())
                .with("delay.#");
    }
}

2、生产者发送延迟消息

MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        // 设置消息的持久
        message.getMessageProperties()
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        // 设置消息延迟的时间,单位ms毫秒
        message.getMessageProperties()
                .setDelay(5000);
        return message;
    }
};

rabbitTemplate.convertAndSend(
        RabbitMQDelayConfig.EXCHANGE_DELAY,
        "delay.do",
        "这是从生产者发送的延迟消息~",
        messagePostProcessor);

3、消费者消费消息

@RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
@RabbitHandler
public void watchQueue(String payload, Message message) {
    System.out.println(payload);
    String routingKey = message.getMessageProperties().getReceivedRoutingKey();
    System.out.println(routingKey);
    if (routingKey.equalsIgnoreCase("delay.do")) {
        System.out.println("消费者接受的延迟消息:" + new Date());
    }
}