文章目录
一.概念
1.什么是MQ
MQ(Message Quene) : 翻译为 消息队列,通过典型的 生产者
和消费者
模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为 消息中间件
通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
2.MQ有哪些
ActiveMQ: ActiveMQ 是Apache出品,最流行的,能力强劲的开源消息总线。它是一个完全支持JMS规范的的消息中间件。丰富的API,多种集群架构模式让ActiveMQ在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
Kafka: Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
RocketMQ: RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
RabbitMQ: RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。
3.RabbitMQ
AMQP 协议
AMQP(advanced message queuing protocol)在2003年时被提出,最早用于解决金融领不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。
AMQP协议模型:
二.RabbitMQ的安装
1.将rabbitmq安装包上传到linux系统中
erlang-22.0.7-1.el7.x86_64.rpm
rabbitmq-server-3.7.18-1.el7.noarch.rpm
2.安装Erlang依赖包
rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
3.安装RabbitMQ安装包(需要联网)
yum install -y rabbitmq-server-3.7.18-1.el7.noarch.rpm
注意: 默认安装完成后配置文件模板在:/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example目录中,需要将配置文件复制到/etc/rabbitmq/目录中,并修改名称为rabbitmq.config
4.复制配置文件
cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
5.查看配置文件位置
ls /etc/rabbitmq/rabbitmq.config
6.修改配置文件(参见下图:)
vim /etc/rabbitmq/rabbitmq.config
将上图中配置文件中红色部分去掉%%
,以及最后的,
逗号 修改为下图:
7.启动rabbitmq中的插件管理
rabbitmq-plugins enable rabbitmq_management
8.启动关闭RabbitMQ的服务
systemctl start rabbitmq-server
systemctl restart rabbitmq-server
systemctl stop rabbitmq-server
9.查看服务状态
systemctl status rabbitmq-server
11.访问web管理界面
密码和用户名都为:guest
connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
Exchanges:交换机,用来实现消息的路由
Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
12.RabbitMQ 管理命令行
服务启动相关
systemctl start|restart|stop|status rabbitmq-server
管理命令行
用来在不使用web管理界面情况下命令操作RabbitMQ
rabbitmqctl help 可以查看更多命令
插件管理命令行
rabbitmq-plugins enable|list|disable
三.Admin用户和虚拟主机管理
1. 添加用户
上面的Tags选项,其实是指定用户的角色,可选的有以下几个:
-
超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
-
监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
-
策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
-
普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
-
其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
2. 创建虚拟主机
虚拟主机: 为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。
3. 绑定虚拟主机和用户
创建好虚拟主机,我们还要给用户添加访问权限:
点击添加好的虚拟主机:
进入虚拟机设置界面:
四.MQ的应用场景
1.异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式
串行方式
:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
并行方式:
将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
消息队列:
假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回.
消息队列
: 引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍。
2.应用解耦
场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
这种做法有一个缺点:
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
订单系统:
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。库存系统:
订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.
3.流量削峰
场景:
秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
1.可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.
2.秒杀业务根据消息队列中的请求信息,再做后续处理.
4.集群
普通集群
默认情况下:RabbitMQ代理操作所需的所有数据/状态都将跨所有节点复制。这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问
架构图
核心解决问题: 当集群中某一时刻master节点宕机,可以对Quene中信息,进行备份
镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。
镜像集群
集群架构图
五.RabbitMQ 的使用
1.AMQP协议的回顾
2.RabbitMQ支持的消息模型
3.引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
4.第一种模型(直连)
在上图的模型中,有以下概念:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来。
- queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
生产者
public class Provider {
//生产消息
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接mq的连接工厂对象
ConnectionFactory connectionFactory=new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("192.168.253.130");
//设置端口号
connectionFactory.setPort(5672);
//设置连接那个虚拟主机
connectionFactory.setVirtualHost("/hello");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("dessw");
connectionFactory.setPassword("wsy696581");
// 获取连接对象
Connection connection=connectionFactory.newConnection();
// 通过连接获取连接中的通道对象
Channel channel=connection.createChannel();
//通道绑定对应消息队列
//参数1: 队列名称 如果队列不存在自动创建
//参数2: 用来定义队列特性是否要持久化 true 持久化队列 false 不持久化
//参数3: exclusive 是否独占队列 true 独占队列 false 不独占
//参数4: autoDelete: 是否在消费完成后自动删除队列 true 自动删除 false 不自动删除
//参数5: 额外附加参数
channel.queueDeclare("hello",false,false,false,null);
//参数1: 交换机名称 参数2:队列名称 参数3:传递消息额外设置 参数4:消息的具体内容
channel.basicPublish("","hello",null,"hello rabbitmq".getBytes());
channel.close();
}
}
消费者
public class Consumer {
// 消费消息
@Test
public void testGetMessage() throws IOException, TimeoutException {
// 创建连接mq的连接工厂
ConnectionFactory connectionFactory=new ConnectionFactory();
// 设置链接rabbitmq主机
connectionFactory.setHost("192.168.253.130");
// 设置主机端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/hello");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("dessw");
connectionFactory.setPassword("wsy696581");
// 获取连接对象
Connection connection = connectionFactory.newConnection();
// 通过连接获取连接中的通道对象
Channel channel=connection.createChannel();
// 通道绑定对应的消息队列
// 参数 队列名称(不存在的时候自动创建)
// 用来定义队列特征是要持久化
// 是否独占队列(true 就只能被当前连接使用)
// 是否在消费完成后自动删除队列
// 附加参数
channel.queueDeclare("hello",false,false,false,null);
//消费消息
//参数1: 消费那个队列的消息 队列名称
//参数2: 开始消息的自动确认机制
//参数3: 消费时的回调接口
DefaultConsumer defaultConsumer=new DefaultConsumer(channel);
channel.basicConsume("hello",true,new DefaultConsumer(channel));
channel.close();
connection.close();
}
}
consumer里不关闭通道和连接的话 会一直消费;
若要输出上图,则要求consumer里不关闭通道和连接,然后先运行consumer(consumer要处于main线程内),再运行provider
5.RabbitMQ封装
封装类
//RabbitMQ封装类
public class RabbitmqUtil {
private static ConnectionFactory connectionFactory;
//直接加载相关信息
static {
connectionFactory=new ConnectionFactory();
connectionFactory.setHost("192.168.253.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/hello");
connectionFactory.setUsername("dessw");
connectionFactory.setPassword("wsy696581");
}
/**
* 获取RabbitMQ的连接
* @return 连接对象
*/
public static Connection getConnection(){
try {
Connection connection=connectionFactory.newConnection();
return connection;
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 关闭连接 注意:一般消费者不用关闭连接 只用于生产者关闭连接
* @param channel
* @param connection
*/
public static void closeConnection(Channel channel,Connection connection){
try {
if (channel!=null){
channel.close();
}
if (connection!=null){
connection.close();
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
提供者
public class ProviderTwo {
@Test
public void testSend() throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitmqUtil.getConnection();
//获取连接中通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
//参数1: 队列名称 如果队列不存在自动创建
//参数2: 用来定义队列特性是否要持久化 true 持久化队列 false 不持久化
//参数3: exclusive 是否独占队列 true 独占队列 false 不独占
//参数4: autoDelete: 是否在消费完成后自动删除队列 true 自动删除 false 不自动删除
//参数5: 额外附加参数
channel.queueDeclare("hello",false,false,false,null);
//参数1: 交换机名称 参数2:队列名称 参数3:传递消息额外设置 参数4:消息的具体内容
channel.basicPublish("","hello",null,"hello rabbitmq".getBytes());
//调用工具类关闭连接和通道
RabbitmqUtil.closeConnection(channel,connection);
}
}
消费者:
public class ConsumerTwo {
public static void main(String[] args) throws IOException, TimeoutException {
//通过工具类获取连接
Connection connection = RabbitmqUtil.getConnection();
//创建通道
Channel channel=connection.createChannel();
//通道绑定对象
channel.queueDeclare("hello",false,false,false,null);
//消费消息
//参数1: 消费那个队列的消息 队列名称
//参数2: 开始消息的自动确认机制
//参数3: 消费时的回调接口
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));
}
});
//调用工具类关闭连接和通道
RabbitmqUtil.closeConnection(channel,connection);
}
}
6.RabbitMQ的API细节参数
不持久化时则在rabbitmq重启的时候就会删除队列
设置为true这里会变成D 但是只是队列持久化 消息还是会被删除
设置队列的消息也持久化 当然消息持久化了 队列必须设置为持久化
设置队列自动删除 消费者不在占用队列时 队列自动删除 (消费者和生成者队列的配置要一样)
7.第二种模型(work quene)
Work queues
,也被称为任务模型(Task queues
)。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
角色:
- P:生产者:任务的发布者
- C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
- C2:消费者-2:领取任务并完成任务,假设完成速度快
生产者
public class Provider {
@Test
public void sendMessage() throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 通过通道声明队列
channel.queueDeclare("work",true,false,false,null);
for (int i = 1; i <=10 ; i++) {
// 生成消息
channel.basicPublish("","work",null,(i+"hello work quene").getBytes());
}
// 关闭资源
RabbitmqUtil.closeConnection(channel,connection);
}
}
消费者1
public class Comsumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
final Channel channel = connection.createChannel();
// 通过通道声明队列
channel.queueDeclare("work",true,false,false,null);
//消费消息
//参数1: 消费那个队列的消息 队列名称
//参数2: 开始消息的自动确认机制
//参数3: 消费时的回调接口
channel.basicConsume("work", 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));
//手动确认 发送ack 代表当前消息消费结束 可以去消费下一个消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
消费者2
public class Comsumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
final Channel channel = connection.createChannel();
// 通过通道声明队列
channel.queueDeclare("work",true,false,false,null);
//消费消息
//参数1: 消费那个队列的消息 队列名称
//参数2: 开始消息的自动确认机制
//参数3: 消费时的回调接口
channel.basicConsume("work", 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));
//手动确认 发送ack 代表当前消息消费结束 可以去消费下一个消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
consumer里不关闭通道和连接,然后先运行consumer1和consumer2,再运行provider
采用轮询算法
消息确认机制和能者多劳实现
在平均消费模式中,消费者只要从队列中拿到消息,就立刻发送确认机制,有可能在处理消息的时候就突然宕机了或者出现意外了,这样消息还没来得及消费就遗失了,就造成业务数据的丢失。
另外,也有可能两个消费者处理消息的效率不一样,就有可能造成一个消费者已经消费完消息然后闲着,而另外一个消费者拿到了消息,却一直处于处理消息的状态,造成资源的浪费。
当前是自动确认消息接收 收到消息就当做消费完成 但是业务不一定结束了 还没执行完所有消息的业务就出事宕机了 就会导致消息丢失
所以将当前两个消费者的该参数设为false 且设置 消费者一次只能消费一个消息
模拟消费者1消费时间比较长
启动 消费者1 2 和生产者
8.第三种模型(fanout广播)
fanout 扇出 也称为广播
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
生产者
public class Provider {
@Test
public void sendMessage() throws IOException {
// 获取连接
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 将通道声明指定交换机 参数1: 交换机的名称 参数2 交换机的类型
channel.exchangeDeclare("change","fanout");
// 发送消息
channel.basicPublish("change","",null,"fanout type message".getBytes());
// 关闭资源
RabbitmqUtil.closeConnection(channel,connection);
}
}
消费者1,2,3
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 将通道声明指定交换机 参数1: 交换机的名称 参数2 交换机的类型
channel.exchangeDeclare("change","fanout");
// 临时队列
String queue = channel.queueDeclare().getQueue();
// 绑定交换机和队列
// 参数三 路由key 暂时在fanout模式没有作业
channel.queueBind(queue,"change","");
// 消费消息
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));
}
});
}
}
启动 1 2 3 生产者 都消费到了这条消息
9.第四种模型(Routing)
在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 的消息
生产者
public class Provider {
@Test
public void sendMessage() throws IOException {
// 获取连接
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 将通道声明指定交换机 参数1: 交换机的名称 参数2 交换机的类型
channel.exchangeDeclare("routing","direct");
// 发送消息
String routingkey="info";// 路由key,consumer2接收
channel.basicPublish("routing",routingkey,null,("direct type message routingKey="+routingkey).getBytes());
String routingkey2="warning";// 路由key,consumer1接收
channel.basicPublish("routing",routingkey2,null,("direct type message routingKey2="+routingkey2).getBytes());
String routingkey3="error";// 路由key,consumer1,2接收
channel.basicPublish("routing",routingkey3,null,("direct type message routingKey3="+routingkey3).getBytes());
// 关闭资源
RabbitmqUtil.closeConnection(channel,connection);
}
}
consumer1 值接收路由key为error,warning的消息
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 连接对象
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("routing","direct");
// 临时队列
String queue = channel.queueDeclare().getQueue();
// 通过router key 绑定交换机和队列
// 参数三 路由key,consumer1接收来自error和warning的路由
channel.queueBind(queue,"routing","error");
channel.queueBind(queue,"routing","warning");
// 消费消息
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));
}
});
}
}
consumer2 值接收路由key为error,info的消息
public class Consumer2 {
public static void main(String[] args) throws IOException {
// 连接对象
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("routing","direct");
// 临时队列
String queue = channel.queueDeclare().getQueue();
// 通过router key 绑定交换机和队列
// 参数三 路由key,consumer2接收来自error和info的路由
channel.queueBind(queue,"routing","error");
channel.queueBind(queue,"routing","info");
// 消费消息
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));
}
});
}
}
结果:
10.第五种模型("topic"可以叫做动态路由)
Topic
类型的Exchange
与Direct
相比,都是可以根据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,后面只能有一个
生产者
public class Provider {
@Test
public void sendMessage() throws IOException {
// 连接对象
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 将通道声明指定交换机 参数1: 交换机的名称 参数2 交换机的类型
channel.exchangeDeclare("topicrouting","topic");
// 发送消息
String routingKey="user.save"; // 路由key
channel.basicPublish("topicrouting",routingKey,null,("topic type message routingKey="+routingKey).getBytes());
// 关闭资源
RabbitmqUtil.closeConnection(channel,connection);
}
}
消费者:
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 连接对象
Connection connection = RabbitmqUtil.getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("topicrouting","topic");
// 临时队列
String queue=channel.queueDeclare().getQueue();
// 通过动态通配符router key 绑定交换机和队列
channel.queueBind(queue,"topicrouting","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));
}
});
}
}
结果:
将生产者的路由key变为user.save,findAll 就无法消费到了
修改 消费者的通配符 router key
这就可以消费到了
user也可以消费到
六.SpringBoot集成RabbitMQ
配置文件
spring.application.name=springboot_rabbitmq
spring.rabbitmq.password=wsy696581
spring.rabbitmq.username=dessw
spring.rabbitmq.host=192.168.253.130
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/hello
1.第一种模型(直连)
生产者
//hello world
@Test
public void testHello(){
rabbitTemplate.convertAndSend("hello","hello world");
}
消费者
@Component
@RabbitListener(queuesToDeclare = @Queue("hello"))
public class Consumer {
@RabbitHandler
public void getMessage(String message){
System.out.println("message="+message);
}
}
效果
2.第二种模型(work queue)
生产者
//work
@Test
public void testWork(){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work","work模型"+i);
}
}
消费者
@Component
public class WorkConsumer {
//一个消费者
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive1(String message){
System.out.println("message1 = "+message);
}
//一个消费者
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message){
System.out.println("message2 = "+message);
}
}
效果
3.第三种模型(fanout广播)
生产者
//fanout 广播
@Test
public void testFanout(){
rabbitTemplate.convertAndSend("logs","","Fanout的模型发送的消息");
}
消费者
@Component
public class FanoutCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange =@Exchange(value = "logs",type = "fanout") //绑定的交换机
)
})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange =@Exchange(value = "logs",type = "fanout") //绑定的交换机
)
})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
效果
4.第四种模型(Routing)
生产者
//route 路由模式
@Test
public void testRoute(){
rabbitTemplate.convertAndSend("directs","error","发送info的key的路由信息");
}
消费者
@Component
public class RouteCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "directs",type = "direct"),//自定交换机名称和类型
key = {"info","error","warn"}
)
})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "directs",type = "direct"),//自定交换机名称和类型
key = {"error",}
)
})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
效果
5.第五种模型(topic动态路由)
生产者
//topic 动态路由 订阅模式
@Test
public void testTopic(){
rabbitTemplate.convertAndSend("topics","product.save.add","produce.save.add 路由消息");
}
消费者
@Component
public class TopicCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(type = "topic",name="topics"),
key={"user.save","user.*"}
)
})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(type = "topic",name="topics"),
key={"order.#","product.#","user.*"}
)
})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
效果
七.延时队列的使用
配置文件
@Configuration
public class RabbitMqConfig {
// 声明延迟交换机
@Bean("delayExchange")
public CustomExchange delayExchange() {
HashMap<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(MQConstant.LIBRARY_DELAY_EXCHANGE_NAME,"x-delayed-message",true,false,args);
}
// 声明预订延迟队列
@Bean("reserveDelayQueue")
public Queue reserveDelayQueue() {
return new Queue(MQConstant.LIBRARY_DELAY_RESERVE_QUEUE_NAME);
}
// 声明借阅延迟队列
@Bean("borrowDelayQueue")
public Queue borrowDelayQueue() {
return new Queue(MQConstant.LIBRARY_DELAY_BORROW_QUEUE_NAME);
}
// 设置预订延迟队列的绑定关系
@Bean
public Binding reserveDelayBinding(@Qualifier("reserveDelayQueue") Queue queue,
@Qualifier("delayExchange") CustomExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(MQConstant.LIBRARY_DELAY_RESERVE_QUEUE_ROUTING_KEY).noargs();
}
// 设置借阅延迟队列的绑定关系
@Bean
public Binding borrowDelayBinding(@Qualifier("borrowDelayQueue") Queue queue,
@Qualifier("delayExchange") CustomExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(MQConstant.LIBRARY_DELAY_BORROW_QUEUE_ROUTING_KEY).noargs();
}
/**
* 设置属性和消息存活时间
* @param time
*/
public static MessagePostProcessor messagePostProcessor(long time) {
//设置消息存活时间
return message -> {
//设置有效期
message.getMessageProperties().setHeader("x-delay", time);
return message;
};
}
}
生产者
向延迟交换机发送携带key的消息和过期时间,过期时间到了之后延迟交换机会根据key发送到不同的延时队列
long time = 5 * 60 * 1000;
amqpTemplate.convertAndSend(MQConstant.LIBRARY_DELAY_EXCHANGE_NAME, MQConstant.LIBRARY_DELAY_RESERVE_QUEUE_ROUTING_KEY,
JSONObject.toJSONString(map), RabbitMqConfig.messagePostProcessor(time));
消费者
/**
* 监听借阅延时队列
* @param json
*/
@RabbitListener(queues = MQConstant.LIBRARY_DELAY_RESERVE_QUEUE_NAME)
public void borrowHandle(String json) {
JSONObject jsonObject = JSONObject.parseObject(json);
String recordId = (String) jsonObject.get("recordId");
//将借阅图书的状态从正常借出改为超期
try {
libraryRecordService.changeBorrow(recordId);
} catch (Exception e) {
log.error("将借阅图书的状态从正常借出改为超期状态,预约记录id:{}", recordId);
e.printStackTrace();
}
}
八.消息确认机制
RabbitMQ的消息确认有两种
- 第一种是消息发送确认。这种是用来确认生产者将消息发送给交换器,交换器传递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。
- 第二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息。
1.消息发送确认(生产者)
正常情况下,生产者会通过交换机发送消息至队列中,再由消费者来进行消费,但是其实RabbitMQ在接收到消息后,还需要一段时间消息才能存入磁盘,并且其实也不是每条消息都会存入磁盘,可能仅仅只保存到cache中,这时,如果RabbitMQ正巧发生崩溃,消息则就会丢失,所以为了避免该情况的发生,我们引入了生产者确认机制,rabbitmq对此提供了两种方式:
- 通过事务实现
- 通过发送方确认机制(publisher confirm)实现
1.1 事务实现
channel.txSelect(): 将当前信道设置成事务模式
channel.txCommit(): 用于提交事务
channel.txRollback(): 用于回滚事务
通过事务实现机制,只有消息成功被rabbitmq服务器接收,事务才能提交成功,否则便可在捕获异常之后进行回滚,然后进行消息重发,但是事务非常影响rabbitmq的性能。还有就是事务机制是阻塞的过程,只有等待服务器回应之后才会处理下一条消息
//事务模式
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 声明交换机,队列,路由key,消息
String exchangeName = "transaction-exchange";
String queueName = "transaction-queueName ";
String routeKey = "transaction-routeKey ";
String message = "transaction-你好呀!!!! ";
//获取MQ连接
connection = connectionFactory.newConnection("transaction-product");
//通过连接获取通道Channel
channel = connection.createChannel();
//声明队列和交换机 并将队列和交换机通过路由绑定在一起
channel.queueDeclare(queueName, false, false, false, null);
channel.exchangeDeclare(exchangeName,"direct",true,false,null);
channel.queueBind(queueName,exchangeName,routeKey);
//将信道设为事务模式
channel.txSelect();
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以也可以不标明队列名称
channel.basicPublish(exchangeName,routeKey,null,message.getBytes());
System.out.println("======");
Thread.sleep(10000);
int i=1/0;
//事务提交
channel.txCommit();
System.out.println("发送成功!!!");
} catch (Exception e) {
channel.txRollback();
System.out.println("发送失败并回滚!!!");
e.printStackTrace();
} finally {
//关闭资源
if (channel != null && channel.isOpen()) {
channel.close();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
1.2.confirm实现
confirm方式有三种模式:普通confirm模式、批量confirm模式、异步confirm模式
- channel.confirmSelect(): 将当前信道设置成了confirm模式
普通confirm模式
每发送一条消息,就调用waitForConfirms()方法,等待服务端返回Ack或者nack消息
//普通模式
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 声明交换机,队列,路由key,消息
String exchangeName = "confirm11111-exchange";
String queueName = "confirm11111-queueName";
String routeKey = "confirm11111-routeKey";
String message = "confirm11111-你好呀!!!!";
//获取MQ连接
connection = connectionFactory.newConnection("transaction-product");
//通过连接获取通道Channel
channel = connection.createChannel();
//声明队列和交换机 并将队列和交换机通过路由绑定在一起
channel.queueDeclare(queueName, false, false, false, null);
channel.exchangeDeclare(exchangeName,"direct",true,false,null);
channel.queueBind(queueName,exchangeName,routeKey);
//将当前信道设置成了confirm模式
channel.confirmSelect();
for (int i = 0; i < 5; i++) {
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以也可以不标明队列名称
channel.basicPublish(exchangeName,routeKey,null,message.getBytes());
//int k=1/0;
//信道为confirm模式后,即可通过waitForConfirms()接收mq服务端返回来的信息(消息发送到队列后 mq服务器就发送成功的ack)
if (channel.waitForConfirms()){
System.out.println("发送成功!!!");
}
//int k=1/0;
}
} catch (Exception e) {
System.out.println("发送失败");
e.printStackTrace();
} finally {
//关闭资源
if (channel != null && channel.isOpen()) {
channel.close();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
批量confirm模式
//批量comfirm
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 声明交换机,队列,路由key,消息
String exchangeName = "confirm22222-exchange";
String queueName = "confirm22222-queueName ";
String routeKey = "confirm22222-routeKey ";
String message = "confirm22222-你好呀!!!! ";
//获取MQ连接
connection = connectionFactory.newConnection("transaction-product");
//通过连接获取通道Channel
channel = connection.createChannel();
//声明队列和交换机 并将队列和交换机通过路由绑定在一起
channel.queueDeclare(queueName, false, false, false, null);
channel.exchangeDeclare(exchangeName,"direct",true,false,null);
channel.queueBind(queueName,exchangeName,routeKey);
//将当前信道设置成了confirm模式
channel.confirmSelect();
for (int i = 0; i < 5; i++) {
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以也可以不标明队列名称
channel.basicPublish(exchangeName,routeKey,null,message.getBytes());
if (i==3){
int k = 1/0;
}
}
//int i=1/0;
//消息批量发送完成后,通过waitForConfirmsOrDie()方法来接收服务端返回的信息(最后一条消息发送到队列后 接收到mq服务器最后发送的ack或nack)
channel.waitForConfirmsOrDie();
} catch (Exception e) {
System.out.println("发送失败");
e.printStackTrace();
} finally {
//关闭资源
if (channel != null && channel.isOpen()) {
channel.close();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
异步comfirm
//异步comfirm
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 声明交换机,队列,路由key,消息
String exchangeName = "confirm33333-exchange";
String queueName = "confirm33333-queueName";
String routeKey = "confirm33333-routeKey";
String message = "confirm33333-你好呀!!!!";
//获取MQ连接
connection = connectionFactory.newConnection("transaction-product");
//通过连接获取通道Channel
channel = connection.createChannel();
//声明队列和交换机 并将队列和交换机通过路由绑定在一起
channel.queueDeclare(queueName, true, false, false, null);
channel.exchangeDeclare(exchangeName,"direct",true,false,null);
channel.queueBind(queueName,exchangeName,routeKey);
//将当前信道设置成了confirm模式
channel.confirmSelect();
//我们可以创建一个集合,存放未确认的消息标识 每发送一条消息则往集合中添加一条,如果收到ack则在handleAck方法中移除,收到nack则重新发送。
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
//通过channel.addConfirmListener()监听发送方确认模式,通过信道中的waitForConfirmsOrDie等待传回ack或者nack
channel.addConfirmListener(new ConfirmListener() {
//消息成功发送到MQ
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//接收到ack消息 说明消息成功发送到MQ,已持久化到磁盘
//生产者发送消息前可以将消息持久化到mysql或redis,消费端消费结束后再删除
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送成功啦!!!!!!");
//confirmSet.headSet(deliveryTag+1).clear() 这段代码的作用是,删除所有小于等于当前消息 delivery tag 的未确认消息的 delivery tag。
// 这是因为,如果当前消息被确认了,之前的所有消息也都被确认了。因此,这些消息的 delivery tag 可以从未确认集合中删除。
// 这样可以避免未确认集合中积累太多的 delivery tag,提高系统的性能和可靠性。
if (multiple){
System.out.println(" ==========================multiple:"+multiple);
confirmSet.headSet(deliveryTag+1).clear();
} else {
System.out.println("==========================multiple:"+multiple);
confirmSet.remove(deliveryTag);
}
}
//消息发送到MQ失败
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//接收到nack消息 说明发送的消息没到达MQ,可选择重新投递消息
//达到一定时间后也可自动重新投递
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送失败啦!!!!!!!");
// 注意这里需要添加处理消息重发的场景
}
});
//要先添加监听之后 再发送消息才能执行回调方法
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
for (int i = 0; i < 5; i++) {
channel.basicPublish(exchangeName,routeKey,null,message.getBytes());
//每发送一条消息就添加一个消息标识
long seqNo = channel.getNextPublishSeqNo();
confirmSet.add(seqNo);
System.out.println("发送:"+seqNo);
Thread.sleep(1000);
}
} catch (Exception e) {
System.out.println("发送失败");
e.printStackTrace();
} finally {
//关闭资源
if (channel != null && channel.isOpen()) {
channel.close();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
异步comfirm过程中的线程问题
- 消息发送成功,但不执行回调函数
//要先添加监听之后 再发送消息才能执行回调方法
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
for (int i = 0; i < 5; i++) {
channel.basicPublish(exchangeName,routeKey,null,message.getBytes());
//每发送一条消息就添加一个消息标识
long seqNo = channel.getNextPublishSeqNo();
confirmSet.add(seqNo);
System.out.println("发送:"+seqNo);
Thread.sleep(1000);
}
//通过channel.addConfirmListener()监听发送方确认模式,通过信道中的waitForConfirmsOrDie等待传回ack或者nack
channel.addConfirmListener(new ConfirmListener() {
//消息成功发送到MQ
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//接收到ack消息 说明消息成功发送到MQ,已持久化到磁盘
//生产者发送消息前可以将消息持久化到mysql或redis,消费端消费结束后再删除
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送成功啦!!!!!!");
//confirmSet.headSet(deliveryTag+1).clear() 这段代码的作用是,删除所有小于等于当前消息 delivery tag 的未确认消息的 delivery tag。
// 这是因为,如果当前消息被确认了,之前的所有消息也都被确认了。因此,这些消息的 delivery tag 可以从未确认集合中删除。
// 这样可以避免未确认集合中积累太多的 delivery tag,提高系统的性能和可靠性。
if (multiple){
System.out.println(" ==========================multiple:"+multiple);
confirmSet.headSet(deliveryTag+1).clear();
} else {
System.out.println("==========================multiple:"+multiple);
confirmSet.remove(deliveryTag);
}
}
//消息发送到MQ失败
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//接收到nack消息 说明发送的消息没到达MQ,可选择重新投递消息
//达到一定时间后也可自动重新投递
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送失败啦!!!!!!!");
// 注意这里需要添加处理消息重发的场景
}
});
在RabbitMQ中,发送确认机制(confirm)是一种异步操作。当信道(Channel)被设置为确认模式(confirm mode)时,我们可以通过调用channel.addConfirmListener()方法来注册一个确认监听器(ConfirmListener)来检查每条消息的确认状态,包括是否发送成功、是否发送失败等。然而,这个监听器是异步的,也就是说,当你调用channel.basicPublish()方法发送消息时,RabbitMQ并不会立即返回确认或否认的状态,而是在后台异步执行。如果在调用channel.basicPublish()方法之前注册了确认监听器,那么监听器就会监听到后台异步处理的确认状态并执行相应的回调函数。如果在调用channel.basicPublish()方法之后注册确认监听器,那么监听器就无法监听到之前发送的消息的确认状态,因此回调函数也无法执行。
- 监听器在消息发送之后注册,但仍可以执行回调方法
for (int i = 0; i < 5; i++) {
channel.basicPublish("", queueName, null, message.getBytes());
}
//这里能够输出deliveryTag:5 的消息是因为发送最后一条消息之后,mq的发送线程还没结束,而此时新注册的监听器监听到了消息的确认状态,从而执行回调函数
channel.addConfirmListener(new ConfirmListener() {
//该回调方法主要用于收到消费者确认消费后的ack
// deliveryTag:每个消息的唯一标识,从1开始递增
// multiple:当前消息是否同时确认了多个,消息确认有可能是批量确认的,是否批量确认在于返回的multiple的参数,此参数为bool值,如果true表示批量执行了deliveryTag这个值以前的所有消息,如果为false的话表示单条确认
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//接收到ack消息 说明发送的消息被消费者确认消费了,已持久化到磁盘
//生产者发送消息前可以将消息持久化到mysql或redis,消费结束接收到ack后再删除
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送成功啦!!!!!!");
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//接收到nack消息 说明发送的消息没被消费者消费,可选择重新投递消息
//达到一定时间后也可自动重新投递
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送失败啦!!!!!!!");
}
});
System.out.println("消息发送");
Thread.sleep(10000);
这里能够输出deliveryTag:5 的消息是因为发送最后一条消息之后,因为Thread.sleep(10000); mq的发送线程还没结束,而此时新注册的监听器监听到了一条消息的确认状态,从而执行回调函数
- 监听器在消息发送之后注册,发送单条消息 因为发送线程还没结束,而监听器监听到了消息状态的变化,可正常执行回调函数
channel.basicPublish("", queueName, null, message.getBytes());
//这里能够输出deliveryTag:5 的消息是因为发送最后一条消息之后,mq的发送线程还没结束,而此时新注册的监听器监听到了消息的确认状态,从而执行回调函数
channel.addConfirmListener(new ConfirmListener() {
//该回调方法主要用于收到消费者确认消费后的ack
// deliveryTag:每个消息的唯一标识,从1开始递增
// multiple:当前消息是否同时确认了多个,消息确认有可能是批量确认的,是否批量确认在于返回的multiple的参数,此参数为bool值,如果true表示批量执行了deliveryTag这个值以前的所有消息,如果为false的话表示单条确认
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//接收到ack消息 说明发送的消息被消费者确认消费了,已持久化到磁盘
//生产者发送消息前可以将消息持久化到mysql或redis,消费结束接收到ack后再删除
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送成功啦!!!!!!");
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//接收到nack消息 说明发送的消息没被消费者消费,可选择重新投递消息
//达到一定时间后也可自动重新投递
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送失败啦!!!!!!!");
}
});
System.out.println("消息发送");
Thread.sleep(100000);
2.消息接收确认(消费者)
有三种消息确认方式:
- 1.none代表不确认:该模式下,只要队列获取到了消息,就默认已成功消费。该模式下,容易造成消息丢失的情况。
listener:
simple:
acknowledge-mode: none
- 2.manual手动确认: 该模式下需要在代码中进行手动确认消息。若出现异常,会触发消息的重试机制(默认重试三次),若重试结束后仍没有被确认,则消息状态会变成Unacked,如下图示:
listener:
simple:
acknowledge-mode: manual
- auto自动确认(默认模式):自动应答,该模式下若消费出现异常则会触发MQ的重试机制,而重试机制若没处理好则容易导致死循环。如下图示:
listener:
simple:
acknowledge-mode: auto
消息接收确认重试机制的处理配置
避免重试机制导致的队列消费死循环的方法就是限制重试次数,或者使用手动应答等方式处理。
注意,在自动应答模式下,消息的最大重试次数容易造成消息的丢失。
listener:
simple:
acknowledge-mode: auto
retry:
enabled: true #开启重试
max-attempts: 3 #最大重试次数,默认3次,达到次数后,会进行消息移除。若绑定了死信队列,则会放入死信队列中
initial-interval: 2000ms #重试间隔时间
注意!,若想使用重试机制配置来限制重试的行为,那么在对应消费队列代码中,进行Nack的操作时,最后一个入参不能传入true(是否将消息重新发回队列中),否则照样会导致死循环。代码如下:
@RabbitListener(queues = RabbitMqConstants.FANOUT_EMAIL_QUEUE)
public void smsConsumerListener(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
try {
log.info("获取到队列消息:{}",msg);
int a = 1 / 0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
此处最后一个入参为true时,会导致死循环
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
九.重试机制+死信队列
1.死信队列
死信就是消息在特定场景下的一种表现形式,这些场景包括:
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息的 TTL 过期时
- 消息队列达到最大长度
- 达到最大重试限制
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
应用场景
未来保证订单业务的消息数据不丢失,我们需要使用到RabbitMQ的死信队列机制,当消息消费发生异常的时候,我们就把消息投入到死信队列中,比如说用户买东西,下单成功后去支付,但是没有在指定时间支付的时候就会自动失效。
如何配置死信队列
A. 消息过期
生产者
public class DeadLetterProducer {
private static String EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection("DeadLetter-product");
Channel channel = connection.createChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* // 设置消息 TTL 过期时间: 一个是对单个消息设置过期时间,一个是对一个队列设置过期时间,这样的话发送到队列中的消息都遵循那个过期时间
* //如果两种方法同时设置,则TTL以两者之间较小的那个数值为准
*
* 1.通过队列属性设置消息过期时间
* Map<String, Object> arguments = Maps.newHashMap();
* 设置消息发送到队列中在被丢弃之前可以存活的时间,单位:毫秒
* arguments.put("x-message-ttl", 5000);
* 声明队列
* channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
*
*
* 2.对消息本身设定过期时间
* AMQP.BasicProperties.Builder properties = MessageProperties.PERSISTENT_TEXT_PLAIN.builder();
* properties.expiration("5000");
*
*
* 3.设置队列过期时间
* Map<String, Object> arguments = Maps.newHashMap();
* 设置一个队列多长时间未被使用将会被删除,单位:毫秒
* arguments.put("x-expires", 5000);
* channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
*/
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
String message = "hello world!!!";
channel.basicPublish(EXCHANGE_NAME, "zhangsan-key", properties, message.getBytes());
System.out.println("消息发送完成:" + message);
}
}
消费者1
public class DeadLetterConsumer1 {
private static String NORMAL_EXCHANGE_NAME = "normal_exchange";
private static String NORMAL_QUEUE_NAME = "normal-queue";
private static String DEAD_EXCHANGE_NAME = "dead_exchange";
private static String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection("DeadLetter-consumer1");
Channel channel = connection.createChannel();
// 声明一个死信队列
channel.queueDeclare(DEAD_QUEUE_NAME, false, false, false, null);
// 声明一个死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 死信队列与死信交换机绑定
channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, "lisi-key");
// 正常队列与死信交换机的绑定关系
Map<String, Object> deadLetterParams = new HashMap<>(2);
deadLetterParams.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi-key");
// 声明一个正常队列
channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, deadLetterParams);
// 声明一个正常交换机
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 把队列和交换机进行绑定
channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, "zhangsan-key");
System.out.println("C1消费者启动等待消费消息:");
channel.basicConsume(NORMAL_QUEUE_NAME, true, (consumerTag, delivery) -> {
String receivedMessage = new String(delivery.getBody());
System.out.println("消费者接收到消息:" + receivedMessage);
},(consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
});
Thread.sleep(10000);
}
}
消费者2
public class DeadLetterConsumer2 {
private static String NORMAL_EXCHANGE_NAME = "normal_exchange";
private static String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection("DeadLetter-consumer1");
Channel channel = connection.createChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
System.out.println("C2消费者启动等待消费消息:");
channel.basicConsume(DEAD_QUEUE_NAME, true, (consumerTag, delivery) -> {
String receivedMessage = new String(delivery.getBody());
System.out.println("消费者接收到死信:" + receivedMessage);
},(consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
});
}
}
先启动消费者1, 再启动消费者2,消费者正常消费,消息未进入死信队列
全部关闭后,只启动生产者,消息先到达普通队列,
当普通队列的消息未被消费 ,消息超时后自动发送到死信队列
B. 队列达到最大长度
生产者
普通消费者
死信队列消费者
由于普通消费者中修改了队列参数,所以启动前需要先将原先的队列删除,然后再启动普通消费者,创建相关的队列及交换机。接着关闭普通消费者,启动生产者。打开后台系统:
C.消息拒绝后进入死信队列
2.消息重试
- 方案一:使用自动ACK + RabbitMQ重试机制
配置
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: mq-test
username: ********
password: ********
listener:
simple:
# ACK模式(默认为auto)
acknowledge-mode: auto
# 开启重试
retry:
enabled: true
max-attempts: 5
initial-interval: 5000
消费者
@RabbitListener(queues = RabbitMqConfig.USER_ADD_QUEUE, concurrency = "10")
public void userAddReceiver(String data, Message message, Channel channel) throws Exception {
UserVo vo = OBJECT_MAPPER.readValue(data, UserVo.class);
boolean success = messageHandle(vo);
// 通过业务控制是否消费成功,消费失败则抛出异常触发重试
if (!success) {
log.error("消费失败");
throw new Exception("消息消费失败");
}
}
需要说明的是,上述的方法一定要开启自动ACK,才会在到达最大重试上限后发送到死信队列,而且在重试过程中会独占当前线程,如果是单线程的消费者会导致其他消息阻塞,直至重试完成,所以可以使用@RabbitListener上的concurrency属性来控制并发数量。
- 方案二:使用手动ACK + 手动重试机制
配置
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: mq-test
username: ********
password: ********
listener:
simple:
# ACK模式(默认为auto)
acknowledge-mode: manual
十.消息分发机制
RabbitMQ 分发消息默认采用的轮训分发,但是在某种场景下这种策略并不是很好,当有两个消费者在处理任务时,其中有个消费者 处理任务的速度非常快,而另外一个消费者处理速度却很慢,这个时候我们还是采用轮训分发就会导致这处理速度快的这个消费者很大一部分时间处于空闲状态。我们可以通过修改消息分发的默认机制,来达到优化目的;
eg:
生产者
//异步comfirm
public static void main(String[] args) throws IOException, TimeoutException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 声明交换机,队列,路由key,消息
String queueName = "hand-out-queue";
String routeKey = "hand-out-key";
String exchangeName = "hand-out-exchange";
String message = "你好呀!!!!";
//获取MQ连接
connection = connectionFactory.newConnection("hand-out-product");
//通过连接获取通道Channel
channel = connection.createChannel();
//声明队列和交换机 并将队列和交换机通过路由绑定在一起
channel.queueDeclare(queueName, false, false, false, null);
//channel.exchangeDeclare(exchangeName,"topic",true,false,null);
//channel.queueBind(queueName,exchangeName,routeKey);
//将当前信道设置成了confirm模式
channel.confirmSelect();
//我们可以创建一个集合,存放未确认的消息标识 每发送一条消息则往集合中添加一条,如果收到ack则在handleAck方法中移除,收到nack则重新发送。
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
//通过channel.addConfirmListener()监听发送方确认模式,通过信道中的waitForConfirmsOrDie等待传回ack或者nack
channel.addConfirmListener(new ConfirmListener() {
//消息成功发送到MQ
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//接收到ack消息 说明消息成功发送到MQ,已持久化到磁盘
//生产者发送消息前可以将消息持久化到mysql或redis,消费端消费结束后再删除
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送成功啦!!!!!!");
//confirmSet.headSet(deliveryTag+1).clear() 这段代码的作用是,删除所有小于等于当前消息 delivery tag 的未确认消息的 delivery tag。
// 这是因为,如果当前消息被确认了,之前的所有消息也都被确认了。因此,这些消息的 delivery tag 可以从未确认集合中删除。
// 这样可以避免未确认集合中积累太多的 delivery tag,提高系统的性能和可靠性。
if (multiple){
System.out.println(" ==========================multiple:"+multiple);
confirmSet.headSet(deliveryTag+1).clear();
} else {
System.out.println("==========================multiple:"+multiple);
confirmSet.remove(deliveryTag);
}
}
//消息发送到MQ失败
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//接收到nack消息 说明发送的消息没到达MQ,可选择重新投递消息
//达到一定时间后也可自动重新投递
System.out.println("deliveryTag:"+deliveryTag+" multiple: "+multiple);
System.out.println("消息发送失败啦!!!!!!!");
// 注意这里需要添加处理消息重发的场景
}
});
//要先添加监听之后 再发送消息才能执行回调方法
/**
* 发送消息给交换机
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
for (int i = 0; i < 5; i++) {
channel.basicPublish("",queueName,null,(message+i).getBytes());
//每发送一条消息就添加一个消息标识
long seqNo = channel.getNextPublishSeqNo();
confirmSet.add(seqNo);
System.out.println("发送:"+seqNo);
//Thread.sleep(1000);
}
} catch (Exception e) {
System.out.println("发送失败");
e.printStackTrace();
} finally {
//关闭资源
if (channel != null && channel.isOpen()) {
channel.close();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
消费者
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//MQ连接配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.111.5");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection("DeadLetter-consumer1");
Channel channel = connection.createChannel();
String queueName = "hand-out-queue";
channel.queueDeclare(queueName, false, false, false, null);
System.out.println("C1正常消费者启动等待消费消息:");
//false-不自动确认
channel.basicConsume(queueName, false, (consumerTag, delivery) -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String receivedMessage = new String(delivery.getBody());
//System.out.println("C1正常消费者接收到消息:" + receivedMessage);
if ("info3".equals(receivedMessage)){
System.out.println("C1正常消费者接收到消息:" + receivedMessage+"并且拒绝签收");
//禁止重新入队 false-不重新丢入队列
channel.basicReject(delivery.getEnvelope().getDeliveryTag(),false);
} else {
System.out.println("C1正常消费者接收到消息:" + receivedMessage);
//手动确认接收
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
},(consumerTag) -> {
System.out.println(consumerTag + "C1正常消费者取消消费消息");
});
}
在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配,所以处理速度快的消费者很大一部分时间处于空闲状态
处理速度快的消费者已将分配的消息消费完成
处理速度慢的消费者还未消费
channel . basicQos方法允许限制信道上的消费者所能保持的最大未确认消息的数量。如消费端程序调用了channel. basicQos(5),之后订阅了某个队列进行消费。RabbitMQ会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设定的上限,那么RabbitMQ就不会向这个消费者再发送任何消息。直到消费者确认了某条消息之后,RabbitMQ将相应的计数减1,之,后消费者可以继续接收消息,直到再次到达计数上限。
1.不公平分发
通过设置参数 channel.basicQos(1);来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送,实现不公平分发策略使得能者多劳。
还有一点需要注意,使用公平分发,必须关闭自动应答,改为手动应答。
通过RabbitMq的Web管理页面,可以看到Channels的Prefetch count属性显示为1则表示不公平分发成功;
2.预值分发
当消息被消费者接收后,但是没有确认,此时这里就存在一个未确认的消息缓冲区,用于存储非被确认的消息,该缓存区的大小是没有限制的。
预取值: 定义通道上允许的未确认消息的最大数量。一旦未确认消息数量达到配置的最大数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认;例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取值计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack;例如, tag=6 这个消息刚刚被确认 ACK,此时RabbitMQ 将会感知这个情况到并再发送一条消息。
如果消费者消费了大量的消息但是没有确认的话,就会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量。
这里如果是0的话是轮训分发,1的话是不公平分发,其它大于1值的话就是预取值,可以事先规定好给该队列分配几条数据
Qos的取值问题:
在传输效率和消费者消费速度之间做一个平衡。这个值是需要不断尝试的,因为太低,信道传输消息效率太低,如果太高,消费者来不及确认消息导致消息积累问题,内存消耗不断增大。
十一.消息消费的相关问题
1.什么是消息重复消费?
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;(消费者的 消息应答机制)。但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决方法
保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息的 幂等性;
解决方法一:send if not exist
- 首先将 RabbitMQ 的消息自动确认机制改为手动确认,然后每当有一条消息消费成功了,就把该消息的唯一ID记录在Redis 上,然后每次发送消息时,都先去 Redis 上查看是否有该消息的 ID,如果有,表示该消息已经消费过了,不再处理,否则再去处理。
- 利用数据库唯一约束实现幂等
解决方法二:insert if not exist
- 可以通过给消息的某一些属性设置唯一约束,比如增加唯一uuid,添加的时候查询是否存对应的uuid,存在不操作,不存在则添加,那样对于相同的uuid只会存在一条数据
解决方法三:sql的乐观锁
- 比如给用户发送短信,变成如果该用户未发送过短信,则给用户发送短信,此时的操作则是幂等性操作。但在实际上,对于一个问题如何获取前置条件往往比较复杂,此时可以通过设置版本号version,每修改一次则版本号+1,在更新时则通过判断两个数据的版本号是否一致。
基于本地消息表实现消息幂等性
- 创建本地消息表
CREATE TABLE `message_idempotent` (
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`message_content` varchar(2000) DEFAULT NULL COMMENT '消息内容',
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 生产者
@RestController
@Slf4j
public class QueueController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息幂等性
* */
@GetMapping("/sendMessage")
public void sendMessage(String msg, String routingKey, String id) {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setMessageId(id);
messageProperties.setContentType("text/plain");
messageProperties.setContentEncoding("utf-8");
Message message = new Message(msg.getBytes(), messageProperties);
log.info("生产消息:" + message.toString());
// 消息发送确认回调
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("DirectExchange-01", routingKey, message, correlationData);
}
}
- 消费者
@Component
@Slf4j
public class Consumer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 基于本地消息表实现消息幂等性
* @param message
*/
@RabbitListener(queues = "DirectQueue-01")
public void receiveMessage02(Message message, Channel channel) throws IOException {
String messageId = message.getMessageProperties().getMessageId();
String messageContent = new String(message.getBody(), StandardCharsets.UTF_8);
MessageIdempotent messageIdempotent = new MessageIdempotent();
QueryWrapper<MessageIdempotent> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("message_id", messageId);
MessageIdempotent msg = messageIdempotent.selectOne(queryWrapper);
if (ObjectUtil.isNull(msg)) {
messageIdempotent.setMessageId(messageId);
messageIdempotent.setMessageContent(messageContent);
messageIdempotent.insert();
log.info("DirectQueue-01-消费者收到消息,消息ID:" + messageId + " 消息内容:" + messageContent);
// 消息确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} else {
log.info("消息 " + messageId + " 已经消费过!");
}
}
基于 Redis 实现消息幂等性
- 消费者
public class Consumer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 基于 redis 实现消息幂等性
* @param message
*/
//@RabbitHandler
//@RabbitListener(queues = "DirectQueue-01")
public void receiveMessage01(Message message, Channel channel) throws IOException {
String messageId = message.getMessageProperties().getMessageId();
String messageContent = new String(message.getBody(), StandardCharsets.UTF_8);
// 消息不存在则创建,返回 true
Boolean exist = stringRedisTemplate.opsForValue().setIfAbsent(messageId, messageContent);
if (!exist) {
log.info("消息 " + messageId + " 已经消费过");
} else {
// 消息确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("DirectQueue-01-消费者收到消息,消息ID:" + messageId + " 消息内容:" + messageContent);
}
}
}
2.消息传输保障
一般消息中间件的消息传输保障分为三个层级。
- At most once:最多一次。消息可能会丢失,但是绝不会重复传输
- At least once:最少一次。消息绝不会丢失,但可能会重复传输
- Exactly once:刚好一次。每条消息肯定会被传输一次且仅传输一次
3.如何保证消息的顺序性
1个生产者,多个消费者。生成的顺序是数据1、数据2、数据3.消费的数据是数据2、数据1、数据3。没有按之前的顺序。
解决方法:搞3个Queue,每个消费者就消费其中的一个Queue。把需要保证顺序的数据发到1个Queue里去。
核心
要确保消息的顺序,只需要确保两点即可:发送有序,消费有序。
- 发送有序
正常来说,我们发送消息的时候都是按照既定的业务顺序发送的,这点是无疑的。所以发送有序本来不是啥大事,问题在于,有的时候我们的项目是集群化部署,同一个项目有多个实例,当多个不同的实例分布于不同的服务器上运行的时候,都向 MQ 发消息,此时就无法确保消息的有序了。
那么对于这种情况,我们可以考虑使用 Redis 分布式锁来实现,发送消息之前,先去 Redis 上获取到锁,能拿到锁,然后再去发送消息,避免并发发送。
- 消费有序
首先,同一个队列只能有一个消费者,如果存在多个消费者,则消费顺序就无法保证了。
其次,同一个队列不能开启并发消费,例如像下面这样的代码:
@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME,concurrency = "10")
public void handleMsg(String msg) {
logger.info("msg:{}", msg);
}
这个相当于建立了 10 个 channel 去同时消费消息,对于这种情况,也是没法保证消费的有序的,因为本地代码执行的快慢、是否抛异常等等,都有可能会影响到消息的顺序。
无法并发消费,就会导致消费性能下降,如果确实对性能又有比较高的要求,那么我们相同类型的队列可以创建多个,然后依然是每一个队列一个消费者即可。
4.如何解决消息积压
几千万条数据在MQ里,积压了七八个小时。这个时候就是恢复consumer的问题。让它恢复消费速度,然后傻傻地等待几个小时消费完毕。这个肯定不能再面试的时候说。1个消费者1秒时1000条,1秒3个消费者是3000条。1分钟是18万条。1个小时是1000多万条。如果积压了上万条数据,即使消费者恢复了,也大概需要1个多小时才能恢复过来。
- rabbitMq开多线程去处理数据
concurrency属性
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = LoadometerConstant.IOT_SEND_TO_PLATFORM, durable = "true"),
exchange = @Exchange(value = LoadometerConstant.IOT_SEND_TO_PLATFORM)),
concurrency = "1-10"
)
public void sendToPlatform(@Payload String message,
@Headers Map<String,Object> headers,
@Header(value = AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey)
使用 @Payload 和 @Headers 注解可以获取消息中的 body 和 headers 消息。它们都会被 MessageConvert 转换器解析转换后(使用 fromMessage 方法进行转换),将结果绑定在对应注解的方法中。
注意 :concurrency是2.x才有的属性,1.x的可以通过配置工厂的时候设置并发
@Bean
public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory mqConnectionFactory){
SimpleRabbitListenerContainerFactory listenerContainerFactory=new SimpleRabbitListenerContainerFactory();
listenerContainerFactory.setConnectionFactory(mqConnectionFactory);
listenerContainerFactory.setMessageConverter(new Jackson2JsonMessageConverter());
// 最大并发数
listenerContainerFactory.setMaxConcurrentConsumers(20);
// 初始并发数
listenerContainerFactory.setConcurrentConsumers(10);
return listenerContainerFactory;
}
prefetch属性
一次拉取的数量,每个customer会在MQ预取一些消息放入内存的LinkedBlockingQueue中进行消费,这个值越高,消息传递的越快,但非顺序处理消息的风险更高。如果ack模式为none,则忽略。如有必要,将增加此值以匹配txSize或messagePerAck。从2.0开始默认为250;设置为1将还原为以前的行为。
prefetch默认值以前是1,这可能会导致高效使用者的利用率不足。从spring-amqp 2.0版开始,默认的prefetch值是250,这将使消费者在大多数常见场景中保持忙碌,从而提高吞吐量。
不过在有些情况下,尤其是处理速度比较慢的大消息,消息可能在内存中大量堆积,消耗大量内存;以及对于一些严格要求顺序的消息,prefetch的值应当设置为1。
对于低容量消息和多个消费者的情况(也包括单listener容器的concurrency配置)希望在多个使用者之间实现更均匀的消息分布,建议在手动ack下并设置prefetch=1。
prefetch和concurrency
若一个消费者配置prefetch=10,concurrency=2,即会开启2个线程去消费消息,每个线程都会抓取10个线程到内存中(注意不是两个线程去共享内存中抓取的消息)。
- 临时扩容
- 先修改consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉。
- 新建1个topic,partition是原来的10倍,临时建立好原来10倍或者20倍的Queue。
- 然后写一个临时的分发数据的consumer程序,这个程序部署上去,消费积压的数据。消费之后,不做耗时的处理。直接均匀轮训写入临时建立好的10倍数量的Queue。
- 接着征用10倍的机器来部署consume。每一批consumer消费1个临时的queue。
- 这种做法,相当于将queue资源和consume资源扩大10倍,以10倍的速度来消费数据。
- 等快速消费完积压数据之后,恢复原来的部署架构,重新用原先的consumer来消费消息。
-
过期失效了怎么办
过期失效就是TTL。如果消息在Queue中积压超过一定的时间就会被RabbitMQ给清理掉。这个数据就没了。这就不是数据积压MQ中了,而是大量的数据会直接搞丢。
在这种情况下,增加consume消费积压就不起作用了。此时,只能将丢失的那批数据,写个临时的程序,一点一点查出来,然后再灌入MQ中,把白天丢失的数据补回来。 -
优化
消息生产者性能优化
如果我们的代码发送消息的性能有问题,我们可以检查一下是不是发消息之前的业务逻辑耗时太多了。
我们可以通过调整发送消息的批量大小,或者增加并发,来解决消息生产者面临的问题,至于应该选择批量发送还是增加并发,主要取决于发送端程序的业务性质。
线上运行的微服务,主要接收RPC请求处理在线业务,它对请求响应时延比较敏感,适合并发的方式来提升性能。
离线分析系统,不关心时延,更注重整个系统的吞吐量,这种情况适用于批量方式来提升性能。
消息消费者性能优化
我们要保证消费端的性能要好于生产端的发送性能,这样的系统才能健康的持续运行。
消费端除了优化消费业务之外,也可以通过水平扩容,增加消费端的并发数来提升性能,需要注意的是,在扩容消费者的实例数量的同时,必须同步扩容主题中的分区(队列)数量,确保Consumer的数量和分区的数量是尽量相等的。
如果Consumer的实例数量超过分区数量,这样的扩容是完全没有效果的,因为同一个分区(队列)同时只能有一个Consumer去处理消息。
- 增加队列消息存储上限-惰性队列
RabbitMQ加入了新的队列模式:Lazy Queue。使用这种队列的时候,这个队列不会将我们的消息放到内存中,而是在收到了消息直接写入磁盘当中,理论上是没有存储上限的,我们的磁盘有多大,那么我们就能存储多大的内存,这样子就可以处理消息堆积的问题了。
优点: 磁盘存储更为安全,并且存储无上限,避免了内存存储带来的Page Out问题,性能更为稳定。
缺点: 由于磁盘是存储在磁盘中,因此需要进行磁盘的IO,受到IO性能的限制。同时,消息时效性不如内存模式,不过在消息堆积的背景下,影响其实是不大的。
惰性队列
队列具备两种模式:default和lazy。默认的为default模式,在3.6.0的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式,那么只能先删除队列,然后再重新声明一个新的。
使用queueDeclare方法设置一个队列为惰性队列:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
基于命令行设置一个队列为惰性队列:
基于@Bean声明lazy-queue
基于@RabbitListener声明LazyQueue
特性
接收到消息后直接存入磁盘而非内存
消费者要消费消息时才会从磁盘中读取并加载到内存
支持数百万条的消息存储
5.优先队列
顾名思义,拥有高优先级的队列具有高的优先权,优先级高的消息具备优先被消费的权力
在rabbitmq中,优先队列有两种概念:
- 队列优先级
- 队列中的消息优先级
队列优先级
可以在声明队列的时候设置x-max-priority参数来定义一个优先队列:
Map<String, Object> map = new HashMap<>();
map.put("x-max-priority", 10);
channel.queueDeclare(Q_PRIORITY, true, false, false, map);
消息优先级
AMQP.BasicProperties low = new AMQP
.BasicProperties.Builder()
.priority(5)
.build();
可以发现,虽然消息high-priority比消息low-priority晚发送,但是优先级较高,所以消费者端优先消费high-priority消息
6.推拉模式
在rabbitmq中有两种消息处理的模式,一种是推模式/订阅模式/投递模式(也叫push模式),消费者调用channel.basicConsume方法订阅队列后,由RabbitMQ主动将消息推送给订阅队列的消费者;另一种是拉模式/检索模式(也叫pull模式),需要消费者调用channel.basicGet方法,主动从指定队列中拉取消息。
推模式(push)
优点
- 实时(服务端broker一收到消息就推给consumer),消息的实时性很高
缺点
- 容易造成消息堆积(消息保存在服务端broker)
- 加大server(broker)工作量,影响性能。
- 有的消费者机器配置好处理能力强,有的配置低处理能力低,但是server推相同数量级消息给消费者,就会导致消费者强的等待,弱的处理效率跟不上,从而导致崩溃。
- server资源相比消费者的资源肯定是更宝贵
拉模式(Pull)
- 如果只想从队列中获取单条消息而不是持续订阅,则可以使用channel.basicGet方法来进行消费消息。
- 增加消息延迟,降低系统吞吐量
- 实时性不高,短轮询间隔时间大,实时性就低。有其他优化方式,比如CMQ、kafka(腾讯云的服务)也会采用长轮询优化(长轮询如果拉取失败不会直接断开,而是挂在那里wait,如果服务端有新消息就返回最新数据)
结论
- 推模式更关注消息的实时性
- 推模式直接从内存缓冲区中获取消息,能有效的提高消息的处理效率以及吞吐量
- 拉模式更关注消费者的消费能力,只有消费者主动去拉取消息才会去获取消息
- 默认使用推消息,由于某些限制,消费者在某个条件成立时才能消费消息的场景需要从批量获取消息的场景
7.动态获取队列名
@RabbitListener(queues = “#{eventCameraVoiceLogQueue.name}”)
#{}是一个SpEL表达式,用于动态获取队列名
eg
配置
@Configuration
public class CanalQueueConfig {
@Autowired
private LoadometerServerConfig loadometerServerConfig;
@Autowired
public AmqpAdmin amqpAdmin;
private TopicExchange exchange = new TopicExchange("canal");
private String getQueueNamePrefix() {
return "db_tn_" + loadometerServerConfig.getScid().toLowerCase() + "_";
}
private Queue bindQueue(String name) {
Queue queue = new Queue(name, true);
amqpAdmin.declareQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(exchange).with(queue.getName());
amqpAdmin.declareBinding(binding);
return queue;
}
/**
* 语音提醒数据
*
* @return
*/
@Bean
public Queue eventCameraVoiceLogQueue() {
String name = getQueueNamePrefix() + "event_camera_voice_log";
return bindQueue(name);
}
}
@Autowired
private Queue eventCameraVoiceLogQueue;
@RabbitListener(queues = "#{eventCameraVoiceLogQueue.name}",concurrency = "1-10")
public void onEventCameraVoiceLogMessage(byte[] input)