RabbitMQ:
什么是MQ(消息队列):
消息的发送者和接收者不需要同时与消息队列互交。消息会保存在队列中,直到接收者取回它。
三个特点:异步,解耦,流量削峰。
实现:
消息队列常常保存在链表结构中。拥有权限的进程可以向消息队列中写入或读取消息。
当前使用较多的消息队列有 RabbitMQ(数据不会丢失,常用) 、 RocketMQ (有些不开源)、 ActiveMQ(并发量太小) 、 Kafka(消息丢失) 、 ZeroMQ 、 MetaMq等,而部分数据库如 Redis 、 Mysql 以及 phxsql 也可实现消息队列的功能。
2.2. 特点
MQ是消费者-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。MQ和JMS类似,但不同的是JMS是SUN JAVA消息中间件服务的一个标准和API定义,而MQ则是遵循了AMQP协议的具体实现和产品。
注意:
-
AMQP ,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
-
JMS ,Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。 Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。常见的消息队列,大部分都实现了JMS API,如 ActiveMQ , Redis 以及 RabbitMQ 等。
2.3. 优缺点
优点
解耦、异步处理、流量削锋
缺点
系统可用性降低、系统复杂性增加
2.4. 使用场景
消息队列,是分布式系统中重要的组件,其通用的使用场景可以简单地描述为:当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。
在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
2.5. 为什么使用RabbitMQ
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如: Python 、 Ruby 、 .NET 、 Java 、 JMS 、 C 、 PHP 、 ActionScript 、 XMPP 、 STOMP 等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
总结如下:
基于AMQP协议
高并发(是一个容量的概念,服务器可以接受的最大任务数量)
高性能(是一个速度的概念,单位时间内服务器可以处理的任务数)
高可用(是一个持久的概念,单位时间内服务器可以正常工作的时间比例)
强大的社区支持,以及很多公司都在使用
支持插件
支持多语言
4.4. ConnectionFactory**、Connection、**Channel
ConnectionFactory 、 Connection 、 Channel 都是RabbitMQ对外提供的API中最基本的对象。
Connection 是RabbitMQ的 socket 连接,它封装了 socket 协议相关部分逻辑。
ConnectionFactory 为Connection的制造工厂。
Channel 是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义 Queue 、定义 Exchange 、绑定 Queue 与 Exchange 、发布消息等。
5. 简单模式队列
https://www.rabbitmq.com/tutorials/tutorial-one-java.html
1.“Hello,World!”:
/**
* 入门案例-生产者
*/
public class Send {
//定义队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂设置
factory.setHost("192.168.10.100");
factory.setVirtualHost("/shop");
factory.setUsername("shop");
factory.setPassword("shop");
//根据连接工厂创建连接
try (Connection connection = factory.newConnection();
//根据连接创建信道
Channel channel = connection.createChannel()) {
/**
* 声明队列
* 第一个参数queue:队列名称
* 第二个参数durable:是否持久化
* 第三个参数Exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。
* 这里需要注意三点:
* 1. 排他队列是基于连接可见的,同一连接的不同通道是可以同时访问同一个连接创建的排他队列的。
* 2. "首次",如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同。
* 3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。
* 这种队列适用于只限于一个客户端发送读取消息的应用场景。
* 第四个参数Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。
* 这种队列适用于临时队列。
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
//发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
/**
* 入门案例-消费者
*/
public class Recv {
//定义队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂设置
factory.setHost("192.168.10.100");
factory.setVirtualHost("/shop");
factory.setUsername("shop");
factory.setPassword("shop");
//根据工厂创建连接
Connection connection = factory.newConnection();
//根据连接创建信道
Channel channel = connection.createChannel();
//绑定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
/**
* 消费队列
* 1.队列名称
* 2.自动确认
* 3.收到的消息的实体类
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
}
}
问题:如果任务量很大,消息得不到及时的消费会造成队列积压,问题非常严重,比如内存溢出,消息丢失等。
解决:配置多个消费者消费消息。
总结:简单队列-处理消息效率不高,吞吐量较低,不适合生成环境
6. Work queues-工作模式队列
6.1. 工作模式队列-消息轮询分发(Round-robin)
问题:任务量很大,消息虽然得到了及时的消费,单位时间内消息处理速度加快,提高了吞吐量,可
是不同消费者处理消息的时间不同,导致部分消费者的资源被浪费。
解决:采用消息公平分发。
总结:工作队列-消息轮询分发-消费者收到的消息数量平均分配,单位时间内消息处理速度加快,提高了吞吐量。
6.2. 工作模式队列**-**消息公平分发(fair dispatch)
消费者:
//限制每次只发送一条,消费者处理完在发送下一条
int prefetchCount = 1;
channel.basicQos(prefetchCount);
/**
* 手动确认
* 1. 消息实体里的唯一标识
* 2. 是否多条确认
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
/**
* 消费队列
* 1.队列名称
* 2.自动确认
* 3.收到的消息的实体类
*/
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {});
总结:工作队列-公平轮询分发-根据不同消费者机器硬件配置,消息处理速度不同,收到的消息数量也不同,通常速度快的处理的消息数量比较多,最大化使用计算机资源。适用于生成环境。
7. Publish/Subscribe-消息的发布与订阅模式队列
queueName为唯一标识
fanout类型
广播形式
问题:生产者产生的消息所有消费者都可以消费,可不可以指定某些消费者消费呢?
解决:采用direct路由模式
8. Routing-路由模式队列
direct类型
问题:生产者产生的消息如果场景需求过多需要设置很多路由规则,可不可以减少?
解决:采用topic主题模式。
9. Topics-主题模式队列(常用)
topic类型
问题:RabbitMQ本身是基于异步的消息处理,是否可以同步实现?
解决:采用RPC模式。
10. RPC-远程过程调用模式队列
同步的
11. RabbitMQ消息的事务机制
在使用RabbitMQ的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的消息丢失,除此之外我们还会遇到一个问题,当消息的发布者在将消息发送出去之后,消息到底有没有正确到达broker代理服务器呢?如果不进行特殊配置的话,默认情况下发布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达broker的,如果在消息到达broker之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,你怎么进行持久化,那么这个问题该怎么解决呢?
RabbitMQ为我们提供了两种方式:
通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案;
通过将channel设置成confirm模式来实现;
11.1. AMQP事物机制控制
RabbitMQ中与事务机制有关的方法有三个: txSelect() , txCommit() 以及 txRollback(), txSelect() 用于将当前channel设置成transaction模式, txCommit() 用于提交事务,txRollback() 用于回滚事务,在通过 txSelect() 开启事务之后,我们便可以发布消息给broker代理服务器了,如果 txCommit() 提交成功了,则消息一定到达了broker了,如果在 txCommit() 执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback() 回滚事务。
事务确实能够解决producer与broker之间消息确认的问题,只有消息成功被broker接受,事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作同时进行消息重发,但是使用事务机制的话会降低RabbitMQ的性能,那么有没有更好的方法既能保障producer知道消息已经正确送到,又能基本上不带来性能上的损失呢?从AMQP协议的层面看是没有更好的方法,但是RabbitMQ提供了一个更好的方案,即将channel信道设置成confirm模式。
12. confirm确认模式
通过AMQP协议层面为我们提供了事务机制解决了这个问题,但是采用事务机制实现会降低RabbitMQ的消息吞吐量,此时处理AMQP协议层面能够实现消息事物控制外,我们还有第二种方式即:Confirm模式。
12.3. 同步Confirm
12.4. 异步confirm
异步confirm模式的编程实现最复杂,Channel对象提供的 ConfirmListener() 回调方法只包含deliveryTag (当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个 unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次 handleAck 方法, unconfirm 集合删掉相应的一条 (multiple=false) 或多条 (multiple=true) 记录。从程序运行效率上看,这个 unconfirm 集合最好采用有序集合SortedSet存储结构。实际上, waitForConfirms() 方法也是通过SortedSet维护消息序号的。
异步模式的优点就是执行效率高,不需要等待消息执行完,只需要监听消息即可。
13. Spring集成RabbitMQ
官网:https://spring.io/projects/spring-amqp
13.1. 为什么使用spring AMQP?
基于Spring之上社区活跃
对AMQP协议进行了高度的封装
极大的简化了RabbitMQ的操作
易用性、可扩展
13.7. 总结
当然这是官网最简单的例子,以后如果项目是基于配置来做的话要掌握以下:
-
pom中引用jar
-
先配置rabbitmq的配置
-
先配置ConnectionFactory
-
配置RabbitAmdmin
-
-
配置RabbitTemplate这里通常在配置一个Message Convert使用JSON进行数据格式的传输
-
配置Exchange
-
配置Queue
-
配置一个消息处理的bean或者通过Spring扫描,这个Bean最后继承MessageListener 来处理JSON数 据
极大的简化了RabbitMQ的操作
易用性、可扩展
13.7. 总结
当然这是官网最简单的例子,以后如果项目是基于配置来做的话要掌握以下:
-
pom中引用jar
-
先配置rabbitmq的配置
-
先配置ConnectionFactory
-
配置RabbitAmdmin
-
-
配置RabbitTemplate这里通常在配置一个Message Convert使用JSON进行数据格式的传输
-
配置Exchange
-
配置Queue
-
配置一个消息处理的bean或者通过Spring扫描,这个Bean最后继承MessageListener 来处理JSON数 据
-
配置Listener Container