rabbitmq

文章目录

RabbitMQ

image.png

1.什么是rabbitmq?

RabbitMQ是实现了高级消息队列协议AMQP)的开源消息代理软件(亦称面向消息的中间件)。

RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

2.什么是中间件呢?

中间件有很多种web中间件,数据库中间,消息中间件。那么什么是中间件呢???

中间件是加在两个系统之间,主要是来去协调或者提高他们的工作效率,这样的一个东西,我们称它为中间件。

在两个系统之间做了一个桥梁,使得系统之间的通信或者说性能更高效,符合这种特性我们都叫它中间件!!!

image.png

3.关于桌面应用程序的执行?

  • 在我们看到屏幕的内容时,桌面应用应用程序在不断地绘图,一帧一帧的
  • 单核CPU在不同的进程之间来回切换,CPU给每个进程分配的执行时间是不同的,在执行完时间后,就返回去执行别的进程

所以当桌面的应用程序越多的时候,有些程序打开了或者执行了某些操作,会因为没有得到执行权而一直等待,就会导致我们平时看到的屏幕卡顿的效果,

因为桌面是不断地一帧一帧绘图。

image.png

4.应用场景?

1.削峰

1.在高并发的情况下,请求过快,可能导致数据库压力过大,从而崩溃 -------rabbitmq作用:削峰(减少线程积压,削减流量)

image.png

解决:

image.png

加入消息中间件,其实就是一个队列(先进先出)。

  • controller接收请求后,执行对应业务放置一个消息到消息中心(整个过程相当于写入一个字符串),然后将请求进行返回
  • service在从消息中心的消息一个个拿出来,再去执行。

就像商场搞了一个火爆的活动,因为人流太多,导致地板的塌陷或者一些踩踏事件等等。因为我们可以让他们进行排队。

按照排队的顺序,一个一个按顺序来进行消费。

2.异步

2.用户在执行非常耗时的操作时,假设一次请求需要3s才能完成,可能因为线程积压太多,CPU的执行权在线程之间来回切换,导致请求需要4,5s甚至更久

image.png

解决:

image.png

当我们发起请求到controller接收到service可能要花费3s+10ms。才能够将请求返回。

如果我们使用中间件,当controller发送消息到消息中心的时候,速度是非常快的,只要5ms,那么就能将请求进行返回!!!

然后service在从消息中心中拿去消息一个一个进行消费。

思考问题:

1.那么可能有人会说消息很多的时候,那么service不会特别慢吗?

  • 针对这个,我们可以对service这个服务进行集群,可以同时把消息拿到服务进行一个处理

2.当我们做这个业务时,整个请求可能会失败或者成功,需要告诉用户,当我们通过controller发送请求给消息中心的时候,请求就返回了,而service可能还没做业务,那么我们如何返回结果呢

  • 其实现在就相当于通过异步的方式去执行业务返回请求给用户后,就慢慢执行业务
  • 我们可以通过webscoket去跟用户建立一个长连接,去监听service返回的结果信息,去响应给用户!!!!!

3.流量缓冲保护

3.当我们需要在请求时记录日志在ES中,由于太多日志,ES能力有限,发送记录日志的请求太高太快,导致ES崩溃

------rabbitmq作用:流量缓冲保护

image.png

解决:

image.png

在这里消息中心相当于充当一个缓冲层的作用,之前因为请求的流量太多导致崩溃,现在先将消息放置到消息中心

logback然后将消息一个个取出来,在进行消费。

4.广播

将消息中心的消息广播到多个服务器,共享消息。

image.png

5.解耦

在我们没有使用消息中间件的时候,订单服务会依赖于会记凭证服务,耦合度非常的高

  • 当我们使用消息中间件的时候进行解耦,订单服务跟会记凭证服务是没有相关性的依赖的两个服务
  • 会记凭证服务挂掉了,也不会影响订单服务,我们只要重新会记凭证服务就可以继续消费

思考:

1.如果消息中心挂掉了呢?

  • 默认发消息是存在内存中的,但是它会吧消息持久化到磁盘里面,消息中西挂了,会记凭证服务直接从磁盘里面取出来,消息是不会丢失的

2.当会记凭证服务在把消息中心的消息取出来之后,会记凭证服务挂掉了呢,那么信息不就丢失了吗?

  • 这边有个机制,当会记凭证服务正常完成之后,消息中心的对应消息才会被删除掉,有一系列可靠的机制

image.png

5.消息中心简介?

5.1 消息中间件的理解与对比?

kafla日志,可靠性低,会丢失一点信息

rocketmq参考kalfa,淘宝开源出来,经历过双11,

rabbitmq金融领域用得比较多

5.2 AMQP协议以及模型

两个应用程序进行通讯需要准守一些协议

浏览器与web服务器,http协议

ss工具和服务器之间通过ssh协议

mysql遵循jdbc协议

而我们要与消息中心进行通信必须遵循AMQP协议

image.png

交换机跟队列进行绑定时有一个对应的key标识

image.png

6.关于rabbitmq的下载安装

image.png

image.png

步骤一:Erlang的安装

  • rabbitmq不是java写的,是用Erlang写的,所以在运行mq前要安装一个Erlang
  • 而且Erlang跟mq版本有一个支持的映射关系的

image.png

返回刚刚的页面,我们可以到支持rpm的安装(在虚拟机上直接运行安装下载rpm文件就可

点击RPM进去发现有对应的centos对应的版本的操作

  • RPM的好处,当公司为了安全做了一些隔离,上不了网,那么可以通过rpm进行安装,这样就需要下载一个rpm放进去然后安装
  • 在yum则需要网络才能进行安装

image.png

步骤二:rabbitmqServer的安装

接下来安装rabbitmqServer,下面有对应的下载rpm包和对应的指令操作

image.png

7.rabbitmq入门案例

7.1 关于消息的发送与消费流程?

生产者发送消息到消息中心,消费者进行监听消息中心,拿到对于的消息进行消费,然后执行对于的业务操作

image.png

  • controller接受请求执行业务时,会往消息中心发送消息
  • 对应的消费者某个controller的消息进行监听,监听后拿到消息,将消息作为参数信息,传到service业务层去执行对应的业务逻辑

image.png

7.2 生产者案例

导入对应的amqp协议的客户端依赖(因为生产者属于客户端)

<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>
</dependencies>

编写案例 rabbitmq官网中提供了java相关的案例代码

image.png

首先创建连接rabbitmqService的连接工厂,设置rabbitmqService所在的服务器的ip地址

根据连接工厂来获取连接,根据连接来获取channel通道

根据通道向服务器发送一个请求,声明一个队列(通道绑定一个队列)

通过通道往服务器中发送消息

//生产者案例:模拟生产者向消息中心发送信息
public class Send {
    //定义队列的名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        //1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2. 设置rabbitmq服务器的主机地址
        factory.setHost("192.168.17.128");
        //资源放在try的括号里面,用于关闭资源(jdk1.7新特性)
        try (Connection connection = factory.newConnection();
             //3. 根据连接工厂来获取连接,根据连接来创建通道
             Channel channel = connection.createChannel()) {
            /* 4. 通过通道向服务器发送请求,声明一个队列
              channel.queueDeclare(QUEUE_NAME, false, false, false, null);
              参数1:声明要发送消息到哪个队列
              参数2::
             */
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //定义字符串常量(变量存储要发送的消息)
            String message = "Hello World!";
            /* 5. 通过通道往服务器中发送消息
               channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
               channel.basicPublish("", "发送消息到哪个队列名称", 消息的属性信息,字符串的byte数组);
               生产者部署到一台服务器,消息中心部署到一台服务器
               服务器之间通过网络进行通信,生产者不可能把一个对象作为消息放置到消息中心去
               因此生产者只能说将对象变成二进制的数据,然后通过网络进行传输
             */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

运行截图:

image.png

7.3 消费者案例

7.3.1 案列步骤

导入对应的amqp协议的客户端依赖(因为生产者属于客户端)

<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>
</dependencies>

编写案例 rabbitmq官网中提供了java相关的案例代码

通过连接工厂创建连接,通过连接创建通道

通道声明队列

通道传入队列名称,以及deliverCallback对象(消费时执行对象里的方法),进行消费

package cn.iottepa.rabbitmq._01hello;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class Recv {

    //定义队列的名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //如果队列在服务器中已经存在就不再创建,如果不存在则创建
        //如果服务器中已经有了这个队列,执行这句话,定义队列的参数需要和服务器上的队列参数保持一致,否则报错
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            /*
                consumerTag:消费者的标识
                delivery: 封装了发送信息时的相关信息的封装     delivery.getBody():获取存放在消息中心的内容
             */
            try{
                //处理消息的逻辑
                int a = 1 / 0 ;
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
        };
        //通过管道定义消息的监听对象
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}
7.3.2 细节
  •   消费者是阻塞式的不会被关掉(长连接),所以不需要trycatch代码块进行关闭资源
    
  •   delivery封装了协议,方法,内容实体等等,因此我们可以通过delivery获取发送的信息内容
    

如下类比http协议请求返回的内容:包含了方法,URI,协议版本,请求首部字段,内容实体

image.png

当我们启动消费者时,会有个对应的通道建立,用于传输队列消息和签收

channel里的ip对应的是虚拟网卡8的IPv4的地址

image.png

7.3.3 演示|验证存在队列中是按顺序消费(先进先出)

我们在生产者案例的发送信息代码里面进行修改

String message = "Hello World!" + new Date();

消费者开启,然后生产者连续发送10条信息,可以发现输入台里面的消费信息的对应的时间是按照顺序的

8.队列的相关参数

8.1 队列是否持久化

在生产者或者消费者进行声明队列是设置

/* channel.queueDeclare(队列的名称, 队列是否需要持久化, false, false, null);
    如果不持久化的话,服务区关掉或者重启,队列会被删除掉   ----队列显示D(durable:true  持久化的)
    单单只是队列持久化,信息还是被删掉!!!!!
    解决:我们可以在发送信息的时候,声明信息的持久化
 */
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
8.2 消息是否持久化

在生产者通过管道进行发送信息时进行设置

 //如果队列在服务器中已经存在就不再创建,如果不存在则创建
 //如果服务器中已经有了这个队列,执行这句话,定义队列的参数需要和服务器上的队列参数保持一致,否则报错
/* 5. 通过通道往服务器中发送消息
   channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
   channel.basicPublish("", "发送消息到哪个队列名称", 消息的属性信息,字符串的byte数组);
   生产者部署到一台服务器,消息中心部署到一台服务器
   服务器之间通过网络进行通信,生产者不可能把一个对象作为消息放置到消息中心去
   因此生产者只能说将对象变成二进制的数据,然后通过网络进行传输

   第三个参数:可以声明信息的持久化(服务器重启后,信息不会被删掉) ---MessageProperties.PERSISTENT_TEXT_PLAIN
 */
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
8.3 队列是否排他性

在声明队列的时候,进行设置

 /* channel.queueDeclare(队列的名称, 队列是否需要持久化, 是否排他性的队列, false, null);
            排他性的队列:这个队列的消息只能给当前connection所创建出来的通道使用,别的connection时没法获取都这个消息的
                       特性:connection关闭之后,这个队列就会自动的删除掉(因为使用了try会关闭资源,所以我们可以使用睡10秒)
                       队列显示exclusive:true,表示当前队列位排他性队列
  */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
8.3.1 验证排他性实验一

在生产者代码进行睡眠10s,这样可以观察到管控台页面显示队列显示exclusive:true

如果不进行睡眠,当连接关闭了之后,队列就会被删除掉看不到效果

//排他性实验1.用于验证排他性队列    队列显示exclusive:true
// TimeUnit.SECONDS.sleep(10);
8.3.2 验证排他性实验二

使用消费者的通道去消费信息,结果发现不行

  • 生产者声明为排他性队列时,只能给生产者的connection对应的chanel通道去消费、
  • 而消费者的connection跟生产者不同,所以不行
//排他性实验2.用于演示不同connection的通道不能够消费,使用消费者进行消费生产者的connection
//结果:消费者服务显示 cannot obtain exclusive access
// TimeUnit.SECONDS.sleep(60);
8.3.3 验证排他性实验三

使用生产者的同个connection下的不同channel通道去消费

这里注意通过通道去消费是异步的操作,需要将try去掉,防止释放connecion,这样就消费不了

            /*排他性实验13.模拟排他性的信息,同连接的不同通道能够进行消费
                basicConsume发送消息是异步的,发送完,连接就被断了,队列就被删除了,消费不了
                因此我们可以进行将try去掉,这样就一直没有关闭资源(异步操作,代码执行往下走,trycatch代码里的连接就被关闭了)

                结果:
                 [x] Sent 'Hello World!Mon Jun 21 17:29:03 CST 2021'
                 [x] Received11111 'Hello World!Mon Jun 21 17:29:03 CST 2021'
             */
            TimeUnit.SECONDS.sleep(10);
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String m = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received11111 '" + m + "'");
            };
            channel2.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
8.4 是否使用完队列之后删除队列(自动删除)

在声明队列的时候进行设置为true,如果消费完没有人在监听队列就删除掉队列

/* 
    channel.queueDeclare(队列的名称, 队列是否需要持久化, 是否排他性的队列, 是否使用完队列之后删除队列, null);
     消息消费完,并且没有人在监听队列就删除掉   队列显示auto-delete:true
 */
channel.queueDeclare(QUEUE_NAME, true, false, false, null);

9.消息的签收方式

在消费者通过管道进行消费的时候,进行设置

//通过管道定义消息的监听对象
/*channel.basicConsume(需要监听的队列名称, true:自动签收,false:手动签收, 消息的监听事件处理器, 消息队列删除的时候会触发这个方法);
      自动签收:当消费者拿到消息中心的信息的时候,就会发送一个请求给消息中心,然后消息中心去删除当前对应的信息,会导致消息的丢失
      手动签收:当消费者拿到消息中心的信息的时候,当消费成功后就会发送一个请求给消息中心,然后消息中心去删除当前对应的信息。
              如果消费者崩掉了,重启消费者重新从消费中心服务器拉去信息。(推荐,不会导致信息的丢失)
      通过异常进行测试:int a = 1 / 0 ;

 */
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });

9.1 自动签收

使用自动签收的方式:

  • 当消费者拿到消息中心的信息的时候,就会发送一个请求给消息中心
  • 然后消息中心去删除当前对应的信息,会导致消息的丢失

image.png

9.2 手动签收

手动签收:

  • 当消费者拿到消息中心的信息的时候,当消费成功后就会发送一个请求给消息中心
  • 然后消息中心去删除当前对应的信息

如果消费者崩掉了,重启消费者重新从消费中心服务器拉去信息。(推荐,不会导致信息的丢失)

image.png

当签收失败时:

image.png

没有签收的消息,在断开连接后变成ready状态,然后又可以继续的消费:

image.png

代码:

手动签收,需要通过代码去签收

                /*
                  消息中心和消费者建立了一个channel通道进行连接拿到信息和签收信息
                  每个消息有唯一的标识,将标识编号,消费者签收后通过channel通道传回去标识,消息中心继续做别的操作
                delivery.getEnvelope().getDeliveryTag():标识这条信息在通道中的唯一标识Long数值
                 */
                //手动签收
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

9.3 是否多个签收(批量签收)

  • 消息中心和消费者建立了一个channel通道进行连接拿到信息和签收信息
  • 每个消息有唯一的标识,将标识编号,消费者签收后通过channel通道传回去标识,消息中心继续做别的操作
/*
  消息中心和消费者建立了一个channel通道进行连接拿到信息和签收信息
  每个消息有唯一的标识,将标识编号,消费者签收后通过channel通道传回去标识,消息中心继续做别的操作
delivery.getEnvelope().getDeliveryTag():标识这条信息在通道中的唯一标识Long数值
 */
//第二个参数:是否多个一起签收,批量签收
//手动签收
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

9.4 拒绝签收

// channel.basicReject(); 很早以前的api,拒绝签收,让消息重新回队列,不支持批量签收
// channel.basicNack("消息的唯一标识",是否批量处理,是否需要重新进入队列);
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,true);

9.5 关于手动签收的理解与套路使用

关于手动签收:

1.当我们使用手动签收的时候,需要写代码进行手动签收(ack),以及拒绝签收

  1. 当我们调用业务方法出现异常等等,则不会调用ack方法,消息处于unAcked状态
  • 如果没有处理,这条消息始终都是unacked,也不会重新发送给消费者
  • 只有当消费者断开连接后,消息状态转为Ready状态,才能够被重新消费
  1. 当失败的时候,可以通过nacked方法,可以通过此操作可以不签收,可以让这个消息重新进入队列中(可能造成死循环)
    所以需要自己去控制尝试的次数
      //伪代码:使用redis控制尝试的次数
      try{
           //业务操作
           channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
      }catch(Exception e){
           String orderNo = 从消息中获取到订单编号;
           long count = redis.incr("前缀"+orderNo);
           if(count < 3){
              channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,true);
           }else{
              //当尝试的次数已达最大次数时,不允许重新进入队列,在被消费者进行监听消费
              channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,false);
           }
      }

注意:
basicNack和basicReject,在执行的时候,如果requeue=false,不签收消息/拒绝消息
如果没有配置死信队列,消息就直接丢弃了
如果配置了死信队列,消息就会自动的去到死信队列中
死信队列是消息没办法通过程序去处理,可以通过人工进行处理(一般写个消费者监听死信队列,然后发送信息给客服去人工处理)

image.png

完整代码:

public class Recv {

    //定义队列的名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            try{
                //处理消息的逻辑
                int a = 1 / 0 ;
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
                /*
                  消息中心和消费者建立了一个channel通道进行连接拿到信息和签收信息
                  每个消息有唯一的标识,将标识编号,消费者签收后通过channel通道传回去标识,消息中心继续做别的操作
                delivery.getEnvelope().getDeliveryTag():标识这条信息在通道中的唯一标识Long数值
                 */
                //第二个参数:是否多个一起签收,批量签收
                //手动签收
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
            }catch (Exception e){
                e.printStackTrace();
                // channel.basicReject(); 很早以前的api,拒绝签收,让消息重新回队列,不支持批量签收
                // channel.basicNack("消息的唯一标识",是否批量处理,是否需要重新进入队列);
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,true);
            }
        };
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}

10.work模式

10.1 测试与思考

  • 开启两个消费者进行消费,我们使用生产者发送20条信息,发现他们是轮询的
  • 但是在服务器中,每个服务器的性能不同,所以应该要能者多劳

生产者:

//worker模式---预选机制
public class Send_worker {
    //定义队列的名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        //1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2. 设置rabbitmq服务器的主机地址
        factory.setHost("192.168.17.128");
        //资源放在try的括号里面,用于关闭资源(jdk1.7新特性)
        try (Connection connection = factory.newConnection();
             //3. 根据连接工厂来获取连接,根据连接来创建通道
             Channel channel = connection.createChannel();
             Channel channel2 = connection.createChannel();) {
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            for (int i = 0; i < 20 ; i++) {
                String message = "Hello World!" + i;
                channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
            }
            System.out.println("发送完毕");
        }
    }

}

消费者:

public class Recv_worker1 {

    //定义队列的名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            try{
                TimeUnit.SECONDS.sleep(1);
                //处理消息的逻辑
                // int a = 1 / 0 ;
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println("Recv_worker1 ==>" + message + "'");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
            }catch (Exception e){
                e.printStackTrace();
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,true);
            }
        };

        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}

10.2 预选机制

/*
   关于预取机制?
   rabbitmq中存在一个预取机制,默认预取250个消息
   1.假设我们有两台服务器,当我们发送300个信息,则两台服务器会各预取150
   2.假设两台服务器,我们发送600个信息,则它们分别预取250个,剩下100个,按照谁消费完,谁监听
   3.假设有三台服务器,我们发送500个信息,则两台服务器各预取250个,另外一台服务器收不到消息,浪费服务器资源
   因此我们需要将预选改成1,谁做的更快,谁先取
 */

image.png

在消费者的通道进行设置预选的数量

channel.basicQos(1); ///设置预选数量

测试:

定义两个消费者,一个在执行业务前睡眠一秒,一个睡眠两秒,发现他们执行的数量是不同的。

11.发布与订阅模式

指的是一个生产者进行发布信息,多个订阅者进行消费者信息

image.png

11.1 如何实现呢?

关于发布订阅模式:实现多个消费者监听已发个队列信息

实现原理:

  • 消费者监听是轮询的,要么给a要么给b,不能实现同时收到信息,因此一个消费者只能绑定一个对应队列的标识
  • 因此我们可以队列使用匿名(随机名字),这样就可以将一个信息同时发送多个消费者

image.png

集群只需要有一个就行

image.png

11.2 为什么匿名队列都有自动删除和排他性的?

我们将交换机与匿名生成的队列名称进行绑定,生成的匿名队列具有自动删除和排他性的

  • 自动删除,我们生产者可以根据绑定交换机,然后发送请求给该交换机所绑定的所有队列,因为外界是不知道匿名队列是什么名称,不可能直接通过消费
    所以是一次性的,当消费者连接中断则删除这个信息
  • 排他性:由客户端(消费者)生产的匿名队列,只能给当前的客户端去使
11.3 执行流程?

执行流程:

  • 当消费者客户端,进行绑定了交换机与队列的关系后,进行监听
  • 生产者绑定消费者绑定的交换机,通过交换机像所有的匿名队列的发送同样的信息------广播模式
11.4 实现代码

生产者:

public class EmitLog {
    //定义队列的名称
    private final static String EXCHANGE_NAME = "logs";
    public static void main(String[] argv) throws Exception {
        //1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2. 设置rabbitmq服务器的主机地址
        factory.setHost("192.168.17.128");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel();) {
            //定义交换机,类型是fanout(生产匿名随机队列)
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
            String message = "Hello World!";
            channel.basicPublish(EXCHANGE_NAME, "",null, message.getBytes("UTF-8"));
        }
    }

}

消费者:

/*
   关于发布订阅模式:实现多个消费者监听已发个队列信息
   消费者监听是轮询的,要么给a要么给b,不能实现同时收到信息,因此一个消费者只能绑定一个对应队列的标识
   因此我们可以队列使用匿名(随机名字),这样就可以将一个信息同时发送多个消费者

   我们将交换机与匿名生成的队列名称进行绑定,生成的匿名队列具有自动删除和排他性的
   自动删除,我们生产者可以根据绑定交换机,然后发送请求给该交换机所绑定的所有队列,因为外界是不知道匿名队列是什么名称,不可能直接通过消费
           所以是一次性的,当消费者连接中断则删除这个信息
   排他性:由客户端(消费者)生产的匿名队列,只能给当前的客户端去使用

   执行流程:
      当消费者客户端,进行绑定了交换机与队列的关系后,进行监听
      生产者绑定消费者绑定的交换机,通过交换机像所有的匿名队列的发送同样的信息------广播模式
 */
public class ReceiveLogs {
    //定义队列的名称
    private final static String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //定义队列的名称
        //channel.queueDeclare().getQueue();  匿名的queue(随机名字的queue)
        String queueName = channel.queueDeclare().getQueue();
        //声明队列和交换机的绑定关系
        //channel.queueBind(队列名称,交换机名称,"");
        channel.queueBind(queueName,EXCHANGE_NAME,"");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println("ReceiveLogs ==> '" + message + "'");

        };
        channel.basicConsume(queueName, false, deliverCallback, consumerTag -> { });
    }
}

12.routing路由模式

12.1 关于路由模式的理解

关于路由模式:
交换机的模式为路由模式routing

  • 消费者客户端绑定交换机(路由模式routing),绑定三者交换机,路由key,队列名称
  • 消费者客户端绑定交换机(路由模式routing),向特定地路由key标识发送信息到关联队列

生产者通过特定的交换机,找到特定的路由key,根据路由key关联的队列进行发送信息

消费者通过特定的交换,找到特定的路由key,根据路由key以及队列进行消费信息

路由key绑定的队列是匿名队列,随机生产的名称

image.png

12.2 实现代码

生产者:

public class RoutingProducer {
    //定义队列的名称
    private final static String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        //1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2. 设置rabbitmq服务器的主机地址
        factory.setHost("192.168.17.128");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel();) {
            //定义交换机,类型是路由
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            String message = "directMsg!";
            String serverity = "info";
            //channel.basicPublish(交换机, 路由key,null, message.getBytes("UTF-8"));
            channel.basicPublish(EXCHANGE_NAME, serverity,null, message.getBytes("UTF-8"));
        }
    }

}

消费者:

/*
   关于路由模式:
     交换机的模式为路由模式routing
     消费者客户端绑定交换机(路由模式routing),绑定三者交换机,路由key,队列名称
     消费者客户端绑定交换机(路由模式routing),向特定地路由key标识发送信息到关联队列
 */
public class DirectLog1 {

    //定义队列的名称
    private final static String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //定义队列的名称
        //channel.queueDeclare().getQueue();  匿名的queue(随机名字的queue)
        String queueName = channel.queueDeclare().getQueue();
        //声明队列和交换机的绑定关系
        //channel.queueBind(队列名称,交换机名称,路由key);
        channel.queueBind(queueName,EXCHANGE_NAME,"info");
        channel.queueBind(queueName,EXCHANGE_NAME,"warm");
        channel.queueBind(queueName,EXCHANGE_NAME,"bug");


        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                //delivery.getEnvelope().getRoutingKey() 获取交换机路由key
                System.out.println("ReceiveLogs2 ==> '" + delivery.getEnvelope().getRoutingKey() +message + "'");

        };
        channel.basicConsume(queueName, false, deliverCallback, consumerTag -> { });
    }
}

13.topic主题模式

13.1 主题模式的理解

topic模式的交换机(与routing模式一样,多了支持通配符号

  • 消费者客户端绑定交换机(topic模式),绑定三者队列名称,交换机,路由key
  • 生产者客户端绑定交换机(topic模式),向特定的路由key发送信息到关联的队列

#表示单个字母

image.png

13.2 实现代码

生产者;

public class TopicProducer {
    //定义队列的名称
    private final static String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        //1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2. 设置rabbitmq服务器的主机地址
        factory.setHost("192.168.17.128");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel();) {

            //定义交换机,类型是路由
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            String message = "topicMsg!";
            String serverity = "log.error";

            //channel.basicPublish(交换机, 路由key,null, message.getBytes("UTF-8"));
            channel.basicPublish(EXCHANGE_NAME, serverity,null, message.getBytes("UTF-8"));
        }
    }
}

消费者:

/*
   topic模式的交换机(与routing模式一样,多了支持通配符号)
   消费者客户端绑定交换机(topic模式),绑定三者队列名称,交换机,路由key
   生产者客户端绑定交换机(topic模式),向特定的路由key发送信息到关联的队列
 */
public class TopicLog1 {
    //定义队列的名称
    private final static String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.17.128");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //定义队列的名称
        //channel.queueDeclare().getQueue();  匿名的queue(随机名字的queue)
        String queueName = channel.queueDeclare().getQueue();
        //声明队列和交换机的绑定关系
        //channel.queueBind(队列名称,交换机名称,路由key);
        channel.queueBind(queueName,EXCHANGE_NAME,"log.*");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                //delivery.getEnvelope().getRoutingKey() 获取交换机路由key
                System.out.println("TopicLog1 ==> '" + delivery.getEnvelope().getRoutingKey() +message + "'");
        };
        channel.basicConsume(queueName, false, deliverCallback, consumerTag -> { });
    }
}

14.关于普通交换机的解释

为什么在我们写生产者和消费者的案例的时候,并没有声明和绑定交换机呢(不需要交换机?),却能够进行进行生产消费?
其实内部有个AMQP default的默认交换机,当没有定义交换机时默认使用的就是这个交换机
路由key为队列的名称!!!

//声明队列的时候,队列是和默认的交换机进行绑定,绑定的路由key是队列名称
 channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//api中第一个参数为空(没有写交换机),默认发送到AMQP default的默认交换机上
 channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));

image.png

15.关于SpringBoot集成操作

操作前的准备:

导入依赖

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

编写配置文件:

spring.rabbitmq.host=192.168.17.128

15.1 普通生产者和消费者

生产者:

@RestController
public class HelloController {
    /*
       RabbitTemplate相当于使用原生api内部帮我们实现了获取连接,获取管道(有默认值,否则根据配置文件配置即可)等等
       我们只需要通过方法进行对应的操作即可
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @RequestMapping("/hello")
    public String hello(String msg){
        rabbitTemplate.convertAndSend("","boot_hello",msg);
        return "发送成功";
    }
}

消费者:

@Component
public class BootRecv {
    /*
    普通队列
    @RabbitListener(queuesToDeclare = @Queue(name = "boot_hello"))
      监听注解       声明队列
     */
    @RabbitListener(queuesToDeclare = @Queue(name = "boot_hello"))
    public void handlerNormalQueue(String msg){
        System.out.println("接受到信息:"+msg);
    }
}

15.2 手动签收

//手动签收
 /*
    手动签收,注入业务逻辑代码需要的对象
    delivery对象的标识long值,放在了对象头里
  */
 @RabbitListener(queuesToDeclare = @Queue(name = "boot_hello"))
 public void handlerNormalQueue2(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
     try{
         int i = 1 / 0;
         System.out.println("接受到信息:"+msg);
         channel.basicAck(deliveryTag,false);
     } catch (IOException e) {
         e.printStackTrace();
         channel.basicNack(deliveryTag,false,false);
     }
 }

15.3 worker模式(预选机制)

生产者:

@RequestMapping("/batch")
public String hello2(){
    for (int i = 0; i < 20; i++) {
        rabbitTemplate.convertAndSend("","boot_hello","消息"+i);
    }
    return "发送成功";
}

消费者:

//worker轮询
@RabbitListener(queuesToDeclare = @Queue(name = "boot_hello"))
public void handlerNormalQueue3(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
    try{
        System.out.println("handlerNormalQueue3接受到信息:"+msg);
        channel.basicAck(deliveryTag,false);
    } catch (IOException e) {
        e.printStackTrace();
        channel.basicNack(deliveryTag,false,false);
    }
}

配置文件配置:

spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=1

15.4 发布订阅模式

生产者:

@RequestMapping("/pub_sub")
public String pub_sub(){
      rabbitTemplate.convertAndSend("boot_logs","","广播信息");
    return "发送成功";
}

消费者:

//pub-sub模式:广播消息
@Component
public class pubsubRecv {
    //绑定交换机(交换机模式fanout),@Queue:表示匿名队列的名称
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_logs",type = "fanout")
    ))
    public void handlerNormalQueue(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("handlerNormalQueue:"+msg);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }

    //绑定交换机(交换机模式fanout),@Queue:表示匿名队列的名称
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_logs",type = "fanout")
    ))
    public void handlerNormalQueue2(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("handlerNormalQueue2:"+msg);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }
}

15.6 routing模式

image.png

生产者:

@RequestMapping("/routing")
public String routing(String direct){
    rabbitTemplate.convertAndSend("boot_direct_logs",direct,"direct信息");
    return "发送成功";
}

消费者:

//routing direct-topic
@Component
public class DirectRecv {
    //绑定交换机(交换机模式fanout),@Queue:表示匿名队列的名称
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_direct_logs"),
            key = {"log.info","log.error"}
    ))
    public void handlerNormalQueue(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("handlerNormalQueue:"+msg);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }

    //绑定交换机(交换机模式fanout),@Queue:表示匿名队列的名称
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_direct_logs"),
            key = {"log.error"}
    ))
    public void handlerNormalQueue2(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("handlerNormalQueue2:"+msg);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }
}

15.7 topic模式

生产者:

@RequestMapping("/topic")
public String topic(String direct){
    rabbitTemplate.convertAndSend("boot_topic_logs",direct,"topic信息");
    return "发送成功";
}

消费者:

//routing direct-topic
@Component
public class TopicRecv {

    //topic
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_topic_logs",type = "topic"),
            key = "log.*"
    ))
    public void topic(String direct, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("topic1:"+direct);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }

    //topic
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name = "boot_topic_logs",type = "topic"),
            key = "log.error"
    ))
    public void topic2(String direct, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        try{
            System.out.println("topic2:"+direct);
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            channel.basicNack(deliveryTag,false,false);
        }
    }
}

16.消息的排他性和自动删除

关于匿名队列的自动删除与排他性

  • 自动删除,每次启动生产的匿名队列都是不一样的
  • 当我们下线或者重启,那么消费者和原先的匿名队列失去连接,建立了先的匿名队列的连接
  • 原来的匿名队列则会一直存储在消息中,浪费内存空间,没有存在价值,直接删除掉

image.png

排他性:如果有人知道这个匿名队列的名称进行监听,其实是监听不了的,因为他具有排他性,监听他的人只能是定义他的人

在发布订阅就使用到排他性

100.使用docker安装

首先安装docker并且下载对应的镜像

(3条消息) docker安装RabbitMQ_努力明天会更好的博客-CSDN博客_docker安装rabbitmq

安装web界面管理插件

(3条消息) docker启动rabbitmq后无法访问15672端口_梦昼初-CSDN博客

页面出现Stats in management UI are disabled on this node

(3条消息) Docker部署rabbitmq遇到的问题 Stats in management UI are disabled on this node_Luke.Du的博客-CSDN博客

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值