RabbitMQ入门基本案例

165 阅读11分钟

官网 :www.rabbitmq.com/

介绍

RabbitMQ是部署最广泛的开源消息代理。

RabbitMQ有成千上万的用户,是最受欢迎的开源消息代理之一。从T-Mobile 到Runtastic,RabbitMQ在全球范围内的小型初创企业和大型企业中都得到使用。

RabbitMQ轻巧,易于在内部和云中部署。它支持多种消息传递协议。RabbitMQ可以部署在分布式和联合配置中,以满足大规模,高可用性的要求。

 

使用

pom.xml依赖

<!--RabbitMQ-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
   <version>2.0.4.RELEASE</version>
</dependency>

application.yml

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

controller

import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author :tangcv
 * @date :Created in 2020/3/4 14:59
 * @Time: 14:59
 * @description:消息队列发送消息
 * @modified By:
 * @version: 1.0$
 */
@Api(tags = "消息队列")
@RestController
@RequestMapping("/api/RabbitMQ")
public class RabbitMQController {

    @Autowired private DirectSender directSender;

    @Autowired private TopicSender topicSender;

    @Autowired private FanoutSender fanoutSender;


    /**
     * 订阅模式,发布与订阅,完全匹配
     * @return
     */
    @GetMapping("/sendDirectQueue")
    public Object sendDirectQueue() {
        directSender.sendDirectQueue();
        return "ok";
    }

    /**
     * 主题模式,规则匹配
     * @return
     */
    @GetMapping("/sendTopic")
    public Object sendTopic() {
        topicSender.sendTopic();
        return "ok";
    }

    /**
     * 广播模式
     * 广播的是发一次,不管你收没收到就没了
     * @return
     */
    @GetMapping("/sendFanout")
    public Object sendFanout() {
        fanoutSender.sendFanout();
        return "ok";
    }
}

 

订阅模式 

发送


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 发送
 * @author :tangcv
 * @date :Created in 2020/3/4 13:50
 * @Time: 13:50
 * @description:
 * @modified By:
 * @version: $
 */
@Component
@Slf4j
public class DirectSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendDirectQueue() {
        log.info("【sendDirectQueue已发送消息】");
        // 第一个参数是指要发送到哪个队列里面, 第二个参数是指要发送的内容
        this.amqpTemplate.convertAndSend(DirectRabbitMQConfig.QUEUE, "DirectQueue消息");
    }

}

配置



import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 消息中的路由键(routing key)如果和 Binding 中的 binding key 一致
 * 交换器就将消息发到对应的队列中。路由键与队列名完全匹配
 */
@Configuration
public class DirectRabbitMQConfig {

    static final String QUEUE = "direct_queue";
    /**
     * Direct模式
     * @return
     */
    @Bean
    public Queue directQueue() {
        // 第一个参数是队列名字, 第二个参数是指是否持久化
        return new Queue(QUEUE, true);
    }

}

接收


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 接收
 * @author :tangcv
 * @date :Created in 2020/3/4 13:49
 * @Time: 13:49
 * @description:
 * @modified By:
 * @version: $
 */
@Component
@Slf4j
public class DirectReceiver {

    // queues是指要监听的队列的名字
    @RabbitListener(queues = DirectRabbitMQConfig.QUEUE)
    public void receiverDirectQueue(String a) {
        log.info("【receiverDirectQueue监听到消息】" + a);
    }

}

主题模式

发送


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author :tangcv
 * @date :Created in 2020/3/4 14:22
 * @Time: 14:22
 * @description:
 * @modified By:
 * @version: $
 */
@Component
@Slf4j
public class TopicSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendTopic() {
        log.info("【TopicSender已发送消息】");
        // 第一个参数:TopicExchange名字
        // 第二个参数:Route-Key
        // 第三个参数:要发送的内容
        this.amqpTemplate.convertAndSend(TopicRabbitMQConfig.TOPIC_EXCHANGE, "lzc.message", "a" );
        this.amqpTemplate.convertAndSend(TopicRabbitMQConfig.TOPIC_EXCHANGE, "lzc.lzc", "b");
    }

}

配置


import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。
 * 它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
 * 它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词
 * @author :admin
 * @date :Created in 2020/3/4 14:20
 * @Time: 14:20
 * @description:
 * @modified By:
 * @version: $
 */
@Configuration
public class TopicRabbitMQConfig {

    public static final String TOPIC_QUEUE1 = "topic.queue1";
    public static final String TOPIC_QUEUE2 = "topic.queue2";
    public static final String TOPIC_EXCHANGE = "topic.exchange";

    /**
     * Topic模式
     * @return
     */
    @Bean
    public Queue topicQueue1() {
        return new Queue(TOPIC_QUEUE1);
    }
    @Bean
    public Queue topicQueue2() {
        return new Queue(TOPIC_QUEUE2);
    }
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE);
    }
    @Bean
    public Binding topicBinding1() {
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("lzc.message");
    }
    @Bean
    public Binding topicBinding2() {
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("lzc.#");
    }

}

接收者


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 接收者
 * @author :tangcv
 * @date :Created in 2020/3/4 14:21
 * @Time: 14:21
 * @description:
 * @modified By:
 * @version: $
 */
@Component
@Slf4j
public class TopicReceiver {
    // queues是指要监听的队列的名字
    @RabbitListener(queues = TopicRabbitMQConfig.TOPIC_QUEUE1)
    public void receiveTopic1(String a) {
        log.info("【TopicReceiver1监听到消息】" + a);
    }
    @RabbitListener(queues = TopicRabbitMQConfig.TOPIC_QUEUE2)
    public void receiveTopic2(String a) {
        log.info("【TopicReceiver2监听到消息】" + a);
    }

}

广播模式

发送


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


/**
 * 广播模式
 * @author :admin
 * @date :Created in 2020/3/4 14:56
 * @Time: 14:56
 * @description:
 * @modified By:
 * @version: $
 */
@Component
@Slf4j
public class FanoutSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendFanout() {
        log.info("【sendFanout已发送消息】");
        // 注意, 这里的第2个参数为空。
        // 因为fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,
        // 每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上
        this.amqpTemplate.convertAndSend(FanoutRabbitMQConfig.FANOUT_EXCHANGE, "", "a" );
    }

}

配置



import org.springframework.amqp.core.*;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author :tangcv
 * @date :Created in 2020/3/4 14:50
 * @Time: 14:50
 * @description:
 * @modified By:
 * @version: $
 */

@Configuration
public class FanoutRabbitMQConfig {

    public static final String FANOUT_QUEUE1 = "fanout.queue1";
    public static final String FANOUT_QUEUE2 = "fanout.queue2";

    public static final String FANOUT_EXCHANGE = "fanout.exchange";


    /**
     * 这里为了方便就没有创建新的队列了,直接使用topic时所创建的队列。
     * @return
     */
    @Bean
    public Queue fanoutQueue1() {
        return new Queue(FANOUT_QUEUE1);
    }
    @Bean
    public Queue fanoutQueue2() {
        return new Queue(FANOUT_QUEUE2);
    }

    /**
     * Fanout模式
     * Fanout 就是我们熟悉的广播模式或者订阅模式,给Fanout交换机发送消息,绑定了这个交换机的所有队列都收到这个消息。
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE);
    }
    @Bean
    public Binding fanoutBinding1() {
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutBinding2() {
        return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
    }

}

接收


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 广播模式
 * 接收者
 * @author :tangcv
 * @date :Created in 2020/3/4 14:54
 * @Time: 14:54
 * @description:
 * @modified By:
 * @version: $
 */

@Component
@Slf4j
public class FanoutReceiver {

    // queues是指要监听的队列的名字
    @RabbitListener(queues = FanoutRabbitMQConfig.FANOUT_QUEUE1)
    public void receiveTopic1(String FanoutReceiver) {
        log.info("【receiveTopic1监听到消息】" + FanoutReceiver);
    }

    @RabbitListener(queues = FanoutRabbitMQConfig.FANOUT_QUEUE2)
    public void receiveTopic2(String FanoutReceiver) {
        log.info("【receiveTopic2监听到消息】" + FanoutReceiver);
    }

}

RabbitMQ 优缺点

优点

异步、解耦、消峰填谷

缺点

主要在于系统的可用性、复杂性、一致性问题,引入消息队列后,需要考虑MQ的可用性,万一MQ崩溃了岂不是要爆炸?而且复杂性明显提高了,需要考虑一些消息队列的常见问题和解决方案,还有就是一致性问题,一条消息由多个消费者消费,万一有一个消费者消费失败了,就会导致数据不一致。

 

使用场景

服务解耦

假设有这样一个场景, 服务A产生数据, 而服务B,C,D需要这些数据, 那么我们可以在A服务中直接调用B,C,D服务,把数据传递到下游服务即可

但是,随着我们的应用规模不断扩大,会有更多的服务需要A的数据,如果有几十甚至几百个下游服务,而且会不断变更,再加上还要考虑下游服务出错的情况,那么A服务中调用代码的维护会极为困难

这是由于服务之间耦合度过于紧密
在这里插入图片描述

再来考虑用RabbitMQ解耦的情况

A服务只需要向消息服务器发送消息,而不用考虑谁需要这些数据;下游服务如果需要数据,自行从消息服务器订阅消息,不再需要数据时则取消订阅即可

解耦

流量削峰

假设我们有一个应用,平时访问量是每秒300请求,我们用一台服务器即可轻松应对

低流量

而在高峰期,访问量瞬间翻了十倍,达到每秒3000次请求,那么单台服务器肯定无法应对,这时我们可以考虑增加到10台服务器,来分散访问压力

但如果这种瞬时高峰的情况每天只出现一次,每次只有半小时,那么我们10台服务器在多数时间都只分担每秒几十次请求,这样就有点浪费资源了

流量峰值

这种情况,我们就可以使用RabbitMQ来进行流量削峰,高峰情况下,瞬间出现的大量请求数据,先发送到消息队列服务器,排队等待被处理,而我们的应用,可以慢慢的从消息队列接收请求数据进行处理,这样把数据处理时间拉长,以减轻瞬时压力

这是消息队列服务器非常典型的应用场景

流量销峰

异步调用

考虑定外卖支付成功的情况

支付后要发送支付成功的通知,再寻找外卖小哥来进行配送,而寻找外卖小哥的过程非常耗时,尤其是高峰期,可能要等待几十秒甚至更长

这样就造成整条调用链路响应非常缓慢

阻塞

而如果我们引入RabbitMQ消息队列,订单数据可以发送到消息队列服务器,那么调用链路也就可以到此结束,订单系统则可以立即得到响应,整条链路的响应时间只有200毫秒左右

寻找外卖小哥的应用可以以异步的方式从消息队列接收订单消息,再执行耗时的寻找操作

在这里插入图片描述

Rabbitmq 基本概念

RabbitMQ是一种消息中间件,用于处理来自客户端的异步消息。服务端将要发送的消息放入到队列池中。接收端可以根据RabbitMQ配置的转发机制接收服务端发来的消息。RabbitMQ依据指定的转发规则进行消息的转发、缓冲和持久化操作,主要用在多服务器间或单服务器的子系统间进行通信,是分布式系统标准的配置。

### Rabbitmq

Exchange(交换机)

接受生产者发送的消息,并根据Binding规则将消息路由给服务器中的队列。ExchangeType决定了Exchange路由消息的行为。在RabbitMQ中,ExchangeType常用的有direct、Fanout和Topic三种。

exchange

Message Queue(信息队列)

消息队列。我们发送给RabbitMQ的消息最后都会到达各种queue,并且存储在其中(如果路由找不到相应的queue则数据会丢失),等待消费者来取。

Binding Key(绑定key队列)

它表示的是Exchange与Message Queue是通过binding key进行联系的,这个关系是固定。

Routing Key(路由key队列)

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则。这个routing key需要与Exchange Type及binding key联合使用才能生,我们的生产者只需要通过指定routing key来决定消息流向哪里。

Rabbitmq六种工作模式

简单模式

简单

RabbitMQ是一个消息中间件,你可以想象它是一个邮局。当你把信件放到邮箱里时,能够确信邮递员会正确地递送你的信件。RabbitMq就是一个邮箱、一个邮局和一个邮递员。

  • 发送消息的程序是生产者
  • 队列就代表一个邮箱。虽然消息会流经RbbitMQ和你的应用程序,但消息只能被存储在队列里。队列存储空间只受服务器内存和磁盘限制,它本质上是一个大的消息缓冲区。多个生产者可以向同一个队列发送消息,多个消费者也可以从同一个队列接收消息.
  • 消费者等待从队列接收消息

在这里插入图片描述

工作模式

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

发布订阅模式

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

路由模式

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

主题模式

在这里插入图片描述

RPC模式

在这里插入图片描述

 

RabbitMQ几个常用面试题

1、什么是RabbitMQ?为什么使用RabbitMQ?

答:RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的,消息中间件;

可以用它来:解耦、异步、削峰。

 

2、RabbitMQ有什么优缺点?

答:优点:解耦、异步、削峰;

缺点:降低了系统的稳定性:本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;

增加了系统的复杂性:加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。

 

3、如何保证RabbitMQ的高可用?

答:没有哪个项目会只用一搭建一台RabbitMQ服务器提供服务,风险太大;

 

4、如何保证RabbitMQ不被重复消费?

答:先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;

但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。

针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;

比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;

 

5、如何保证RabbitMQ消息的可靠传输?

答:消息不可靠的情况可能是消息丢失,劫持等原因;

丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;

 

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

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

confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后;

rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

 

消息队列丢数据:消息持久化。

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

这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。

这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢?

这里顺便说一下吧,其实也很容易,就下面两步

  1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列
  2. 发送消息的时候将deliveryMode=2

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

 

消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!

消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;

如果这时处理消息失败,就会丢失该消息;

解决方案:处理消息成功后,手动回复确认消息。

 

6、如何保证RabbitMQ消息的顺序性?

答:单线程消费保证消息的顺序性;对消息进行编号,消费者处理消息是根据编号处理消息;