消息队列RabbitMq精析

1.Mq简介

1.什么是Mq

Mq翻译为消息队列(消息中间件),通过典型的生产者消费者模型,生产者不断向消息队列中生产消息。消费者不断从队列中取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接受,没有业务逻辑的侵入,轻松实现消息间的解耦。通过利用高效可靠的消息传递机制进行平台无关的数据交流。并基于数据通信来进行分布式系统的集成。

2.Mq有哪些

ActiveMq,RocketMq,Kafka,RabbitMq

3.不同Mq的不同特点

  • ActiveMq
    ActiveMq是Apache出品,最流行,能力强筋的消息总线,完全支持JMS规范的消息中间件,丰富的API,多种集群架构模式。
  • RocketMq
    高吞吐,高可用,适合大规模分布式系统应用。
  • Kafka
    是分布式发布-订阅消息系统。Apache顶级项目。基于oull的模式来处理消息消费,追求高吞吐,不支持事务,对消息重复,丢失错误无明显要求,适合大数据业务。
  • RabbitMq
    基于AMQP协议实现,面向消息,队列,路由,可靠,安全。对数据一致性要求很高。性能和吞吐量一般。

4.AMQP

真正使用时,一个服务使用一个虚拟主机(virtual host)
在这里插入图片描述

2.RabbitMq简介

1.RabbitMq基本消息模型-----直连

1.1生产者

生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区
在这里插入图片描述

1.创建项目,导入amqp-client依赖
2.启动rabbitmq,进入http://localhost:15672/#/vhosts,新建一个virtual host 名字为/ems在这里插入图片描述

3.新建一个user名字为ems在这里插入图片描述
4.点击user目录下的name(我们设置的为ems),去配置它的虚拟主机在这里插入图片描述
此时ems已经绑定/ems虚拟主机在这里插入图片描述
5.接着我们构建项目代码

 public  void sendMessage() throws IOException, TimeoutException {
    
    
        //创建连接mq的工厂对象
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //设置连接rabbitmq主机,端口号,虚拟主机,用户名,密码
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/ems");
        connectionFactory.setUsername("ems");
        connectionFactory.setPassword("123");
        //获取连接对象
        Connection connection = connectionFactory.newConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        //通道绑定对应消息队列
        //参数1:队列名称,不存在就自动创建
        //参数2:是否需要持久化 true需要
        //参数3:是否独占队列(只能一个通道用它) true独占
        //参数4:是否完成队列后自动删除队列 true是
        //参数5:额外附加参数
        channel.queueDeclare("hello",false,false,false,null);
        //发布消息
        //参数1:交换机名称 参数2:队列名称 参数3:传递消息额外设置 参数4:消息具体内容
        channel.basicPublish("","hello",null,"hello word mq".getBytes());
        channel.close();
        connection.close();
    }

运行后,在 http://localhost:15672/#/queues会发现新建了一个hello队列并且放入了一条消息。在这里插入图片描述

1.2.消费者
class Consumer {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置连接rabbitmq主机,端口号,虚拟主机,用户名,密码
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/ems");
        connectionFactory.setUsername("ems");
        connectionFactory.setPassword("123");
        //获取连接对象
        Connection connection = connectionFactory.newConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        channel.queueDeclare("hello", false, false, false, null);
        //消费消息
        //参数1:被消费的队列名称
        //参数2:自动确认机制
        //参数3:消费时的回调接口
        channel.basicConsume("hello", true, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println(new String(body));
            }
        });

    }
}

运行后,如果不中断此线程,会持续接受队列中的信息。

注:很明显,我们的代码冗余度是非常大的,下面我们来简化。
使用如下applicat.yml中进行的应用配置

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    connection-timeout: 2000ms
    template:
      mandatory: true
    listener:
      simple:
        prefetch: 2000
        acknowledge-mode: manual
        concurrency: 5
        max-concurrency: 10

可节约掉如下代码

        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/ems");
        connectionFactory.setUsername("ems");
        connectionFactory.setPassword("123");

这个模型的缺点是可能造成消息在队列中堆积,从而造成不可预计的麻烦。

2.RabbitMq基本消息模型----work queue

在这里插入图片描述
工作队列:(又名:任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反,我们安排任务在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享(平均分配)。

与第一种模式的区别为:第一种只有一个消费者,而work queue允许多个消费者平均消息,以加快消费速度。

代码实现为在第一种直连模式的基础上,复制多份消费者并启动,在此不做演示

3.消息确认机制

简单说,在消费信息时,我们会设置是否自动确认接收到消息,在上面的例子中,两个消费者平均分配消息,从队列中取走信息会进行自动确认消息,队列就会删除此消息的信息。但这样的危害是,如果消费者拿到了消息,此时因为意外情况消费者挂掉了,那么没有执行业务逻辑的消息会全部丢失,这会给系统带来不小的风险。

解决方法:关闭自动消息确认,改为手动确认,在我们业务逻辑执行完之后,进行手动确认,这样就算消费者挂掉了,消息也还在队列中没有被删除,保证了系统的安全。

实现方法:把消费者改成如下,加注释的为新添代码。

class Consumer {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    
        ConnectionFactory connectionFactory = new ConnectionFactory();
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //设置每次只取一条消息
        channel.basicQos(1);
        channel.queueDeclare("work", true, false, false, null);      
        //参数2:自动确认机制(设置为不开启)  
        channel.basicConsume("work", false, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println("消费者2"+new String(body));
                //手动确认机制
                //参数1:需要确认的是哪个消息  (通过envelope的方法可得到消息的标签)
                // 参数2:是否一次确认多个消息
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });

    }
}

4.RabbitMq基本消息模型----fanout

又名扇出,广播模式。
消息发送流程如下:

  • 可以有多个消费者
  • 每个消费者有自己的queue
  • 每个队列绑定到自己的Exchange(交换机)
  • 生产者生产的消息只能发送到交换机,由交换机决定发给哪个队列。生产者无法决定。
  • 交换机将消息发送给所有绑定过的队列
  • 队列的消费者都可以拿到消息,实现一个队列有多个消费者。

5.RabbitMq基本消息模型----Routing之订阅模型Direct(直连)

在fanout中,一条消息,会被所有订阅的队列消费。但是,在某种场景下,我们希望不同的消息被不同的队列消费,这时就要用到Direct的Exchange。

在Direct模型下:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey
  • Exchange不再把消息交给某一个绑定的队列,而是根据RoutingKey判断,只有队列的RoutingKey与消息的RoutingKey相匹配,才会接收到消息。

在这里插入图片描述

package com.rabbitmq.routing;
import com.rabbitmq.client.*;
import com.rabbitmq.utils.Utils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class RoutingDirect {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    
        Producter producter = new Producter();
        producter.sendMessage();
    }
}

class Producter {
    
    
    public void sendMessage() throws IOException, TimeoutException {
    
    
        Connection connection = Utils.getConnection();
        Channel channel = connection.createChannel();
        //创建交换机
        //参数1:交换机名称
        //参数2:direct  路由模式
        channel.exchangeDeclare("logs_direct", "direct");
        //设置routingkey
        String routingkey = "info";
        //发布消息
        channel.basicPublish("logs_direct", routingkey, null, "这是routingkey发布的消息".getBytes());
        channel.close();
        connection.close();
    }
}

class Consumer {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    

        Connection connection = Utils.getConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        channel.exchangeDeclare("logs_direct", "direct");
        //临时队列,没必要持久化,加大压力
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue, "logs_direct", "error");

        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println("消费者1" + new String(body));
                //手动确认机制
                //参数1:需要确认的是哪个消息  (通过envelope的方法可得到消息的标签)
                // 参数2:是否一次确认多个消息
            }
        });
    }
}

class Consumer2 {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    

        Connection connection = Utils.getConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        channel.exchangeDeclare("logs_direct", "direct");
        //临时队列,没必要持久化,加大压力
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue, "logs_direct", "error");
        channel.queueBind(queue, "logs_direct", "info");
        channel.queueBind(queue, "logs_direct", "warning");
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println("消费者1" + new String(body));            
            }
        });
    }
}

注:就算只发布了一条消息,如果是相匹配的routingkey,则每个消费者都可以收到,类似微信公众号。(前面的都是一个消息只被消费一次)

6.RabbitMq基本消息模型----Topic

发送到主题交换机的消息不能具有任意的 routing_key-它必须是单词列表,以分隔。这些词可以是任何东西,但通常它们指定与消息相关的某些功能。一些有效的路由关键示例:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。路由关键字中可以包含任意多个单词,最多255个字节。

绑定密钥也必须采用相同的形式。主题交换背后的逻辑 类似于直接交换-用特定路由键发送的消息将传递到所有用匹配绑定键绑定的队列。但是,绑定键有两个重要的特殊情况:

  • *(星号)可以代替一个单词。
  • #(哈希)可以替代零个或多个单词。
    在这里插入图片描述
    如下代码中消费者队列的routingkey一个为user.#,一个为user.*
    • 消息的routingkey为X.X,则两个都能消费到信息。
    • 消息的routingkey为X.X.X,则只有第一个可以消费到信息。
package com.rabbitmq.topic;

import com.rabbitmq.client.*;
import com.rabbitmq.utils.Utils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Topic {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    
        Producter producter = new Producter();
        producter.sendMessage();

    }
}
class Producter {
    
    
    public void sendMessage() throws IOException, TimeoutException {
    
    
        Connection connection = Utils.getConnection();
        Channel channel = connection.createChannel();
        //创建交换机
        //参数1:交换机名称
        //参数2:direct  路由模式
        channel.exchangeDeclare("topic", "topic");
        //设置routingkey
        String routingkey = "user.save.save";
        //发布消息
        channel.basicPublish("topic", routingkey, null, "这是topic发布的消息".getBytes());
        channel.close();
        connection.close();
    }
}

class Consumer {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    

        Connection connection = Utils.getConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        channel.exchangeDeclare("topic", "topic");
        //临时队列,没必要持久化,加大压力
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue, "topic", "user.#");

        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println("消费者1" + new String(body));
                //手动确认机制
                //参数1:需要确认的是哪个消息  (通过envelope的方法可得到消息的标签)
                // 参数2:是否一次确认多个消息
            }
        });
    }
}

class Consumer2 {
    
    
    public static void main(String[] args) throws IOException, TimeoutException {
    
    

        Connection connection = Utils.getConnection();
        //获取连接中通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        channel.exchangeDeclare("topic", "topic");
        //临时队列,没必要持久化,加大压力
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue, "topic", "user.*");
        channel.queueBind(queue, "topic", "info");
        channel.queueBind(queue, "topic", "warning");
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
    
    
            @Override
            //body中存放的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                System.out.println("消费者2" + new String(body))             
            }
        });
    }
}

3.Rabbitmq与Spring Boot集成

3.1 第一种hello world模型使用(一对一发布接受消息)

导入依赖,填写配置信息后,使用RabbitTemplate来简化操作,使用时直接在项目中注入即可。

  1. 开发生产者
    //注入rabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    //参数1:routingkey 参数2:消息体
    public void testHello(){
          
          
      rabbitTemplate.convertAndSend("hello","hello world");
    }
    
  2. 开发消费者
      //@RabbitListener接听指定队列,并传给@RabbitHandler去处理
    @Component
    @RabbitListener(queuesToDeclare = @Queue("hello"))
    public class HelloCustomer {
          
          
    
        @RabbitHandler
        public void receive1(String message){
          
          
            System.out.println("message = " + message);
        }
    }
    

3.2 第二种work模型使用(平均分配消息)

  1. 开发生产者
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void testWork(){
          
          
      for (int i = 0; i < 10; i++) {
          
          
        rabbitTemplate.convertAndSend("work","hello work!");
      }
    }
    
  2. 开发消费者
    @Component
    public class WorkCustomer {
          
          
        @RabbitListener(queuesToDeclare = @Queue("work"))
        public void receive1(String message){
          
          
            System.out.println("work message1 = " + message);
        }
    
        @RabbitListener(queuesToDeclare = @Queue("work"))
        public void receive2(String message){
          
          
            System.out.println("work message2 = " + message);
        }
    }
    

    说明:默认在Spring AMQP实现中Work这种方式就是公平调度,如果需要实现能者多劳需要额外配置

3.3 Fanout 广播模型(发布/订阅模式,无routingkey)

  1. 开发生产者
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void testFanout() throws InterruptedException {
          
          
      rabbitTemplate.convertAndSend("logs","","这是日志广播");
    }
    
  2. 开发消费者
    @Component
    public class FanoutCustomer {
          
          
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue,
                exchange = @Exchange(name="logs",type = "fanout")
        ))
        public void receive1(String message){
          
          
            System.out.println("message1 = " + message);
        }
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue, //创建临时队列
                exchange = @Exchange(name="logs",type = "fanout")  //绑定交换机类型
        ))
        public void receive2(String message){
          
          
            System.out.println("message2 = " + message);
        }
    }
    

3.4 Route 路由模型(发布/订阅模式,有routingkey)

  1. 开发生产者
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void testDirect(){
          
          
      rabbitTemplate.convertAndSend("directs","error","error 的日志信息");
    }
    
  2. 开发消费者
    @Component
    public class DirectCustomer {
          
          
    
        @RabbitListener(bindings ={
          
          
                @QueueBinding(
                        value = @Queue(),
                        key={
          
          "info","error"},
                        exchange = @Exchange(type = "direct",name="directs")
                )})
        public void receive1(String message){
          
          
            System.out.println("message1 = " + message);
        }
    
        @RabbitListener(bindings ={
          
          
                @QueueBinding(
                        value = @Queue(),
                        key={
          
          "error"},
                        exchange = @Exchange(type = "direct",name="directs")
                )})
        public void receive2(String message){
          
          
            System.out.println("message2 = " + message);
        }
    }
    
    

3.5 Topic 订阅模型(发布/订阅模型,动态路由模型,可指定routingkey的模型范围)

  1. 开发生产者
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    //topic
    @Test
    public void testTopic(){
          
          
      rabbitTemplate.convertAndSend("topics","user.save.findAll","user.save.findAll 的消息");
    }
    
  2. 开发消费者
    @Component
    public class TopCustomer {
          
          
        @RabbitListener(bindings = {
          
          
                @QueueBinding(
                        value = @Queue,
                        key = {
          
          "user.*"},
                        exchange = @Exchange(type = "topic",name = "topics")
                )
        })
        public void receive1(String message){
          
          
            System.out.println("message1 = " + message);
        }
    
        @RabbitListener(bindings = {
          
          
                @QueueBinding(
                        value = @Queue,
                        key = {
          
          "user.#"},
                        exchange = @Exchange(type = "topic",name = "topics")
                )
        })
        public void receive2(String message){
          
          
            System.out.println("message2 = " + message);
        }
    }
    

4. MQ的应用场景

1.异步处理(发布/订阅模型)

场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式

  • 串行方式: 将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.

在这里插入图片描述

  • 并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。

在这里插入图片描述

  • 消息队列:假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回. 消息队列: 引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理

在这里插入图片描述

由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍。

4.2 应用解耦

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cvOkhmG-1598148693905)(RibbitMQ 实战教程.assets/SouthEast-20191127211247287.png)]

这种做法有一个缺点:

当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rumP8Bvx-1598148693907)(RibbitMQ 实战教程.assets/SouthEast-20191127211304085.png)]

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

  • 库存系统:订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.

4.3 流量削峰

场景: 秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。

作用:

​ 1.可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)

​ 2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVq3vOlE-1598148693910)(RibbitMQ 实战教程.assets/SouthEast-20191127211341601.png)]

  • 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.

  • 秒杀业务根据消息队列中的请求信息,再做后续处理.


猜你喜欢

转载自blog.csdn.net/weixin_40485391/article/details/108133246