RabbitMQ学习笔记

1.MQ引言

1.1 什么是MQ

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

1.2 MQ有哪些

当今市面上有很多主流的消息中间件,如老牌的ActiveMQRabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。

1.3 不同MQ特点

  • 1.ActiveMQ
    ActiveMQ 是Apache出品,最流行的,能力强劲的开源消息总线。它是一个完全支持JMS规范的的消息中间件。丰富的API,多种集群架构模式让ActiveMQ在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
  • 2.Kafka
    Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
  • 3.RocketMQ
    RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
  • 4.RabbitMQ
    RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。

2.RabbitMQ 的引言

2.1 RabbitMQ

基于AMQP协议,erlang语言开发,是部署最广泛的开源消息中间件,是最受欢迎的开源消息中间件之一。

在这里插入图片描述
官网: https://www.rabbitmq.com/
官方教程: https://www.rabbitmq.com/#getstarted

AMQP 协议
AMQP(advanced message queuing protocol)在2003年时被提出,最早用于解决金融领域不同平台之间的消息传递交互问题。 顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。以下是AMQP协议模型:

在这里插入图片描述

2.2 RabbitMQ 的架构

2.2.1 RabbitMQ 架构图与概念

在这里插入图片描述
关键组成部分:

  • Message:它是由消息头和消息体组成,消息体是不透明的,消息头则由一系列可选属性组成,比如:routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
  • Publisher:是消息的生产者,也是一个向交换器发布消息的客户端应用程序。
  • Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
  • Binding:字面意思是:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
  • Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
  • Connection:publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
  • Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接,可见图中一个连接有多个虚拟信道。
  • Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
  • Broker:表示消息队列服务器实体,可理解为一个Rabbitmq Server的实体就是一个Broker。
  • Virtual Host:虚拟主机,表示为一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。一个Broker上可有多个Virtual Host,多个Virtual Host之间权限相互隔离。
  • Clustering:多个RabbitMQ服务器(Broker)可以组成一个集群,形成一个逻辑上的大Broker。

Exchange 四种分发消息类型:

  • direct:通过精确匹配route key,然后发送往匹配的对应队列。
  • topic:支持通配符的route key,匹配的route key 对应的队列都会收到消息。
  • fanout:就是广播形式,所有队列都收到消息,速度最快。
  • head:在消息头中带对应的属性x-form-forward,用的最少。

2.3 RabbitMQ 的安装

2.3.1 用Docker安装RabbitMQ

这里采用Docker安装RabbitMQ,主要是快捷省时,方便平时开发使用。

查询镜像

注意获取镜像的时候要获取management版本的,不要获取last版本的,management版本的才带有管理界面

docker search rabbitmq:management

在这里插入图片描述

获取镜像
docker pull rabbitmq:management

在这里插入图片描述

运行镜像

5672为服务端口,15672为界面管理端口,需要从docker容器中映射出到本机中

docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management

出现如下结果,则表示运行成功
在这里插入图片描述

启动rabbitmq_management插件
docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management

在这里插入图片描述

关闭防火墙服务(或打开相应端口)

很多教程都说要关闭防火墙,其实不用,只需要打开主机的相应端口,让别的主机能访问就行了,把防火墙关闭反而容易有安全问题。
例如:我的虚拟主机为Ubuntu,防火墙服务为ufw
Ubuntu用如下命令关闭防火墙(不安全)

sudo ufw disable
sudo ufw status

在这里插入图片描述
或打开防火墙,但暴露端口

sudo ufw enable
sudo ufw allow 5672
sudo ufw allow 15672
sudo ufw status

在这里插入图片描述
其它linux发行版本,怎么操作防火墙,自行百度即可

访问web管理界面
登录管理界面

http://172.16.102.18:15672/#/
(换成自己主机的IP)

	# 默认用户和密码
	username:  guest
	password:  guest

在这里插入图片描述


3. RabiitMQ 配置

3.1 RabbitMQ 管理命令行

# 如果要执行RabbitMQ命令,则需要先进入Docker容器
docker exec -it "容器ID" /bin/bash
# 进入后,其它命令使用方法一致
rabbitmqctl help

3.2 web管理界面介绍

3.2.1 overview概览

在这里插入图片描述

  • Overview:概览,对所有系统信息的一个总览
  • Connections:连接,无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
  • Channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
  • Exchanges:交换机,用来实现消息的路由
  • Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
  • Admin: 管理,对用户,虚拟机,特色功能,策略,限制,集群的管理
3.2.2 Admin用户和虚拟主机管理
1. 添加用户

在这里插入图片描述
上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

  • 超级管理员(administrator)
    可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
  • 监控者(monitoring)
    可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
  • 策略制定者(policymaker)
    可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
  • 普通管理者(management)
    仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
  • 其他
    无法登陆管理控制台,通常就是普通的生产者和消费者。

例如我创建的帐号为:

帐号:root
密码:123456
角色:Admin

注:为了安全问题,新注册一个Admin用户后,可把原guest帐号删除掉,点击名称,往下拉即可看到删除选项。

2. 创建虚拟主机

虚拟主机
为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。可以简单类比mysql数据库中的具体数据库概念,主要是为了方便区分各虚拟机。

在这里插入图片描述

3. 绑定虚拟主机和用户

创建好虚拟主机,我们还要给用户添加访问权限:
点击添加好的虚拟主机:
在这里插入图片描述
进入虚拟机设置界面:
添加新用户,权限默认就好
在这里插入图片描述
以下操作同理
在这里插入图片描述
绑定好后,我们的新用户,就可以操作新的虚拟机了
在这里插入图片描述


4.RabbitMQ 编程

4.1 RabbitMQ支持的消息模型

在这里插入图片描述

4.2 引入依赖

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

4.3 第一种模型(直连)

在这里插入图片描述
在上图的模型中,有以下概念:

  • P:生产者,也就是要发送消息的程序
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
1. 开发生产者
  //创建连接工厂
  ConnectionFactory connectionFactory = new ConnectionFactory();
  connectionFactory.setHost("172.16.102.18");
  connectionFactory.setPort(5672);
  connectionFactory.setUsername("ems");
  connectionFactory.setPassword("123456");
  connectionFactory.setVirtualHost("/ems");
  Connection connection = connectionFactory.newConnection();
  //创建通道
  Channel channel = connection.createChannel();
  //参数1: 是否持久化  参数2:是否独占队列 参数3:是否自动删除  参数4:其他属性
  channel.queueDeclare("hello",true,false,false,null);
  channel.basicPublish("","hello", null,"hello rabbitmq".getBytes());
  channel.close();
  connection.close();
2. 开发消费者
  //创建连接工厂
  ConnectionFactory connectionFactory = new ConnectionFactory();
  connectionFactory.setHost("172.16.102.18");
  connectionFactory.setPort(5672);
  connectionFactory.setUsername("ems");
  connectionFactory.setPassword("123456");
  connectionFactory.setVirtualHost("/ems");
  Connection connection = connectionFactory.newConnection();
  Channel channel = connection.createChannel();
  channel.queueDeclare("hello", true, false, false, null);
  channel.basicConsume("hello",true,new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
      System.out.println(new String(body));
    }
  });
3. 参数的说明
  channel.queueDeclare("hello",true,false,false,null);
  '参数1':用来声明通道对应的队列
  '参数2':用来指定是否持久化队列
  '参数3':用来指定是否独占队列
  '参数4':用来指定是否自动删除队列
  '参数5':对队列的额外配置

4.4 第二种模型(work quene)

Work queues,也被称为(Task queues),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
在这里插入图片描述
角色:

  • P:生产者:任务的发布者
  • C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
  • C2:消费者-2:领取任务并完成任务,假设完成速度快
1. 开发生产者
channel.queueDeclare("hello", true, false, false, null);
for (int i = 0; i < 10; i++) {
  channel.basicPublish("", "hello", null, (i+"====>:我是消息").getBytes());
}
2.开发消费者-1
channel.queueDeclare("hello",true,false,false,null);
channel.basicConsume("hello",true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});
3.开发消费者-2
channel.queueDeclare("hello",true,false,false,null);
channel.basicConsume("hello",true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    try {
      Thread.sleep(1000);   //处理消息比较慢 一秒处理一个消息
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("消费者2: "+new String(body));  
  }
});
4.测试结果

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

总结:默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。

5.消息自动确认机制

Doing a task can take a few seconds. You may wonder what happens if one of the consumers starts a long task and dies with it only partly done. With our current code, once RabbitMQ delivers a message to the consumer it immediately marks it for deletion. In this case, if you kill a worker we will lose the message it was just processing. We’ll also lose all the messages that were dispatched to this particular worker but were not yet handled.

But we don’t want to lose any tasks. If a worker dies, we’d like the task to be delivered to another worker.

channel.basicQos(1);//一次只接受一条未确认的消息
//参数2:关闭自动确认消息
channel.basicConsume("hello",false,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
    channel.basicAck(envelope.getDeliveryTag(),false);//手动确认消息
  }
});
  • 设置通道一次只能消费一个消息
  • 关闭消息的自动确认,开启手动确认消息
    在这里插入图片描述
    在这里插入图片描述

4.5 第三种模型(fanout)

fanout 扇出 也称为广播
在这里插入图片描述
在广播模式下,消息发送流程是这样的:

  • 可以有多个消费者
  • 每个消费者有自己的queue(队列)
  • 每个队列都要绑定到Exchange(交换机)
  • 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
  • 交换机把消息发送给绑定过的所有队列
  • 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
1. 开发生产者
//声明交换机
channel.exchangeDeclare("logs","fanout");//广播 一条消息多个消费者同时消费
//发布消息
channel.basicPublish("logs","",null,"hello".getBytes());
2. 开发消费者-1
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});
3. 开发消费者-2
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});
4.开发消费者-3
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者3: "+new String(body));
  }
});
5. 测试结果

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


4.6 第四种模型(Routing)

4.6.1 Routing 之订阅模型-Direct(直连)

在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey

  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
    流程:
    在这里插入图片描述
    图解:

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

  • C1:消费者,其所在队列指定了需要routing key 为 error 的消息

  • C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

1. 开发生产者
//声明交换机  参数1:交换机名称 参数2:交换机类型 基于指令的Routing key转发
channel.exchangeDeclare("logs_direct","direct");
String key = "";
//发布消息
channel.basicPublish("logs_direct",key,null,("指定的route key"+key+"的消息").getBytes());
2.开发消费者-1
 //声明交换机
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","warn");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});
3.开发消费者-2
//声明交换机
channel.exchangeDeclare("logs_direct","direct");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机
channel.queueBind(queue,"logs_direct","error");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});
4.测试生产者发送Route key为error的消息时

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

5.测试生产者发送Route key为info的消息时

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


4.6.2 Routing 之订阅模型-Topic

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!这种模型Routingkey 一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
在这里插入图片描述

#统配符:
	* (star) can substitute for exactly one word.    匹配不多不少恰好1个词
	# (hash) can substitute for zero or more words.  匹配一个或多个词
#如:
	audit.#    匹配audit.irs.corporate或者 audit.irs 等
    audit.*   只能匹配 audit.irs
1.开发生产者
//生命交换机和交换机类型 topic 使用动态路由(通配符方式)
channel.exchangeDeclare("topics","topic");
String routekey = "user.save";//动态路由key
//发布消息
channel.basicPublish("topics",routekey,null,("这是路由中的动态订阅模型,route key: ["+routekey+"]").getBytes());
2.开发消费者-1

Routing Key中使用*通配符方式

 //声明交换机
channel.exchangeDeclare("topics","topic");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列与交换机并设置获取交换机中动态路由
channel.queueBind(queue,"topics","user.*");

//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});
3.开发消费者-2

Routing Key中使用#通配符方式

//声明交换机
channel.exchangeDeclare("topics","topic");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列与交换机并设置获取交换机中动态路由
channel.queueBind(queue,"topics","user.#");

//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});
4.测试结果

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

4.7 第五种模型(RPC)

4.7.1 RPC 结构图与概念

如果需要在远程计算机上运行一个函数并等待结果,这种同步操作以上的模型怎么解决呢,其实不好解决,于是这里有一个新的模型来处理这种情况,这种模式通常被称为远程过程调用RPC
在这里插入图片描述
RPC工作模式如下:

  • 对于RPC请求,客户机发送带有两个属性的消息:replyTo,它被设置为仅为请求创建的匿名独占队列,correlationId,它被设置为每个请求的唯一值。
  • 请求被发送到rpc_queue队列。
  • RPC工作者(又名:Server)正在等待该队列上的请求。当请求出现时,它执行任务并使用replyTo字段中的队列将带有结果的消息发送回客户机。
  • 客户端在应答队列中等待数据。当消息出现时,它检查correlationId属性。如果它与请求中的值匹配,则将响应返回给应用程序。

4.8 第六种模型(Publisher Confirms)

4.8.1 Publisher Confirms

Publisher Confirm是RabbitMQ的一个扩展,用于实现可靠的发布。当在通道上启用了发布者确认时,客户机发布的消息将由代理异步确认,来确保已发布的消息已安全到达代理。用此扩展可以确定消息发布与确认的可靠性
Publisher Confirm是RabbitMQ对AMQP 0.9.1协议的扩展,所以默认情况下不启用。发布者确认在通道级别使用confirmSelect方法启用:

Channel channel = connection.createChannel();
channel.confirmSelect();

必须在希望使用Publisher Confirm的每个通道上调用此方法。确认应该只启用一次,而不是针对发布的每条消息。

常使用的Publisher Confirms策略

实现Publisher Confirms有多种策略,每种各有优缺点,可供选择:

  • 单独发布消息(Publishing Messages Individually):优点:简单。缺点:吞吐量小。
  • 批量发布消息(Publishing Messages in Batches):优点:吞吐量大。缺点:粒度大,不知错在哪一条消息。
  • 异步确认发布的消息(Handling Publisher Confirms Asynchronously):优点:吞吐量大。性能好。缺点:实现较复杂。

5. SpringBoot中使用RabbitMQ

5.1 搭建初始环境

1. 引入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 配置配置文件
spring:
  application:
    name: springboot_rabbitmq
  rabbitmq:
    host: 172.16.102.18
    port: 5672
    username: ems
    password: 123456
    virtual-host: /ems

RabbitTemplate 用来简化操作 使用时候直接在项目中注入即可使用

5.2 “hello world” 模型使用

  1. 开发生产者
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void testHello(){
      rabbitTemplate.convertAndSend("hello","hello world");
    }
    
  2. 开发消费者
    @Component
    @RabbitListener(queuesToDeclare = @Queue("hello"))
    public class HelloCustomer {
    
        @RabbitHandler
        public void receive1(String message){
            System.out.println("message = " + message);
        }
    }
    

5.3 Work queues模型使用

  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这种方式就是公平调度,如果需要实现能者多劳需要额外配置

5.4 Publish/Subscribe(Fanout )广播模型

  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);
        }
    }
    

5.5 Route(Routing)路由模型

  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);
        }
    }
    
    

5.6 Topic 订阅模型(动态路由模型)

  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);
        }
    }
    

6. MQ的应用场景

6.1 异步处理

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

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

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

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

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

6.2 应用解耦

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
在这里插入图片描述

这种做法有一个缺点:
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
在这里插入图片描述

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
  • 库存系统:订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.

6.3 流量削峰

场景: 秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
​ 1.可以控制活动人数,超过此一定阀值的订单直接丢弃
​ 2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
在这里插入图片描述
1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.
2.秒杀业务根据消息队列中的请求信息,再做后续处理.


7. RabbitMQ的集群

7.1 集群架构

7.1.1 普通集群(副本集群)

All data/state required for the operation of a RabbitMQ broker is replicated across all nodes. An exception to this are message queues, which by default reside on one node, though they are visible and reachable from all nodes. To replicate queues across nodes in a cluster

默认情况下:RabbitMQ代理操作所需的所有数据/状态都将跨所有节点复制。这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问

  1. 架构图

在这里插入图片描述
核心解决问题: 当集群中某一时刻master节点宕机,可以对Quene中信息,进行备份

  1. 集群搭建
    # 0.集群规划
    	node1: 10.15.0.3  mq1  master 主节点
    	node2: 10.15.0.4  mq2  repl1  副本节点
    	node3: 10.15.0.5  mq3  repl2  副本节点
    
    # 1.克隆三台机器主机名和ip映射
    	vim /etc/hosts加入:
    		10.15.0.3 mq1
        	10.15.0.4 mq2
        	10.15.0.5 mq3
    	node1: vim /etc/hostname 加入:  mq1
    	node2: vim /etc/hostname 加入:  mq2
    	node3: vim /etc/hostname 加入:  mq3
    
    # 2.三个机器安装rabbitmq,并同步cookie文件,在node1上执行:
    	scp /var/lib/rabbitmq/.erlang.cookie root@mq2:/var/lib/rabbitmq/
    	scp /var/lib/rabbitmq/.erlang.cookie root@mq3:/var/lib/rabbitmq/
    
    # 3.查看cookie是否一致:
    	node1: cat /var/lib/rabbitmq/.erlang.cookie 
    	node2: cat /var/lib/rabbitmq/.erlang.cookie 
    	node3: cat /var/lib/rabbitmq/.erlang.cookie 
    
    # 4.后台启动rabbitmq所有节点执行如下命令,启动成功访问管理界面:
    	rabbitmq-server -detached 
    
    # 5.在node2和node3执行加入集群命令:
    	1.关闭       rabbitmqctl stop_app
    	2.加入集群    rabbitmqctl join_cluster rabbit@mq1
    	3.启动服务    rabbitmqctl start_app
    
    # 6.查看集群状态,任意节点执行:
    	rabbitmqctl cluster_status
    
    # 7.如果出现如下显示,集群搭建成功:
    	Cluster status of node rabbit@mq3 ...
    	[{nodes,[{disc,[rabbit@mq1,rabbit@mq2,rabbit@mq3]}]},
    	{running_nodes,[rabbit@mq1,rabbit@mq2,rabbit@mq3]},
    	{cluster_name,<<"rabbit@mq1">>},
    	{partitions,[]},
    	{alarms,[{rabbit@mq1,[]},{rabbit@mq2,[]},{rabbit@mq3,[]}]}]
    
    # 8.登录管理界面,展示如下状态:
    

在这里插入图片描述

# 9.测试集群在node1上,创建队列

在这里插入图片描述

# 10.查看node2和node3节点:

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

# 11.关闭node1节点,执行如下命令,查看node2和node3:
	rabbitmqctl stop_app

在这里插入图片描述

7.1.2 镜像集群

By default, contents of a queue within a RabbitMQ cluster are located on a single node (the node on which the queue was declared). This is in contrast to exchanges and bindings, which can always be considered to be on all nodes. Queues can optionally be made mirrored across multiple nodes.

镜像队列机制就是将队列在三个节点之间设置主从关系,所有消息信息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升了MQ集群的整体高可用性。

1.集群架构图

在这里插入图片描述

2.配置集群架构
# 0.策略说明
	rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern>  <definition>
	-p Vhost: 可选参数,针对指定vhost下的queue进行设置
	Name:     policy的名称
	Pattern: queue的匹配模式(正则表达式)
	Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
           		ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes
                        all:表示在集群中所有的节点上进行镜像
                        exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
                        nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
            	 ha-params:ha-mode模式需要用到的参数
                ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual
                priority:可选参数,policy的优先级
                
                 
# 1.查看当前策略
	rabbitmqctl list_policies

# 2.添加策略
	rabbitmqctl set_policy ha-all '^hello' '{"ha-mode":"all","ha-sync-mode":"automatic"}' 
	说明:策略正则表达式为 “^” 表示所有匹配所有队列名称  ^hello:匹配hello开头队列

# 3.删除策略
	rabbitmqctl clear_policy ha-all

# 4.测试集群

8. RabiitMQ 的高级特性

8.1 如何保证消息100%投递成功

8.1.1 什么是生产端的可靠性投递?
  • 保障消息的成功发出
  • 保障MQ节点的成功接收
  • 发送端收到MQ节点(Broker)确认应答
  • 完善的消息进行补偿机制

前三步不一定能保障消息能够100%投递成功。因此要加上第四步,故有两种解决方案:

  • 一、消息落库,对消息状态进行打标
  • 二、消息的延迟投递,做二次确认,回调检查

具体采用哪种方案,还需要根据业务与消息的并发量而定。

8.1.2 消息落库,对消息状态进行打标

  在发送消息的时候,需要将消息持久化到数据库中,并给这个消息设置一个状态(未发送、发送中、到达)。当消息状态发生了变化,需要对消息做一个变更。针对没有到达的消息做一个轮训操作,重新发送。对轮训次数也需要做一个限制3-5次。确保消息能够成功的发送。
在这里插入图片描述
图解:
蓝色部分表示:生产者负责发送消息发送至Broker端
Biz DB:订单数据库 MSG DB: 消息数据
面对小规模的应用可以采用加事务的方式,保证事务的一致性。但在大厂中面对高并发,并没有加事务,事务的性能拼接非常严重,而是做补偿。
例如:

  • step1:存储订单消息(创建订单),业务数据入库,消息也入库。缺点:需要持久化两次。(status:0)
  • step2:在step1成功的前提下,发送消息
  • step3:Broker收到消息后,confirm给我们的生产端。Confirm Listener异步监听Broker回送的消息。
  • step4:抓取出指定的消息,更新(status=1),表示消息已经投递成功。
  • step5:分布式定时任务获取消息状态,如果等于0则抓取数据出来。
  • step6:重新发送消息
  • step7:重试限制设置3次。如果消息重试了3次还是失败,那么(status=2),认为这个消息就是失败的。

查询这些消息为什么失败,可能需要人工去查询。以上方案有个问题,对数据有两次入库,一次业务数据入库,一次消息入库。这样对数据的入库是一个瓶颈。在高并发下,其实并不适合

8.1.3 消息的延迟投递,做二次确认,回调检查

这种方式并不一定能保证100%成功,但是也能保证99.99%的消息成功。如果遇到特别极端的情况,那么就只能需要人工去补偿,或者定时任务去做。
在这里插入图片描述
图解:
Upstream service:生产端
DownStream service:消费端
Callback service:回调服务

  • step1:业务消息入库成功后,第一次消息发送。
  • step2:同样在消息入库成功后,发送第二次消息,这两条消息是同时发送的。第二条消息是延迟检查,可以设置2min、5min 延迟发送。
  • step3:消费端监听指定队列。
  • step4:消费端处理完消息后,内部生成新的消息send confirm。投递到MQ Broker。
  • step5: Callback Service 回调服务监听MQ Broker,如果收到Downstream service发送的消息,则可以确定消息发送成功,执行消息存储到MSG DB。
  • step6:Check Detail检查监听step2延迟投递的消息。此时两个监听的队列不是同一个,5分钟后,Callback service收到消息,检查MSG DB。如果发现之前的消息已经投递成功,则不需要做其他事情。如果检查发现失败,则Callback 进行补偿,主动发送RPC 通信。通知上游生产端重新发送消息。

这样做的目的:少做了一次DB存储。关注点并不是百分百的投递成功,而是性能。

8.2 幂等性与重复消费问题

8.2.1 幂等性是什么?

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中,即f(f(x)) = f(x)。简单的来说就是一个操作多次执行产生的结果与一次执行产生的结果一致

  • 我们可以借鉴数据库的乐观锁机制:
  • 比如我们执行一条更新库存的SQL语句:
  • UPDATE T_REPS SET COUNT = COUNT - 1,VERSION = VERSION + 1 WHERE VERSION = 1
8.2.2 消费端-幂等性保障

在高并发的情况下,会有大量的消息到达MQ,消费端需要监听大量的消息。这样的情况下,难免会出现消息的重复投递,网络闪断等等。如果不去做幂等,则会出现消息的重复消费。消费端实现幂等性,就意味着,我们的消息永远不会被消费多次,即使我们收到了多条一样的消息,也只会执行一次。
那么怎么在消费端避免重复消费呢:主要有以下两个方案:

  • 唯一ID+指纹吗机制,利用数据库主键去重。
  • 利用Redis的原子性实现
8.2.3 唯一ID+指纹码机制
  • 唯一ID + 指纹吗机制,利用数据库主键去重。
    保证唯一性
  • SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码
    如果查询没有,则添加。有则不需要做任何操作,消费端不需要消费消息。
  • 好处:实现简单
  • 坏处:高并发下有数据库写入的性能瓶颈
  • 解决方案:跟进ID进行分库分表进行算法路由分摊流量压力。
8.2.4 Redis 原子特性实现

最简单的是使用Redis储存已消费过的消息,如果消息已存在,则不再消费。但其实这里还是有两个问题:

  • 我们是否需要数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
  • 如果不进行落库,那么都存储到缓存中,如何设置定时同步的策略?还有最重要的是怎么保证数据的稳定性?

8.3 Confirm确认消息、Return返回消息

8.3.1 理解Confirm 消息确认机制:
  • 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答
  • 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障!
    在这里插入图片描述
    生产者把消息发送到Broker端,Broker收到消息之后回送给producer。Confirm Listener 监听应答。
    操作是异步操作,当生产者发送完消息之后,就不需要管了。Confirm Listener 监听MQ Broker的应答。
8.3.2 如何实现Confirm确认消息?
  • 在channel上开启确认模式:channel.confirmSelect()
  • 在chanel上 添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!
8.3.3 理解Return消息确认机制:
  • Return Listener用于处理一些不可路由的消息!
  • 我们的消息生产者,通过指定一个Exchange和Routingkey,把消息送达到某一个队列中去,然后我们的消费者监听队列,进行消费处理操作!
  • 但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用Return Listener!

在基础API中有一个关键的配置项:

  • Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息!
    在这里插入图片描述
    Producer生产端将消息发送到MQ Broker端,但是出现NotFind Exchange,发送的消息的Exchange,在Broker端未能找到。或者找到了,但是路由key路由不到指定的队列。因此是一个错误的消息。
    这个时候,生产端应该知道发送的这条消息,并不会被处理。因此MQ Broker提供了这种Return机制,将这些不可达的消息发送给生产端,这时候生产端就需要设置Return Listener去接收这些不可达的消息。然后及时记录日志,去处理这些消息。

8.4 自定义消费者

8.4.1 消费端自定义监听

我们一般就在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理!但是这种轮训的方式肯定是不好的,代码也比较low

  • 我们使用自定义的Consumer更加的方便,解耦性更加的强,也是在实际工作中最常见的使用方式!
public class MyConsumer extends DefaultConsumer {
	public MyConsumer(Channel channel) {
		super(channel);
	}
	//根据需求,重写自己需要的方法。
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
	// ... ...
	}
}

8.5 消费端限流

8.5.1 什么是消费端的限流?
  • 假设一个场景,首先,我们Rabbitmq服务器有上万条未处理的消息,我们随便打开一个消费者客户端,会出现下面情况:巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!这个时候很容易导致服务器崩溃,出现故障。
8.5.2 为什么不在生产端进行限流呢?

因为在高并发的情况下,客户量就是非常大,所以很难在生产端做限制。因此我们可以用MQ在消费端做限流。

  • RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。(在限流的情况下,千万不要设置自动签收,要设置为手动签收。)
  • void BasicQos(uint prfetchSize,ushort prefetchCount,bool global);
    prefetchSize:0
  • prefetchCount:会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack。
  • global: true\false 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别还是consumer级别。
  • prefetchSize和global这两项,rabbitmq没有实现,暂且不研究prefetch_count在no_ask = false的情况下生效,即在自动应答的情况下这两个值是不生效的。

第一个参数:消息的限制大小,消息多少兆。一般不做限制,设置为0
第二个参数:一次最多处理多少条,实际工作中设置为1就好
第三个参数:限流策略在什么上应用。在RabbitMQ一般有两个应用级别:1.通道 2.Consumer级别。一般设置为false,true 表示channel级别,false表示在consumer级别

8.6 消息的ACK与重回队列

8.6.1 消费端的手工ACK和NACK

消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!如果由于服务器宕机等严重问题,那我们就需要手工进行ACK保障消费端消费成功!

8.6.2 消费端的重回队列

消费端重回队列是为了对没有处理成功的消息,把消息重新传递给Broker! 一般我们在实际应用中,都会关闭重回队列,也就是设置为False.

注意:
可以会看到重回队列会出现重复消费导致死循环的问题,这时候最好设置重试次数,比如超过三次后,如果重试消息还是消费失败,就将消息丢弃。

8.7 TTL队列/消息

8.7.1 TTL概念
  • TTL是Time To Live的缩写,也就是生存时间
  • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
  • RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除

8.8 死信队列

8.8.1 概念理解

死信队列:DLXDead-Letter-Exchange,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX,RabbitMQ的死信队里与Exchange息息相关,消息变成死信有以下几种情况:

  • 消息被拒绝(basic.reject/basic.nack)并且requeue=false
  • 消息TTL过期
  • 队列达到最大长度

DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。可以监听这个队列中消息做相应的处理,这个特征可以弥补RabbitMQ3.0以前支持的immediate参数的功能。


9. 面试题

9.1 面试题与答案

9.1.1 使用RabbitMQ有什么好处?

1.解耦:系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!
2.异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度
3.削峰:并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常,此时MQ可做限流

9.1.2 RabbitMQ 中的 broker 是指什么?cluster 又是指什么?

Broker表示消息队列服务器实体,可理解为一个Rabbitmq Server的实体就是一个Broker。多个RabbitMQ服务器(Broker)可以组成一个集群,形成一个逻辑上的大Broker就是Clustering

9.1.3 RabbitMQ 概念里的 queue、channel 和 exchange 是逻辑概念,还是对应着进程实体?分别起什么作用?

Queue 具有自己的 erlang 进程;exchange 内部实现为保存 binding 关系的查找表;channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue 。由 AMQP 协议描述可知,channel 是真实 TCP 连接之上的虚拟连接,所有 AMQP 命令都是通过 channel 发送的,且每一个 channel 有唯一的 ID。一个 channel 只能被单独一个操作系统线程使用,故投递到特定 channel 上的 message 是有顺序的。但一个操作系统线程上允许使用多个 channel 。

9.1.4 vhost 是什么?起什么作用?

vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部均含有独立的 queueexchangebinding 等,但最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。

9.1.5 消息基于什么传输?

由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。

9.1.6 消息怎么路由?

从概念上来说,消息路由必须有三部分:交换器路由绑定。生产者把消息发布到交换器上;绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。

消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。

常用的交换器主要分为一下三种:

  • direct:如果路由键完全匹配,消息就被投递到相应的队列

  • fanout:如果交换器收到消息,将会广播到所有绑定的队列上

  • topic:可以使来自不同源头的消息能够到达同一个队列。使用topic交换器时,可以使用通配符。
    比如:“*” 匹配一部分的任意思字符, “.” 把路由键分为了几部分,“#” 匹配所有规则(零或多部分任意思字符)等。

特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是由"."隔开的一系列的标识符组成。

9.1.7 什么是元数据?元数据分为哪些类型?包括哪些内容?与 cluster 相关的元数据有哪些?元数据是如何保存的?元数据在 cluster 中是如何分布的?

在非 cluster 模式下,元数据主要分为 Queue 元数据(queue 名字和属性等)、Exchange 元数据(exchange 名字、类型和属性等)、Binding 元数据(存放路由关系的查找表)、Vhost 元数据(vhost 范围内针对前三者的名字空间约束和安全属性设置)。
cluster 模式下,还包括 cluster 中 node 位置信息和 node 关系信息。元数据按照 erlang node 的类型确定是仅保存于 RAM 中,还是同时保存在 RAM 和 disk 上。元数据在 cluster 中是全 node 分布的。

9.1.8 在单node 系统和多 node 构成的 cluster 系统中声明 queue、exchange ,以及进行 binding 会有什么不同?

答:当你在单 node 上声明 queue 时,只要该 node 上相关元数据进行了变更,你就会得到 Queue.Declare-ok 回应;而在 cluster 上声明 queue ,则要求 cluster 上的全部 node 都要进行元数据成功更新,才会得到 Queue.Declare-ok 回应。另外,若 node 类型为 RAM node 则变更的数据仅保存在内存中,若类型为 disk node 则还要变更保存在磁盘上的数据。

死信队列&死信交换器:DLX 全称(Dead-Letter-Exchange),称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。

9.1.9 如何确保消息正确地发送至RabbitMQ?

RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ
发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

9.1.10 如何确保消息接收方消费了消息?

接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。

下面罗列几种特殊情况:

  • 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
  • 如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
9.1.11 如何避免消息重复投递或重复消费?

在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。

这个问题针对业务场景来答分以下几点:

1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

9.1.12 如何解决丢数据的问题?

1.生产者丢数据

生产者的消息没有投递到MQ中怎么办?从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。

然而缺点就是吞吐量下降了。因此,按照博主的经验,生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

2.消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

①、将queue的持久化标识durable设置为true,则代表是一个持久的队列

②、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

3.消费者丢数据

启用手动确认模式可以解决这个问题

①自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。

②手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。

③不确认模式,acknowledge=“none” 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

9.1.13 死信队列和延迟队列的使用?

死信消息:

消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
消息过期了
队列达到最大的长度

过期消息:

在 rabbitmq 中存在2种方可设置消息的过期时间,第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。

队列设置: 在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒

单个消息设置: 是设置消息属性的 expiration 参数的值,单位为 毫秒

延时队列: 在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。

9.1.14 使用了消息队列会有什么缺点?

1.系统可用性降低:你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低

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

9.1.15 多个消费者监听一个队列时,消息如何分发?
  • 轮询: 默认的策略,消费者轮流,平均地接收消息
  • 公平分发: 根据消费者的能力来分发消息,给空闲的消费者发送更多消息

//当消费者有x条消息没有响应ACK时,不再给这个消费者发送消息

channel.basicQos(int x)
9.1.16 无法被路由的消息去了哪里?

无设置的情况下,无法路由(Routing key错误)的消息会被直接丢弃
解决方案:
mandatory设置为true,并配合ReturnListener,实现消息的回发

声明交换机时,指定备份的交换机

 Map<String,Object> arguments = new HashMap<String,Object>();
	arguments.put("alternate-exchange","备份交换机名");
9.1.17 消息在什么时候会变成死信?
  • 消息拒绝并且没有设置重新入队
  • 消息过期
  • 消息堆积,并且队列达到最大长度,先入队的消息会变成DL
9.1.18 RabbitMQ如何实现延时队列?

利用TTL(队列的消息存活时间或者消息存活时间),加上死信交换机

 // 设置属性,消息10秒钟过期
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
		.expiration("10000") // TTL

 // 指定队列的死信交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
9.1.19 如何保证消息的可靠性投递

发送方确认模式:
将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。
一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。
如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
接收方确认机制
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。
这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。保证数据的最终一致性;
下面罗列几种特殊情况:
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。

9.1.20 消息如何被优先消费
//生产者
 Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-max-priority",10);

//消费者
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .priority(5) // 优先级,默认为5,配合队列的 x-max-priority 属性使用
9.1.21 如何保证消息的顺序性

一个队列只有一个消费者的情况下才能保证顺序,否则只能通过全局ID实现(每条消息都一个msgId,关联的消息拥有一个parentMsgId。可以在消费端实现前一条消息未消费,不处理下一条消息;也可以在生产端实现前一条消息未处理完毕,不发布下一条消息)

9.1.22 如何自动删除长时间没有消费的消息
// 通过队列属性设置消息过期时间
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-message-ttl",6000);

 // 对每条消息设置过期时间
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .expiration("10000") // TTL
9.1.23 RabbitMQ的集群模式和集群节点类型

普通模式: 默认模式,以两个节点(rabbit01,rabbit02)为例来进行说明,对于Queue来说,消息实体只存在于其中一个节点rabbit01(或者rabbit02),rabbit01和rabbit02两个节点仅有相同的元数据,即队列结构。当消息进入rabbit01节点的Queue后,consumer从rabbit02节点消费时,RabbitMQ会临时在rabbit01,rabbit02间进行消息传输,把A中的消息实体取出并经过B发送给consumer,所以consumer应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理Queue。否则无论consumer连rabbit01或rabbit02,出口总在rabbit01,会产生瓶颈。当rabbit01节点故障后,rabbit02节点无法取到rabbit01节点中还未消费的消息实体。如果做了消息持久化,那么等到rabbit01节点恢复,然后才可被消费。如果没有消息持久化,就会产生消息丢失的现象。

镜像模式: 把需要的队列做成镜像队列,存在与多个节点属于RabibitMQ的HA方案,该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取,该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉,所以在对可靠性要求比较高的场合中适用
节点分为内存节点(保存状态到内存,但持久化的队列和消息还是会保存到磁盘),磁盘节点(保存状态到内存和磁盘),一个集群中至少需要一个磁盘节点

9.1.24 ActiveMQ、RabbitMQ、Kafka、RocketMQ 有什么优缺点?

在这里插入图片描述
ActiveMQ: 最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以最不推荐使用的就是这个MQ了,其它三个都各有所长。
RabbitMQ: 开源的,有比较稳定的支持,活跃度也高,吞吐量,性能都不错,且可选模式也多,稳定性好,且有管理界面可以操作,换句话说,如果是传统的开发不涉及大数据及别的数据,RabbitMQ是首先的MQ,当然它也有缺点:是用erlang语言开发的,不利于工程师深入与二次开发。
RocketMQ: 阿里出品,性能有保障,吞吐量高,有管理界面,但是开源社区不活跃有可能会黄,专业版要收费,不过有阿里在,这个MQ必将是未来大数据最好的选择
Kafka: 大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,几乎是全世界这个领域的事实性规范,RocketMQ也是按Kafka开发,但是缺点也很多,看个人选择。


参考

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小夏陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值