图文并茂,一文带你掌握RabbitMQ

1.初识MQ

  • 微服务间通讯有同步和异步两种方式:
    • 同步通讯:就像打电话,需要实时响应。
    • 异步通讯:就像发邮件,不需要马上回复。
      两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。

1.1、同步通讯

Feign调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:
在这里插入图片描述
总而言之:

  • 同步调用的优点:时效性较强,可以立即得到结果
  • 同步调用的问题:
    • 耦合度高、性能和吞吐能力下降、有额外的资源消耗、有级联失败问题

1.2、异步通讯

  • 异步调用则可以避免上述问题:
    在这里插入图片描述
    我们以如上购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。

  • 在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id,订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。

  • 为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。异步调用:如上完成支付服务并发布支付成功的事件后,不需要去等待订单服务、仓储服务、短信等诸多服务的响应
    在这里插入图片描述

  • Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。

  • Broker的好处:

    • 吞吐量提升:无需等待订阅者处理完成,响应更快速
    • 故障隔离:服务没有直接调用,不存在级联失败问题
    • 调用间没有阻塞,不会造成无效的资源占用
    • 耦合度极低,每个服务都可以灵活插拔,可替换
    • 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件-
  • 缺点:

    • 架构复杂了,业务没有明显的流程线,不好管理
    • 需要依赖于Broker的可靠、安全、性能
  • 而MQ(消息队列)就可以看作是Broker的具体实现,

1.3、MQ技术对比

  • MQ,即消息队列(MessageQueue),字面来看就是存放消息的队列,也就是事件驱动架构中的Broker
  • 比较常见的MQ实现:
    • ActiveMQ、RabbitMQ、RocketMQ、Kafka
      在这里插入图片描述
  • 追求可用性:Kafka、 RocketMQ 、RabbitMQ
  • 追求消息可靠性:RabbitMQ、RocketMQ
  • 追求吞吐能力:RocketMQ、Kafka
  • 追求消息低延迟:RabbitMQ、Kafka
    Kafka虽然单机吞吐量高,但却是以牺牲消息的可靠性和延迟 换来的。

2、RabbitMQ快速入门

2.1、RabbitMQ安装

  • 在Centos7虚拟机中使用Docker来安装RabbitMQ。
  • 开启docker服务后,在线镜像拉取
docker pull rabbitmq:3-management
  • 运行拉去的RabbitMQ镜像:
docker run \
 -e RABBITMQ_DEFAULT_USER=root \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management
  • 在Windows端,发送请求:http://192.168.40.132:15672/
    就可来到RabbitMQ的登录页面,Username和Password就是docker run命令后面配置的账户名和密码,登录后就可来到管理界面
    在这里插入图片描述
    在这里插入图片描述

2.2、消息模型

2.3、基于SpringAMQP实现的入门案例

在这里插入图片描述

1)Publisher发送消息
  • 在父工程中引入依赖(Publisher和Consumer都需要引入amqp的依赖)
 <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
  • 配置MQ地址,在publisher服务的application.yml中添加配置:
spring:
  rabbitmq:
    host: 192.168.40.132 # 主机IP
    port: 5672 # rabbitmq的默认端口
    virtual-host: /
    username: root
    password: 123456
  • 在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        String queueName = "simple.queue";
        String message = "hello,RabbitMQ and Spring AMQP!";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

运行如上的测试类,在RabbitMQ的可视化管理界面中按照如下步骤,就可以看到Publisher发送的消息:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2)Consumer接收消息
  • 同前面接收消息的操作,配置MQ地址,在consumer服务的application.yml中添加配置:
spring:
  rabbitmq:
    host: 192.168.40.132 # 主机IP
    port: 5672 # rabbitmq的默认端口
    virtual-host: /
    username: root
    password: 123456
  • 在consumer服务的cn.itcast.mq.listener包中新建一个类SpringRabbitListener,代码如下:
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg){
        System.out.println("消费者接收到消息:【" + msg + "】");
    }
}
  • 启动Consumer模块的服务,即可接收到Publisher存储在"simple.queue"中的消息
消费者接收到消息:【hello,RabbitMQ and Spring AMQP!】

在这里插入图片描述

3、SpringAMQP高阶使用

前面的入门案例属于简单队列模型,当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

  • 此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了

3.1、WorkQueue

  • Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。简单队列和工作队列都属于点对点模式。
    在这里插入图片描述
  • 消息发送
    在Publisher对应的测试类中,添加testWorkQueue方法,使其向队列中不停发送消息,模拟消息堆积
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testWorkQueue() throws InterruptedException {
        String queueName = "simple.queue";
        String message = "hello,message id = ";
        for (int i = 1; i <= 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message + i);
            Thread.sleep(20);   
        }
    }
  • 消息接收,修改Consumer模块中的SpringRabbitListener类,添加如下方法,实现多个消费者绑定单个队列
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
    System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
    System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(200);
}
  • 测试
    启动ConsumerApplication后,再执行Publisher服务中刚刚编写的发送测试方法testWorkQueue,可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息,也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的,由于预取消息的机制,两个消费者会先从队列中轮流拿取消息,但两个消费者处理消息的能力不同,导致消费者1处理完了拿取的所有消息后,消费者2还要好久才能处理完消息
    在这里插入图片描述
    在这里插入图片描述
  • 解决:修改Consumer服务的application.yml文件,添加配置:
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

在这里插入图片描述

  • 由如下的测试结果可见,两个消费者都只是按照自己处理消息的能力去拿取消息,没有造成处理消息能力强的消费者空闲,能力弱的消费者忙碌的状态在这里插入图片描述
  • WorkQueue模型总结:
    • 多个消费者绑定到一个队列,使用BasicQueue或WorkQueue,同一条消息都只会被一个消费者处理
    • 可以在xml配置文件中 设置prefetch来控制消费者预取的消息数量

3.2、发布/订阅

发布/订阅模型图解如下:
在这里插入图片描述
可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
  • Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
  • Consumer:消费者,与以前一样,订阅队列,没有变化
  • Queue:消息队列也与以前一样,接收消息、缓存消息。

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

1)Fanout交换机

Fanout,译为扇出,在MQ中中可以理解为广播
在这里插入图片描述

  • 在广播模式下,消息发送流程是这样的:

    • 1) 可以有多个队列
    • 2) 每个队列都要绑定到Exchange(交换机)
    • 3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
    • 4) 交换机把消息发送给绑定过的所有队列
    • 5) 订阅队列的消费者都能拿到消息
  • 使用案例:
    在这里插入图片描述

  • 声明交换机和队列
    Spring提供了一个接口Exchange,来表示所有不同类型的交换机:
    在这里插入图片描述

在Consumer模块中,声明队列、交换机,并将队列和交换机之间建立绑定关系

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

@Configuration
public class RabbitConfig{

    // 声明名为 fanout.exchange 的交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout.exchange");
    }

    // 声明名为 fanout.queue1的队列
    @Bean
    public Queue queue1(){
        return new Queue("fanout.queue1");
    }
    // 声明 队列到交换机的绑定关系
    @Bean
    public Binding fanoutBinding1(Queue queue1,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue1).to(fanoutExchange);
    }

    @Bean
    public Queue queue2(){
        return new Queue("fanout.queue2");
    }

    // 注入spring容器的bean名称就是对应的方法名
    @Bean
    public Binding fanoutBinding2(Queue queue2,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue2).to(fanoutExchange);
    }
}
  • 在Consumer中, 分别监听fanout.queue1和fanout.queue2两个队列,使其队列有消息时,消费者可以从中拿取消息后处理
 @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg) {
        System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg) {
        System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
    }
  • 在Publisher模块中,添加发送消息的方法:
@Test
public void testFanoutExchange() {
    // 队列名称
    String exchangeName = "fanout.exchange";
    // 消息
    String message = "hello, every fanout queue!";
    rabbitTemplate.convertAndSend(exchangeName, "", message);
}
  • 启动Consumer服务后,再运行发送消息的testFanoutExchange方法,在Consumer端即可接收到Publisher发送的消息(通过Fanout交换机,来到队列,再传到消费者)
  • 输出结果:
消费者2接收到Fanout消息:【hello, every fanout queue!】
消费者1接收到Fanout消息:【hello, every fanout queue!】
  • 在RabbitMQ管理界面中,按照如下截图即可看到交换机和队列的绑定关系
    在这里插入图片描述

    Fanout交换机总结:
  • 交换机的作用:
    • 接收publisher发送的消息
    • 将消息按照规则路由到与之绑定的队列
    • 不能缓存消息,路由失败,消息丢失
    • FanoutExchange的会将消息路由到每个绑定的队列
  • 声明队列、交换机、绑定关系的Bean分别是什么
    • Queue、FanoutExchange、Binding
2)Direct交换机
  • 在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

  • 在Direct模型下:

    • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
    • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey
    • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
  • 假设存在如下案例需求如下:
    1)利用@RabbitListener声明Exchange、Queue、RoutingKey
    2)在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
    3)在publisher中编写测试方法,向itcast. direct发送消息

  • 声明队列、交换机、路由并接收消息
    基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明(在注解中声明队列和交换机,队列到交换机的路由)。在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机,代码如下:

  @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "xyl.direct",type = ExchangeTypes.DIRECT),
            key = {"China","Singapore","America"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者接1 ===》 收到direct.queue1的消息:【" + msg + "】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "xyl.direct",type = ExchangeTypes.DIRECT),
            key = {"China","Canada"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者2  ---> 接收到direct.queue2的消息:【" + msg + "】");
    }
  • 消息发送
    在publisher服务的SpringAmqpTest类中添加如下的测试方法:
    @Test
    public void testSendDirectExchange() {
        // 交换机名称
        String exchangeName = "xyl.direct";
        // 消息
        String message1 = "中华文化,源远流长!";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "China", message1);
        String message2 = "亚洲四小龙之首";
        rabbitTemplate.convertAndSend(exchangeName, "Singapore", message2);
    }
  • 测试,依次启动Consumer模块的服务和testSendDirectExchange测试方法,因为message1发给经由"China"路由发送给两个消费者(两个消费者都有“China”的路由),而message2经由“Singapore”只发给了消费者2(只有消费者2有“Singapore”的路由),结果如下:
    在这里插入图片描述
    总结:
  • 描述下Direct交换机与Fanout交换机的差异?
    • Fanout交换机将消息路由给每一个与之绑定的队列
    • Direct交换机根据RoutingKey判断路由给哪个队列
    • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
  • 基于@RabbitListener注解声明队列和交换机会用到哪些注解?
    • @Queue、@Exchange
3)Topic交换机
  • Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符,通过通配符,消息可以从交换机发送到一个或多个指定的队列

  • Routingkey 一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

  • 通配符规则:
    #:匹配一个或多个词
    *:匹配不多不少恰好1个词
    举例说明:item.#:能够匹配item.spu.insert 或者 item.spu;而item.*:可以匹配到item.spu,而不能匹配到item.spu.insert

  • 再如下图所示:
    在这里插入图片描述

    • Queue1:绑定的是china.# ,因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weather
    • Queue2:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news
  • 案例需求如下:
    1)并利用@RabbitListener声明Exchange、Queue、RoutingKey
    2)在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
    3)在publisher中编写测试方法,向itcast. topic发送消息

在这里插入图片描述

  • 声明队列、交换机、路由并交由消费者接收消息
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "xyl.topic",type = ExchangeTypes.TOPIC),
            key = {"china.#"}
    ))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者1 --> 接收到topic.queue1的消息:【" + msg + "】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "xyl.topic",type = ExchangeTypes.TOPIC),
            key = {"#.news"}
    ))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者2 ==> 接收到topic.queue2的消息:【" + msg + "】");
    }
  • 消息发送
    在publisher服务的SpringAmqpTest类中添加测试方法:
  @Test
    public void testSendTopicExchange() {
        // 交换机名称
        String exchangeName = "xyl.topic";
        // 消息
        String msg1 = "中国青年报,人民日报联合发表评论文章...";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "china.news", msg1);

        String msg2 = "Java生态发展势头依然强劲";
        rabbitTemplate.convertAndSend(exchangeName,"java.job.soft.news",msg2);

        String msg3 = "中国考研人数再创历史新高";
        rabbitTemplate.convertAndSend(exchangeName,"china.people.history",msg3);
    }
  • 测试结果如下:
消费者1 --> 接收到topic.queue1的消息:【中国青年报,人民日报联合发表评论文章...】
消费者2 ==> 接收到topic.queue2的消息:【中国青年报,人民日报联合发表评论文章...】
消费者2 ==> 接收到topic.queue2的消息:【Java生态发展势头依然强劲】
消费者1 --> 接收到topic.queue1的消息:【中国考研人数再创历史新高】
  • Topic交换机总结:
    • Topic交换机接收的消息RoutingKey可以是一个或多个单词
    • Topic交换机与队列绑定时的bindingKey可以指定通配符,#:代表0个或多个词, *:代表1个词

3.3、消息转换器

Spring会把发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
在这里插入图片描述

  • 而默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
    • 数据体积过大、有安全漏洞、可读性差
  • 在RabbitConfig中声明一个实现消息转换器的队列(同前面实现的队列一样)
  //  消息转换器
    @Bean
    public Queue objectQueue(){
        return new Queue("object.queue");
    }
  • 测试默认转化器:
    //  修改消息发送的代码,发送一个Map对象:
    @Test
    public void testSendMap() throws InterruptedException {
        // 准备消息
        Map<String,Object> msg = new HashMap<>();
        msg.put("name", "Jack");
        msg.put("age", 21);

        // 发送消息
        rabbitTemplate.convertAndSend("object.queue", msg);}
  • 发送消息后,在控制台中可以看到发送的消息已经转变为JSON格式的字符串,即Spring会把发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
配置JSON转换器
  • 显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
  • 在publisher和consumer两个服务中都引入依赖(可两个服务的父工程的pom文件中引入依赖):
 <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.9.10</version>
    </dependency>
  • 配置消息转换器
    在两个服务的启动类中添加一个消息转换器的Bean,如在Consumer服务的启动类中:
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    // 注意:MessageConverter、Jackson2JsonMessageConverter的包不要导错
    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}
  • 在消息监听类SpringRabbitListener中添加如下代码,实现对相应队列的消息监听:
 @RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map<String,Object> message){
        System.out.println("consumer接收到object.queue的消息:" + message);
    }
  • 再分别启动Consumer、Publisher服务,运行Publisher中的testSendMap方法,如此可见,已经正常完成消息的发送和接收

总结:

  • SpringAMQP中消息的序列化和反序列化是怎么实现的:
    • 利用MessageConverter 实现,默认是JDK的序列化方式
    • 注意,消息的发送方和接收方必须使用相同的MessageConverter (org.springframework.amqp.support.converter.MessageConverter

消息队列的模式总结:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值