MQ介绍
MQ:MessageQueue,消息队列。 队列,是一种FIFO 先进先出的数据结构。消息由生产者发送到MQ进行排队,然后按原来的顺序交由消息的消费者进行处理
MQ的作用主要有以下三个方面:
异步
:提高系统响应速度和吞吐量;(快递员放快递到菜鸟驿站,客户有时间再取)解耦
:减小服务间影响(提高稳定性),实现数据分发(发布/订阅模式,一个生产者对应多个消费者)削峰
:稳定的系统资源应对突发的流量冲击(防止突发流量造成服务宕机)
MQ优缺点
- 系统可用性降低(外部依赖增多,一旦MQ宕机,会对业务产生影响)
- 系统复杂性提高(同步变异步,引入消息消费问题,消息不丢失,顺序消费,重复消费等)
- 消息一致性问题(多个消费者如何保证同时成功/失败)
常用的MQ产品包括Kafka、RabbitMQ和RocketMQ
RabbitMQ安装
Releases · rabbitmq/erlang-rpm (github.com)下载地址
rpm -ivh rabbitmq-server-3.9.15-1.el7.noarch.rpm
# 启动服务,后台进程
service rabbitmq-server start
# 启动程序,前台运行,查看启动过程
rabbitmqctl start_app
# 状态
service rabbitmq-server status
# 关闭服务
rabbitmqctl stop_app
安装web控制台插件,重启后生效
rabbitmq-plugins enable rabbitmq_management
http://192.168.119.133:15672
创建管理员用户
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
集群搭建
普通模式
在这种集群模式下,集群各节点之间只会有相同的元数据
,而消息不会冗余,只存在一个节点,消费者在消费时,请求到了没有该消息的节点,RabbitMQ会临时在节点间进行数据传输。
这种模式下消息可靠性不高
。也不支持高可用,某一个节点挂了之后,需要重启服务后,才能让这个节点的消息正常消费。
镜像模式
RabbitMQ的官方HA高可用方案。需要在搭建了普通集群之后再补充搭建。其本质区别在于,这种模式会在镜像节点中间主动进行消息同步
,而不是在客户端拉取消息时临时同步。
并且在集群内部有一个算法会选举产生master
和slave
,当一个master挂了后,也会自动选出一个来。从而给整个集群提供高可用能力。
这种模式的消息可靠性更高,因为每个节点上都存着全量的消息
。而他的弊端也是明显的,集群内部的网络带宽会被这种同步通讯大量的消耗,进而降低整个集群的性能。这种模式下,队列数量最好不要过多。
1、同步集群节点的cookie,路径/var/lib/rabbitmq/.erlang.cookie
2、将worker1和worker3加入worker2的集群中rabbitmqctl join_cluster --ram rabbit@worker2
加入时首先要启动worker1和worker3上的服务,否则出现如下错误
NODENAME=rabbit@worker3
依次执行
service rabbitmq-server start
rabbitmqctl start_app
rabbitmqctl join_cluster --ram rabbit@worker2
加入集群后如下所示,rabbitmqctl cluster_status
查看集群状态
--ram
,表示节点的元数据(交换机、队列定义信息)只保存在内存中;此时存在单点故障,如果worker2节点宕机,元数据有可能丢失。所以官方不建议所有节点都使用ram。
通常在生产环境中,为了减少RabbitMQ集群之间的数据传输,在配置镜像策略时,会针对固定的虚拟主机virtual host来配置。
创建一个虚拟主机,并添加对应的镜像策略
rabbitmqctl add_vhost /mirror
rabbitmqctl set_policy ha-all --vhost "/mirror" "^" '{"ha-mode":"all"}'
通常镜像模式的集群已经足够满足大部分的生产场景了。虽然他对系统资源消耗比较高,但是在生产环境中,系统的资源都是会做预留的,所以正常的使用是没有问题的。但是在做业务集成时,还是需要注意队列数量不宜过多,并且尽量不要让RabbitMQ产生大量的消息堆积。
创建队列
基础概念
RabbitMQ是基于AMQP协议
(消息队列协议,用于生产者和消费者之间通信)开发的一个MQ产品
虚拟主机Virtual host
在一个RabbitMQ Server或集群中可以划分出多个虚拟主机,每一个虚拟主机都有AMQP的全套组件
,并且可以针对每个虚拟主机进行权限和数据分配;虚拟主机之间是完全隔离的
。
连接Connection
客户端与RabbitMQ进行交互,首先就需要建立一个TPC连接,这个连接就是 Connection。
信道Channel
一旦客户端与与RabbitMQ建立了连接,就会分配一个AMQP信道 Channel。每个信道有唯一ID,数据操作基本在信道Channel中展开。RabbitMQ为了减少性能开销,会在一个Connection中建立多个Channel,这样便于客户端进行多线程连接,这些连接会复用同一个Connection的TCP通道。
交换机Exchange
消息发送到RabbitMQ中后,会首先进入一个交换机,然后由交换机将数据转发到不同的队列中。RabbitMQ中有多种不同类型的交换机来支持不同的路由策略。
Direct Exchange
:根据消息的Routing key
,将消息路由到匹配的队列Topic Exchange
: 根据消息的Routing key
和通配符(*
匹配一个单词,#
匹配多个单词),将消息路由到匹配的队列Headers Exchange
: 根据消息的头部信息
路由消息到匹配队列,头部信息可以是任意键值对Fanout Exchange
: 将消息广播到与之绑定的所有队列
队列Queue
队列是实际保存数据的最小单位
,具备FIFO的特性,消息会被发到队列中,然后才被消费者消费
Classic经典队列
在单机环境中,拥有比较高的消息可靠性
Quorum仲裁队列
Quorum是基于Raft一致性
协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式
设计的。简单理解就是quorum队列中的消息需要有集群中半数节点同意确认
后,才会写入到队列中。这种队列类似于RocketMQ当中的DLedger集群。这种方式可以保证消息在集群内部不会丢失
。同时,Quorum是以牺牲很多高级队列特性为代价,来进一步保证消息在分布式环境下的高可靠。
Feature | Classic Mirrored | Quorum | 注释 |
---|---|---|---|
Non-durable queues | yes | no不支持 | 不持久化队列 |
Exclusivity | yes | no | 只能由声明队列的connection使用 |
Per message persistence | per message | always | |
Membership changes | automatic | manual | |
Message TTL (Time-To-Live) | yes | yes (since 3.10) | |
Queue TTL | yes | partially (lease is not renewed on queue re-declaration) | |
Queue length limits | yes | yes (except x-overflow: reject-publish-dlx) | |
Lazy behaviour | yes | always (since 3.10) | |
Message priority | yes | no | |
Consumer priority | yes | yes | |
Dead letter exchanges | yes | yes | |
Adheres to policies | yes | yes (see Policy support) | |
Poison message handling | no | yes | 毒消息 |
Global QoS Prefetch | yes | no |
Poison message handling
:毒消息,消息一直不能被消费者正常消费(消费失败或者消费逻辑有问题),就会导致消息不断的重新入队,造成性能浪费;Quorum队列会跟踪消息的失败次数,记录在x-delivery-count
头部参数中,然后通过设置Delivery limit
设置阈值,失败次数超过阈值就会删除消息,或者配置了死信队列,就进入对应的死信队列
声明Quorum队列
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","quorum");
//声明Quorum队列的方式就是添加一个x-queue-type参数,指定为quorum。默认是classic
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
Quorum队列更适合于队列长期存在,并且对容错、数据安全方面的要求比低延迟、不持久等高级队列更能要求更严格的场景。
例如 电商系统的订单,引入MQ后,处理速度可以慢一点,但是订单不能丢失。
不适用的场景
- 临时使用的队列,或者经常修改和删除的队列
- 对消息延迟要求高,Raft一致性算法会影响消息的延迟
- 对数据安全性要求不高,Quorum队列需要消费者手动通知或者生产者手动确认
- 队列消息积压严重或者消息很大,Quorum队列会将所有消息始终保存在内存中,直到撑爆内存
Stream队列
Stream队列是RabbitMQ自3.9.0版本开始引入的一种新的数据队列类型,也是目前官方最为推荐的队列类型。这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。
Stream队列的核心是以append-only的方式将消息持久化到日志文件中,然后通过调整消费者的消费进度offset,实现消息的多次分发。以下是4个主要特点
- 大规模分发,已有的消息队列是一个消费者绑定一个专用队列,Stream队列允许任意数量的消费者使用同一个队列
- 消息回溯,已有的队列中,消息被消费者消费完后,会从队列中删除,无法读取消费过的消息,Stream允许消费者从日志的任何节点开始重新读取消息
- 高吞吐量
- 大日志,已有的队列中积累的消息过多时,性能下降会非常明显,Stream队列的设计目标以最小的内存开销存储大量数据
功能对比
Feature | Classic | Stream |
---|---|---|
Non-durable queues | yes | no |
Exclusivity | yes | no |
Per message persistence | per message | always |
Membership changes | automatic | manual |
TTL | yes | no (but see Retention) |
Queue length limits | yes | no (but see Retention) |
Lazy behaviour | yes | inherent |
Message priority | yes | no |
Consumer priority | yes | no |
Dead letter exchanges | yes | no |
Adheres to policies | yes | (see Retention) |
Reacts to memory alarms | yes | no (uses minimal RAM) |
Poison message handling | no | no |
Global QoS Prefetch | yes | no |
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","stream");
params.put("x-max-length-bytes", 20_000_000_000L); // 日志文件的最大字节数: 20 GB
params.put("x-stream-max-segment-size-bytes", 100_000_000); // 每一个日志文件的最大大小: 100 MB
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
RabbitMQ编程模型
基础模型
成功发送消息
for (int i = 100; i < 200; i++) {
String newMessage = String.format("亚索%d级了", i);
channel.basicPublish("", QUEUE_NAME, null, newMessage.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + newMessage + "'");
}
消费2个消息
pull,主动从服务器上获取消息
GetResponse response = channel.basicGet(QUEUE_NAME, true);
push,服务端推送消息过来,执行回调函数
channel.basicConsume(QUEUE_NAME, true, myconsumer);
官网模型
1、Hello World
Producer端发送一个消息到指定的queue,中间不需要任何exchange规则,Consumer端按queue消费
Producer
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
------------------------------------------------------------------------------
Consumer:
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
GetResponse response = channel.basicGet(QUEUE_NAME, true);
2、Work Queues
Producer:发消息到目标队列
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); //持久化消息
channel.basicPublish("", TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
------------------------------------------------------------------------------
Consumer:
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
channel.basicQos(1);
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
3、Publish/Subscribe
发布/订阅机制
Producer只负责发送消息到Exchange,再由Exchange(type=fanout)分配到与该Exchange绑定的Queue,消费者创建队列绑定到Exchange上
Producer:
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
------------------------------------------------------------------------------
Consumer: 绑定到Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
4、Routing
Producer只负责发送消息到Exchange,Exchange(type=direct)根据routingkey
将不同类别的消息分发到不同的Queue
Producer:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.basicPublish(EXCHANGE_NAME, ro utingKey, null, message.getBytes("UTF-8"));
------------------------------------------------------------------------------
Consumer:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);
5、Topics
Producer只负责发送消息到Exchange,Exchange(type=direct)根据routingkey
将不同类别的消息分发到不同的Queue,routingkey
支持模糊匹配,单词间用.
隔开,*
代表一个单词,#
代表0个或多个单词
Producer:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.basicPublish(EXCHANGE_NAME, "hero.yasuo.lol", null, message.getBytes("UTF-8"));
------------------------------------------------------------------------------
Consumer:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueBind(queueName, EXCHANGE_NAME, "*.*.lol");
channel.queueBind(queueName, EXCHANGE_NAME, "#.lol");
channel.basicConsume(queueName, true, consumer);
6、RPC远程调用
异步降级为同步调用,不推荐使用
7、Publish Confirms
保证生产者发送成功,发送者发送消息的基础APIProducer.basicPublish方法
是没有返回值的,也就是说,一次发送消息是否成功,生产者是不知道的,这在业务上就容易造成消息丢失。而这个模块就是通过给发送者提供一些确认机制,来保证这个消息发送的过程是成功的。