消息队列RabbitMQ

一、RabbitMQ

1.1现实问题

目前我们已经完成了商品和搜索系统的开发。我们思考一下,是否存在问题?

  • 商品的原始数据保存在数据库中,增删改查都在数据库中完成。
  • 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。

如果我们在后台修改了商品的价格,搜索页面依然是旧的价格,这样显然不对。该如何解决?

这里有两种解决方案:

  • 方案1:每当后台对商品做增删改操作,同时要修改索引库数据
  • 方案2:搜索服务对外提供操作接口,后台在商品增删改后,调用接口

以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。

所以,我们会通过另外一种方式来解决这个问题:消息队列

消息队列RabbitMQ_第1张图片

消息队列RabbitMQ_第2张图片

1.2消息队列(MQ)

1.2.1什么是消息队列

消息队列,即MQ,Message Queue。

消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。

结合前面所说的问题:

  • 商品服务对商品增删改以后,无需去操作索引库,只是发送一条消息,也不关心消息被谁接收。
  • 搜索服务服务接收消息,去处理索引库。

如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。

1.2.2AMQP和JMS

MQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。

两者间的区别和联系:

  • JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
  • JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
  • JMS规定了两种消息模型;而AMQP的消息模型更加丰富

1.2.3常见的MQ产品

  • ActiveMQ:基于JMS
  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
  • Kafka:分布式消息系统,高吞吐量

1.2.4 RabbitMQ

RabbitMQ是基于AMQP的一款消息管理系统

官网: http://www.rabbitmq.com/

官方教程:http://www.rabbitmq.com/getstarted.html
消息队列RabbitMQ_第3张图片

消息队列RabbitMQ_第4张图片

消息队列RabbitMQ_第5张图片

消息中间件的两大规范:JMS,AMQP

RabbitMQ就是根据AMQP实现,也兼容JMS

提供的五种消息模型,第一种就是对队列的实现,后四种是我们发布订阅的变形实现

也就是遵循了两种实现,一个订阅的模式(发布订阅主体模式),还有一个队列模式点对点

假设我们有一个对象想要传出去,但它传输只支持流,我们把对象序列化成json,json就是一个字符串,把字符串以流的形式(byte)传输出去

消息队列RabbitMQ_第6张图片

spring boot无论整合那个消息中间件都非常简单,因为spring boot已经原生支持JMS,RabbitMQ

二、RabbitMQ概念

RabbitMQ简介:

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。

核心概念

  • Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则有一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

  • Publisher:消息生产者,也是一个向交换器发布消息的客户端应用程序。

  • Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有四种类型:direct(默认),fanout,topic,和headers,不同类型的Echange转发消息的策略有所区别

  • Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走

  • Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange和Queue的绑定可以是多对多的绑定。

  • Connection:网络连接,比如一个TCP连接

  • Channel:信道,多路复用连接中的一条独立的双向绑定数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入信道的概念,以复用一条TCP连接。

  • Consumer:消费的消费者,表示一个从消息队列中取得消息的客户端应用程序

  • Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须是在连接时指定,RabbitMQ默认的是vhost是/。

  • Broker:表示消息队列服务器实体

    消息队列RabbitMQ_第7张图片

    原理如下:

消息队列RabbitMQ_第8张图片

三、Docker安装RabbitMQ

消息队列RabbitMQ_第9张图片

3.1安装

下载镜像:docker pull rabbitmq:management

创建实例并启动:

docker run -d --name rabbitmq --publish 5671:5671 \
--publish 5672:5672 --publish 4369:4369 --publish 25672:25672 --publish 15671:15671 --publish 15672:15672 \
rabbitmq:management
加上这个命令,以后虚拟机docker一启动,rabbitmq就启动
docker update rabbitmq --restart=always

注意:

4369 – erlang发现口
5672 --client端通信口

15672 – 管理界面ui端口
25672 – server间内部通信口

3.2测试

我们只要访问15672端口,就可以看到rabbitmq的管理端的登录页

默认账号密码guest

消息队列RabbitMQ_第10张图片

connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况

channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。

Exchanges:交换机,用来实现消息的路由

Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。

端口:

5672: rabbitMq的编程语言客户端连接端口

15672:rabbitMq管理界面端口

25672:rabbitMq集群的端口

四、RabbitMQ运行机制

AMQP中的消息路由由

  • AMQP中消息的路由过程和Java开发熟悉的JMS存在一些差别,AMQP中增加了Exchange和Binding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而binding决定交换器的消息应该发送到那个队列。

消息队列RabbitMQ_第11张图片

Exchange类型

  • Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers。headers匹配AMQP消息的header而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:

    消息队列RabbitMQ_第12张图片

direct和headers是JMS的点对点通信的实现 tanout和topic是发布订阅的实现 headers性能比较低下不建议使用

direct和fanout和topic三个交换机,交换机类型不同,路由到地方就不一样

消息队列RabbitMQ_第13张图片

消息队列RabbitMQ_第14张图片

五、RabbitMQ的图形用户界面使用

添加用户

如果不使用guest,我们也可以直接创建一个用户;

消息队列RabbitMQ_第15张图片

1、 超级管理员(administrator)

可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

2、 监控者(monitoring)

可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

3、 策略制定者(policymaker)

可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

4、 普通管理者(management)

仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。

5、 其他

无法登陆管理控制台,通常就是普通的生产者和消费者。

创建一个新的交换机

消息队列RabbitMQ_第16张图片

image-20220530163601867

交换机必须与队列进行绑定才能工作

进入交换机可以看到

消息队列RabbitMQ_第17张图片

默认是没有任何绑定,我们也可以在里面进行消息传输

所以我们想要使用交换机首先绑定一个队列,绑定一个队列就要先创建一个队列

消息队列RabbitMQ_第18张图片

image-20220530163916975

创建好队列现在将交换机与队列进行绑定(交换机也可以根交换机绑定,也可以跟队列绑定)

消息队列RabbitMQ_第19张图片

进行绑定

消息队列RabbitMQ_第20张图片

接下来我们根据如下图来在RabbitMQ里面创建所有的交换机,队列,以及绑定好他们之间的关系,可以进行测试下各个关系都是怎么进行工作的

消息队列RabbitMQ_第21张图片

首先先创建四个队列以便测试

消息队列RabbitMQ_第22张图片

接着创建交换机image-20220530171015316

使用该交换机绑定四个队列

消息队列RabbitMQ_第23张图片

进入队列可以看到

消息队列RabbitMQ_第24张图片

消息ready为0,消息总量total为0,未回复消息unacked也为0

想要发消息,首先将消息发送给交换机

下面我们是我们直接交换机的演示,就是路由键精确写什么,我们就路由到哪里。direct exchange

消息队列RabbitMQ_第25张图片

发送消息指定我们的路由 根据绑定交换机队列指定的路由不一样而进行开发

消息队列RabbitMQ_第26张图片

对路由为hhxy.news的消息队列进行发送消息

消息队列RabbitMQ_第27张图片
就可以发现hhxy.news中确实有消息发送 ready表示有一个消息准备好了但还没接收

如果想要查看消息队列中的消息进入hhxy.news队列里面,再进入get Message中

消息队列RabbitMQ_第28张图片

Ack Mode回复模式 Nack message requeue true我们把消息拿来,不告诉RabbitMQ我收到消息了,RabbitMQ就会把消息重新存放到队列里面,让别人拿到。

消息队列RabbitMQ_第29张图片

接着我们发现看完消息后,消息还在所以我们换一种类型查看消息消息队列RabbitMQ_第30张图片

消息队列RabbitMQ_第31张图片

通过Automatic ack来获取消息

消息队列RabbitMQ_第32张图片

查看消息成功,当我们再次点击获取消息时会发现

image-20220530173029828

image-20220530173043232

就会发现消息已经不在队列中了

fanout exchange 广播型交换机

消息队列RabbitMQ_第33张图片

image-20220530180450965

同样我们为该交换器绑定消息队列

消息队列RabbitMQ_第34张图片

发送消息查看消息队列谁能接收到消息

消息队列RabbitMQ_第35张图片

可以发现所有的消息队列都获取到数据了

消息队列RabbitMQ_第36张图片

可以说我们发消息无论我们的路由键是什么,我们的消息队列都会有消息

就算我们发消息不写路由键,所有消息队列都会收到

消息队列RabbitMQ_第37张图片

Topic exchange 主题型交换机

首先我们创建一个交换机topic

根据hhxy.#,#.news,#.emps的路由键来绑定交换机

消息队列RabbitMQ_第38张图片

#就代表可以有单词,可以没有 *就代表必须有

现在开始发送消息 观察具体哪一个消息队列获取到消息

假设我们发送一个路由键为hhxy.news的消息

消息队列RabbitMQ_第39张图片

消息队列为1的,当我发送了消息就加1如下图

消息队列RabbitMQ_第40张图片

因为hhxy.news符合路由键中四个要求所以所有的都会加1

我们再发送一个消息这次以.news结尾的路由键 hello.news

消息队列RabbitMQ_第41张图片

根据情况推论,hello.news只符合以上四种路由键中hhxyxueyuan.news的路由键*.news,所以应该只要hhxyxueyuan.news的消息队列会加1,我们测试一下结果如下

消息队列RabbitMQ_第42张图片

果然如同我们猜想,hhxyxueyuan.news消息队列加1了

可以看出topic exchange相当于模糊匹配

五、RabbitMQ整合

1,引入spring-boot-starter-amqp

2,application.yml配置

3,测试RabbitMQ

​ 1,AmqpAdmin:管理组件

​ 2,RabbitTemplate:消息发送处理组件

/**
 * 使用RabbitMQ
 * 1,引入了amqp场景 RabbitAutoConfiguration就会自动生效
 * 2,给容器中自动配置了
 *   RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate
 *   所有的属性都是spring.rabbitmq
 *   @ConfigurationProperties(prefix = "spring.rabbitmq")
 *   public class RabbitProperties
 *
 *
 * 3,给配置文件中配置spring.rabbitmq信息
 * 4,@EnableRabbit:@EnableXxxxx  开启功能
 * 5,监听消息:使用@RabbitListener:必须有@EnableRabbit
 * @RabbitListener:类+方法上(监听哪些队列即可)
 * RabbitHandler:标在方法上(重载区分不同的消息)
 *
 */

给RabbitMQ放一些连接工厂,从里面获取连接

消息队列RabbitMQ_第43张图片

放入组件

消息队列RabbitMQ_第44张图片

消息队列RabbitMQ_第45张图片

消息队列RabbitMQ_第46张图片

配置application.yml可以查看

RabbitAutoConfiguration的代码中 RabbitProperties

消息队列RabbitMQ_第47张图片

配置文件我们写在application.properties中

spring.rabbitmq.host=192.168.172.128
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/

其他的暂时不写,因为RabbitProperties.class中默认定义了很多

消息队列RabbitMQ_第48张图片

现在开始测试使用代码来创建exchange,queue,binding

启动类上要添加注解@EnableRabbit

/**
 * 使用RabbitMQ
 * 1,引入了amqp场景 RabbitAutoConfiguration就会自动生效
 *
 * 2,给容器中自动配置了
 *   RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate
 *   所有的属性都是spring.rabbitmq
 *   @ConfigurationProperties(prefix = "spring.rabbitmq")
 *   public class RabbitProperties
 *
 *
 * 3,给配置文件中配置spring.rabbitmq信息
 * 4,@EnableRabbit:@EnableXxxxx  开启功能
 *
 */
@EnableRabbit
@EnableDiscoveryClient
@SpringBootApplication
public class MallOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallOrderApplication.class,args);
    }
}
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MallOrderApplication {

    @Resource
    AmqpAdmin amqpAdmin;

    /**
     *1,如何创建exchange[hello.java.exchange],queue,binding
     *  1)、使用AmqpAdmin进行创建  管理组件,帮我们进行创建队列,绑定关系,销毁这些队列,后台的增删改查都能使用
     *2,如何收发消息
     *
     */
    //创建出exchange
    @Test
    public void createExchange(){
//        public DirectExchange(String name, boolean durable, boolean autoDelete, Map arguments)
        //三个参数:exchange的名字,是否持久化true,是否自动删除false
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("exchang[{}]创建成功","hello-java-exchange");
    }
}

消息队列RabbitMQ_第49张图片

接着我们创建出队列

  /**
     * Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments)
     * 姓名  是否持久化  是否排它  是否自动删除
     * 排它如果是true,这个排它只能被声明的连接使用 只要有一条连接连上我们队列,是我们声明的这个连接,别人就连不上我们这个队列,实际开发中我们都不应该是排它,我们让所有人都能连接到队列,谁能接到消息可能就一个人能接到消息
     */
    @Test
    public void createQueue(){
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        log.info("queue[{}]创建成功","hello-java-queue");
    }

image-20220531094228734

image-20220531094350065

再将交换机与队列进行绑定

 /**
     * Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map arguments)
     * 目的地  目的地的类型  交换机 路由键  自定义参数
     * 将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定路由键
     */
    @Test
    public void binding(){
        Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);
        amqpAdmin.declareBinding(binding);
        log.info("binding[{}]创建成功","hello-java-binding");
    }

image-20220531095155424

image-20220531095209336

接下来我们来测试发送消息

    @Resource
    RabbitTemplate rabbitTemplate;
//RabbitTemplate可以用来收发消息
    @Test
    public void sendMessage(){
        //1,发送消息
        String msg="hello world";
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",msg);
        //交换机 路由键 发送的消息
        log.info("消息发送完成{}",msg);
    }

image-20220531100004010

image-20220531100014392

我们将消息取出方便接下来测试 hello-java-queue已经清空为0

image-20220531100448924

我们尝试传输一个对象(发送一个对象)

先新建一个对象

package com.hhxy.mall.order;

import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

@ToString
@Data
public class giao implements Serializable {
    private Long id;
    private String name;
    private Integer age;
}
   @Resource
    RabbitTemplate rabbitTemplate;
    @Test
    public void sendMessage(){
        giao giao = new giao();
        giao.setId(1L);
        giao.setName("giao");
        giao.setAge(18);
        //1,发送消息
//        String msg="hello world";
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",giao);
        log.info("消息发送完成{}",giao);
    }

image-20220531100749192

image-20220531100758522

我们进入图形用户界面获取下消息
消息队列RabbitMQ_第50张图片

可以得知该消息是通过Java序列化得到的内容,要想看到真正的内容还得对内容进行反序列化

且发送对象必须对对象进行序列化,因为传输对象使用的序列化机制

我们发送的对象类型的消息,可以是一个json 在RabbitAutoConfiguration中RabbitTemplate中有消息转换器MessageConverter

消息队列RabbitMQ_第51张图片

image-20220531102541250

消息转换器messageConverter在RabbitTemplateConfiguration构造的时候我们就会拿到所有的消息转换器

如果容器中有messageConverter,就用容器中的,就把messageConverter放到RabbitTemplate

如果容器中没有RabbitTemplate则使用SimpleMessageConverter

image-20220531102936350

SimpleMessageConverter就是使用我们序列化机制 它是通过fromMessage进行消息转换

消息队列RabbitMQ_第52张图片

SimpleMessageConverter中还有创建消息createMessage

消息队列RabbitMQ_第53张图片

如果是string就直接做,如果是实现了序列化接口,它就使用序列化工具将它转换成一个byte[]数组

可以看到是messageConverter在起作用,如果我们想要变换messageConverter消息转换策略,我们就一起看看messageConverter内

messageConverter是一个接口我们看看方法内部

消息队列RabbitMQ_第54张图片

要想以json的形式传输我们就要在容器中加一个Jackson2JsonMessageConverter(给容器中放一个消息转换器)

package com.hhxy.mall.order.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter(){
       return new Jackson2JsonMessageConverter();
    }
}

添加了Jackson2JsonMessageConverter我们在测试下发送的对象的消息,获取出来是什么类型

消息队列RabbitMQ_第55张图片

在消息属性可以看到内容类型是json,而且在properties中还有type,那个实体类对象使用json形式传输

接下来我们测试下接收消息 我们想要接收某个队列的内容,就需要先监听这个队列

想要监听队列spring为我们抽取了个注解RabbitListener,它的作用就是监听我们指定的队列

其中方法:queues()可以指定我们想要监听的队列,只要这个队列有内容,我们就可以收到内容,而且该队列必须存在

如果我们只是测试收发消息,发消息之类的可以不用监听消息,我们可以不开启@EnableRabbit注解,如果想要监听消息就一定要先开启@EnableRabbit注解

接下来测试:

监听hello-java-queue这个队列的消息

现在随便进入一个业务逻辑获取写一个方法

  /**
     * queue:声明需要监听的所有队列
     */
    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Object message){
        System.out.println("接收到消息。。。。内容:"+message+"=====>消息类型:"+message.getClass());
    }

接着我们再发送一次消息可以发现服务接收到消息

image-20220531112945240

接收到消息。。。。内容:(Body:'{"id":1,"name":"giao","age":18}' MessageProperties [headers={__TypeId__=com.hhxy.mall.order.giao}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-KustLHtvD-P25mE8sGmu6g, consumerQueue=hello-java-queue])=====>消息类型:class org.springframework.amqp.core.Message

换一种参数写法

 /**
     * queue:声明需要监听的所有队列
     *
     * class org.springframework.amqp.core.Message
     *
     * 参数类型可以写以下类型
     * 1,Message message:原生消息详细信息。头+体
     * 2,T<发送消息的类型>Giao giao
     */
    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Message message, Giao content){
        byte[] body = message.getBody();
        //'{"id":1,"name":"giao","age":18}'
        MessageProperties messageProperties = message.getMessageProperties();
        //消息头属性信息
        System.out.println("接收到消息。。。。内容:"+message+"=====>内容:"+content);
    }
接收到消息。。。。内容:(Body:'{"id":1,"name":"giao","age":18}' MessageProperties [headers={__TypeId__=com.hhxy.mall.order.Giao}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-6bu-qmHY7lcoqBMrSqzTqQ, consumerQueue=hello-java-queue])=====>内容:Giao(id=1, name=giao, age=18)

image-20220531161216626

一个客户端只会建立一个连接 所有的数据都在通道里传输,所以我们可以获取到通道

   /**
     * queue:声明需要监听的所有队列
     *
     * class org.springframework.amqp.core.Message
     *
     * 参数类型可以写以下类型
     * 1,Message message:原生消息详细信息。头+体
     * 2,T<发送消息的类型>Giao giao
     * 3.Channel channel:当前传输数据的通道
     */
    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Message message, Giao content, Channel channel){
        byte[] body = message.getBody();
        //'{"id":1,"name":"giao","age":18}'
        MessageProperties messageProperties = message.getMessageProperties();
        //消息头属性信息
        System.out.println("接收到消息。。。。内容:"+message+"=====>内容:"+content);
    }
 * queue可以很多人来监听。只要收到消息,队列就会删除消息,而且只能有同一个收到此消息
     * 场景:
     *   1,订单服务启动多个

1,订单服务启动多个

当有多个订单服务监听这个消息队列时,获取消息到底是所有都获得还是只有一个呢?

改造发送消息的单元测试方法 for循环发送十次消息

消息队列RabbitMQ_第56张图片

查看接收消息的情况

image-20220531162425271

image-20220531162445447

虽然两个客户端都能接收到消息,但是可以发现同一个消息只能被一个客户端接收

为什么发现有几个消息没有被客户端接收,这三个消息并没有丢失,是因为单元测试中接收了三个消息

image-20220531163111895

2,只有一个消息完全处理完,方法运行结束,我们就可以接受到下一个消息

消息队列RabbitMQ_第57张图片

发送消息代码如上发送十条消息

客户端接收消息代码如下

 @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Message message, Giao content, Channel channel) throws InterruptedException {
        System.out.println("接收到消息。。。。"+content);
        byte[] body = message.getBody();
        //'{"id":1,"name":"giao","age":18}'
        MessageProperties messageProperties = message.getMessageProperties();
        Thread.sleep(3000);
        //消息头属性信息
        System.out.println("消息处理完成=》"+content.getName());
    }

了解RabbitListener和RabbitHandler

RabbitListener可以标记在类+方法上

RabbitHandler可以标记在方法上

消息队列RabbitMQ_第58张图片

消息队列RabbitMQ_第59张图片

二者之间区别 接收不同对象的场景下可以使用RabbitHandler重载处理

消息队列RabbitMQ_第60张图片

消息队列RabbitMQ_第61张图片

消息队列RabbitMQ_第62张图片

发现单元测试总是接收一部分消息不方便测试,编写一个控制类来发送消息 接着观察接收消息的对象

@RestController
public class RabbitController {

    @Resource
    RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMq")
    public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
        for (int i = 0; i <10 ; i++) {
            if (i%2==0){
                OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
                orderReturnApplyEntity.setId(1L);
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity);
            }else{
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderEntity);
            }
        }
        return "ok";
    }
}

@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {

    /**
     * queue:声明需要监听的所有队列
     *
     * class org.springframework.amqp.core.Message
     *
     * 参数类型可以写以下类型
     * 1,Message message:原生消息详细信息。头+体
     * 2,T<发送消息的类型>Giao giao
     * 3.Channel channel:当前传输数据的通道
     *
     * queue可以很多人来监听。只要收到消息,队列就会删除消息,而且只能有同一个收到此消息
     * 场景:
     *   1,订单服务启动多个
     *   2,只有一个消息完全处理完,方法运行结束,我们就可以接受到下一个消息
     */
//    @RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void recieveMessage(Message message, OrderReturnApplyEntity orderReturnApplyEntity, Channel channel) throws InterruptedException {
        System.out.println("接收到消息。。。。"+orderReturnApplyEntity);
        byte[] body = message.getBody();
        //'{"id":1,"name":"giao","age":18}'
        MessageProperties messageProperties = message.getMessageProperties();
//        Thread.sleep(3000);
        //消息头属性信息
        System.out.println("消息处理完成=》"+orderReturnApplyEntity.getId());
    }

    @RabbitHandler
    public void recieveMessage2(OrderEntity orderEntity) throws InterruptedException {
        System.out.println("接收到消息。。。。"+orderEntity);
}
}

消息队列RabbitMQ_第63张图片

所以我们可以使用RabbitHandler来区分不同消息

六、RabbitMQ消息确认机制-可靠抵达

  • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制(我们可以采用Acknowledgements and Confirms消息确认机制,它可以很高性能的保证消息可靠抵达)

  • publisher confirmCallback确认模式

  • publisher returnCallback未投递到queue退回模式

  • consumer ack机制

    消息队列RabbitMQ_第64张图片

详细可以参考 rabbitmq官方文档

Acknowledgements and Confirms有Publisher confirms(发送者确认)如果使用标准的AMQP协议想要保证消息不丢失,我们就可以采用事务,事务就是让通道进行事务化,每一个消息期间它的发布投递都是一个完整的事务,只要事务成功了才会完成。这个示例里面事务可能减少我们的吞吐量大概250倍。如果想要高性能的可靠投递,我们就应该引入Acknowledgements以及Publisher confirms(生成端确认机制)。

可靠抵达-ConfirmCallback

  • spring.rabbitmq.publisher-confirms=true

    -在创建connectionFactory的时候设置Publisherconfirms(true)选项,开启confirmcallback.

    -CorrelationData:用来表示当前消息唯一性

    -消息只要被broker接收到就会执行confirmCallback,如果时cluster模式,需要所有broker接收到才会调用confirmCallback

    -被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到目标queue里。所以需要用到接下来的returnCallback。

    ConfirmCallback其实是在RabbitTemplate中的

    我们只要配置好确认回调,那些消息发送完成就会回调进来,会有几个参数:

    • correlationData消息的关联标识 也就是消息的唯一id
    • ack 表示消息有没有被正确收到 true就是正确收到,false就反之
    • cause 如果没有正确收到,出现了什么异常 异常的各种原因

    如果我们想使用ConfirmCallback那就可以定制RabbitTemplate

    首先先在配置文件application.properties先加上

    #开启发送端确认
    spring.rabbitmq.publisher-confirms=true
    

    其次在MyRabbitConfig中加上

      /**
         * 定制RabbitTemplate
         * 1,#开启发送端确认 spring.rabbitmq.publisher-confirms=true
         * 2,设置确认回调
         */
        @PostConstruct  //MyRabbitConfig对象创建完成以后,执行该方法
        public void initRabbitTemplate() {
            //设置确认回调
            template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    
                @Override
                public void confirm(CorrelationData correlationData, boolean b, String s) {
                    System.out.println("confirm...correlationData["+correlationData+"]===>ack["+b+"]===>cause["+s+"]");
    
                }
            });
        }
    

    重新启动服务器查看回调是否被触发

    消息队列RabbitMQ_第65张图片

可以发现消息除过自己接到外,这些confirm回调都触发了,只要ack为true就说明我们的消息被服务端收到了,消息有没有被消费跟我们这个truefalse有没有关系,我们也可以测试下

将实现类中监听queue的@RabbitListener注解注释掉 @RabbitHandler也注释

重新启动项目会发现依然是true 说明只要消息到达服务器那么我们的回调就为true

image-20220531212653359

可靠抵达-ReturnCallback

  • spring.rabbitmq.publisher-returns=true
  • spring.rabbitmq.template.mandatory=true

​ -confirm模式只能保证信息到达broker,不能保证消息准确投递到目标queue里。在有些业务场景下,我们需要保证消息一定投递到目标queue里,此时就需要用到return退回模式。

​ -这样如果未能投递到目标queue里将调用returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。

在配置文件中添加

#开启发送消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnconfirms
spring.rabbitmq.template.mandatory=true
    //设置消息抵达队列的确认回调
        template.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * 只要消息没有投递给指定的队列,就会触发这个失败的回调
             * @param message 投递失败的消息详细信息
             * @param i replayCode 回复的状态码
             * @param s replayText 回复的文本内容
             * @param s1 exchange 当时这个消息发给那个交换机
             * @param s2 当时这个消息用哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
                System.out.println("Fail Message["+message+"]==>replyCode["+i+"]==>exchange["+s1+"]==>routingKey["+s2+"]");
            }
        });

重启服务,查看结果

消息队列RabbitMQ_第66张图片

发现消息中并没有失败的调用 只有失败的时候才会打印ReturnCallback

我们改造一下controller中发的路由键把它改成错的试试 重新启动下项目

image-20220531224139065

可以发现出现了错误回调
消息队列RabbitMQ_第67张图片

接着再查看服务端发送错误回调的文本信息 修改一条代码

System.out.println("Fail Message["+message+"]==>replyCode["+i+"]==>replayText["+s+"]==>exchange["+s1+"]==>routingKey["+s2+"]");

文本信息显示为NO_ROUTE 没有这个路由 312这个错误码对应的就是NO_ROUTE 没有此路由

消息队列RabbitMQ_第68张图片

注意消息成功发送到服务端confirm中CorrelationData中有一个唯一id,那么这个唯一id该从哪里来呢?

可以发现我们发消息的时候可以添加第四个参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fpagRVb-1654069318703)(C:\Users\靓仔在此\AppData\Roaming\Typora\typora-user-images\image-20220531225259535.png)]

第四个是参数应当是CorrelationData 所以我们放一个CorrealtionData

rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity,new CorrelationData(UUID.randomUUID().toString()));

CorrealtionData这个里面UUID.randomUUID().toString()作为唯一id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGPe1fdW-1654069318704)(C:\Users\靓仔在此\AppData\Roaming\Typora\typora-user-images\image-20220531225726615.png)]

我们随便指定一个uuid,怎么知道服务端收到的是哪些消息呢

所以可以在发消息的时候,不仅把消息发给mq消息服务器外,还可以把消息保存到mysql,每一个唯一id对应此消息,如果我们服务端收到此消息,我们就在数据库里声明一下消息已经被收到了,哪些消息没收到,我们就去遍历数据库,看那些消息状态是没收到i状态 本地事务表的方式 来保证我们消息是可靠抵达。

添加完id我们进行测试 有唯一id

消息队列RabbitMQ_第69张图片

可靠抵达-Ack消息确认机制

  • 消费者获取到消息,成功处理,可以回复Ack给Broker

​ -basic.ack用于肯定确认;broker将移除此消息

​ -basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量

​ -basic.reject用于否定确认;同上,但不能批量

  • 默认自动ack,消息被消费者收到,就会从broker的queue中移除
  • queue无消费者,消息依然会被存储,直到消费者消费
  • 消费者收到消息,默认会自动ack。但是如果无法确认此消息是否处理完,或者成功处理。我们可以开启手动ack模式

​ -消息处理成功,ack(),接受下一个消息,此消息broker就会移除

​ -消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack

​ -消息一直没有调用ack/nack方法,broker认为此消息正被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人

    * 3,消费端确认(保证每一个消息被正确消费,此时才可以broker删除这个消息)
     * 1,默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
     *    问题:
     *    我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失;
     *    手动确认模式。只要我们没有明确告诉mq,货物被签收。没有Ack,
     *    消息就一直是unacked状态。即使Consumer宕机。消息也不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
     * 2,如何签收
     *

如何使用手动确认

先在配置文件中添加一行代码

#手动确认
spring.rabbitmq.listener.direct.acknowledge-mode=manual

一旦我们开启手动确认模式,只要我们不确认的消息,它就都不算成功处理

如何签收

可以查看通道中的方法channel.basicAck();

void basicAck(long var1, boolean var3) throws IOException;
var1-deliveryTag   var3-multiple
var1-当前消息的派发标签,是一个数字   var3-是不是批量确认
    
var1这个数字从哪里来呢
         //channel内按顺序自增
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

检验是否自增且消息是否被处理

消息队列RabbitMQ_第70张图片

但是我们并没有给服务器回复,所以所有消息还在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z8mA6qh3-1654069318704)(C:\Users\靓仔在此\AppData\Roaming\Typora\typora-user-images\image-20220601091120564.png)]

实现类中添加
  //签收货物,非批量模式
        try {
            channel.basicAck(deliveryTag,false);
            System.out.println("签收了货物"+deliveryTag);
        } catch (Exception e) {
            //网络中断
            e.printStackTrace();
        }
打个断点观察队列中的消息
    签收两个消息,再手动宕机了,查看队列中消息情况

消息队列RabbitMQ_第71张图片
按照逻辑是只有签收了的消息才会在消息队列中删除,但是现在发现手动宕机了,剩余的方法也继续执行了

接着我们为了验证逻辑给该代码块进行改造

//签收货物,非批量模式
        try {
            if (deliveryTag%2==0){
                channel.basicAck(deliveryTag,false);
                System.out.println("货物签收了"+deliveryTag);
            }else {
                System.out.println("货物没有被签收"+deliveryTag);
            }

        } catch (Exception e) {
            //网络中断
            e.printStackTrace();
        }
    }

消息队列RabbitMQ_第72张图片

可以发下如上图,1-5中只有2,4签收了,1.3.5没有被签收,所以消息队列中应该还有三个消息未被签收。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u9LPkqMb-1654069318705)(C:\Users\靓仔在此\AppData\Roaming\Typora\typora-user-images\image-20220601092822576.png)]

中断服务,查看下消息队列中消息的状态,状态由上图转变成下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wx47gefD-1654069318706)(C:\Users\靓仔在此\AppData\Roaming\Typora\typora-user-images\image-20220601093011244.png)]

手动确认中也含有拒绝策略

               //退货  requeue=false丢弃  requeue=true发回服务器,服务器重新入队
                //basicNack(long var1, boolean var3, boolean var4)
                //var1-deliveryTag var3-multiple var4-requeue重新入队
                   
                channel.basicNack();
                //basicReject(long var1, boolean var3)
//                channel.basicReject();

现在开始测试

        channel.basicNack(deliveryTag,false,true);

在这里插入图片描述

为什么变成0了呢,

消息队列RabbitMQ_第73张图片

被拒绝的消息重新放入队列,又因为%2,消息重新被签收了,消息队列中消息为0;

接下来我们在测试自动入队为false

        channel.basicNack(deliveryTag,false,false);

重新再发一次消息就会发现消息队列中

消息队列RabbitMQ_第74张图片

有些签收了有些没有签收,因为没签收不入队,所以消息队列中消息为0

在这里插入图片描述

你可能感兴趣的:(RabbitMQ,rabbitmq,java,分布式)