RabbitMQ基础概念和编程模型

971 阅读8分钟

MQ介绍

MQ:MessageQueue,消息队列。 队列,是一种FIFO 先进先出的数据结构。消息由生产者发送到MQ进行排队,然后按原来的顺序交由消息的消费者进行处理

MQ的作用主要有以下三个方面:

  • 异步:提高系统响应速度和吞吐量;(快递员放快递到菜鸟驿站,客户有时间再取)
  • 解耦:减小服务间影响(提高稳定性),实现数据分发(发布/订阅模式,一个生产者对应多个消费者)
  • 削峰:稳定的系统资源应对突发的流量冲击(防止突发流量造成服务宕机)

MQ优缺点

  • 系统可用性降低(外部依赖增多,一旦MQ宕机,会对业务产生影响)
  • 系统复杂性提高(同步变异步,引入消息消费问题,消息不丢失,顺序消费,重复消费等)
  • 消息一致性问题(多个消费者如何保证同时成功/失败)

常用的MQ产品包括Kafka、RabbitMQ和RocketMQ

image.png

RabbitMQ安装

Releases · rabbitmq/erlang-rpm (github.com)下载地址

image.png

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

image.png

安装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高可用方案。需要在搭建了普通集群之后再补充搭建。其本质区别在于,这种模式会在镜像节点中间主动进行消息同步,而不是在客户端拉取消息时临时同步。

并且在集群内部有一个算法会选举产生masterslave,当一个master挂了后,也会自动选出一个来。从而给整个集群提供高可用能力。

这种模式的消息可靠性更高,因为每个节点上都存着全量的消息。而他的弊端也是明显的,集群内部的网络带宽会被这种同步通讯大量的消耗,进而降低整个集群的性能。这种模式下,队列数量最好不要过多。

1、同步集群节点的cookie,路径/var/lib/rabbitmq/.erlang.cookie

2、将worker1和worker3加入worker2的集群中rabbitmqctl join_cluster --ram rabbit@worker2 加入时首先要启动worker1和worker3上的服务,否则出现如下错误

image.png

NODENAME=rabbit@worker3

依次执行
service rabbitmq-server start
rabbitmqctl start_app
rabbitmqctl join_cluster --ram rabbit@worker2

加入集群后如下所示,rabbitmqctl cluster_status查看集群状态

image.png

--ram,表示节点的元数据(交换机、队列定义信息)只保存在内存中;此时存在单点故障,如果worker2节点宕机,元数据有可能丢失。所以官方不建议所有节点都使用ram。


通常在生产环境中,为了减少RabbitMQ集群之间的数据传输,在配置镜像策略时,会针对固定的虚拟主机virtual host来配置。

创建一个虚拟主机,并添加对应的镜像策略

rabbitmqctl add_vhost /mirror

rabbitmqctl set_policy ha-all --vhost "/mirror" "^" '{"ha-mode":"all"}'

通常镜像模式的集群已经足够满足大部分的生产场景了。虽然他对系统资源消耗比较高,但是在生产环境中,系统的资源都是会做预留的,所以正常的使用是没有问题的。但是在做业务集成时,还是需要注意队列数量不宜过多,并且尽量不要让RabbitMQ产生大量的消息堆积。

创建队列

image.png

基础概念

RabbitMQ是基于AMQP协议(消息队列协议,用于生产者和消费者之间通信)开发的一个MQ产品

image.png

虚拟主机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是以牺牲很多高级队列特性为代价,来进一步保证消息在分布式环境下的高可靠。

FeatureClassic MirroredQuorum注释
Non-durable queuesyesno不支持不持久化队列
Exclusivityyesno只能由声明队列的connection使用
Per message persistenceper messagealways
Membership changesautomaticmanual
Message TTL (Time-To-Live)yesyes (since 3.10)
Queue TTLyespartially (lease is not renewed on queue re-declaration)
Queue length limitsyesyes (except x-overflow: reject-publish-dlx)
Lazy behaviouryesalways (since 3.10)
Message priorityyesno
Consumer priorityyesyes
Dead letter exchangesyesyes
Adheres to policiesyesyes (see Policy support)
Poison message handlingnoyes毒消息
Global QoS Prefetchyesno

Poison message handling:毒消息,消息一直不能被消费者正常消费(消费失败或者消费逻辑有问题),就会导致消息不断的重新入队,造成性能浪费;Quorum队列会跟踪消息的失败次数,记录在x-delivery-count头部参数中,然后通过设置Delivery limit设置阈值,失败次数超过阈值就会删除消息,或者配置了死信队列,就进入对应的死信队列

声明Quorum队列

Map<String,Object> paramsnew HashMap<>();  
params.put("x-queue-type","quorum");  
//声明Quorum队列的方式就是添加一个x-queue-type参数,指定为quorum。默认是classic  
channel.queueDeclare(QUEUE_NAME, truefalsefalse, params);

Quorum队列更适合于队列长期存在,并且对容错、数据安全方面的要求比低延迟、不持久等高级队列更能要求更严格的场景。

例如 电商系统的订单,引入MQ后,处理速度可以慢一点,但是订单不能丢失。

不适用的场景

  • 临时使用的队列,或者经常修改和删除的队列
  • 对消息延迟要求高,Raft一致性算法会影响消息的延迟
  • 对数据安全性要求不高,Quorum队列需要消费者手动通知或者生产者手动确认
  • 队列消息积压严重或者消息很大,Quorum队列会将所有消息始终保存在内存中,直到撑爆内存

Stream队列

Stream队列是RabbitMQ自3.9.0版本开始引入的一种新的数据队列类型,也是目前官方最为推荐的队列类型。这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。

Stream队列的核心是以append-only的方式将消息持久化到日志文件中,然后通过调整消费者的消费进度offset,实现消息的多次分发。以下是4个主要特点

  • 大规模分发,已有的消息队列是一个消费者绑定一个专用队列,Stream队列允许任意数量的消费者使用同一个队列
  • 消息回溯,已有的队列中,消息被消费者消费完后,会从队列中删除,无法读取消费过的消息,Stream允许消费者从日志的任何节点开始重新读取消息
  • 高吞吐量
  • 大日志,已有的队列中积累的消息过多时,性能下降会非常明显,Stream队列的设计目标以最小的内存开销存储大量数据

功能对比

FeatureClassicStream
Non-durable queuesyesno
Exclusivityyesno
Per message persistenceper messagealways
Membership changesautomaticmanual
TTLyesno (but see Retention)
Queue length limitsyesno (but see Retention)
Lazy behaviouryesinherent
Message priorityyesno
Consumer priorityyesno
Dead letter exchangesyesno
Adheres to policiesyes(see Retention)
Reacts to memory alarmsyesno (uses minimal RAM)
Poison message handlingnono
Global QoS Prefetchyesno
Map<String,Object> paramsnew 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, truefalsefalse, params);

RabbitMQ编程模型

基础模型

成功发送消息

image.png

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个消息

image.png

pull,主动从服务器上获取消息
GetResponse response = channel.basicGet(QUEUE_NAME, true);

push,服务端推送消息过来,执行回调函数
channel.basicConsume(QUEUE_NAME, true, myconsumer);

官网模型

1、Hello World

image.png

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, falsefalsefalsenull);
GetResponse response = channel.basicGet(QUEUE_NAME, true);

2、Work Queues

image.png

Producer:发消息到目标队列

channel.queueDeclare(TASK_QUEUE_NAME, truefalsefalsenull); //持久化消息
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

发布/订阅机制

image.png

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

image.png

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个或多个单词

image.png

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方法是没有返回值的,也就是说,一次发送消息是否成功,生产者是不知道的,这在业务上就容易造成消息丢失。而这个模块就是通过给发送者提供一些确认机制,来保证这个消息发送的过程是成功的。