RabbitMQ三两事

什么是队列(queue)?
queue在计算机科学中随处可见,Queue是一个存储、组织数据的数据结构,其最大的特性就是FIFO;

什么是消息队列(MQ)?
服务之间最常见的通信方式是直接调用彼此来通信,消息从一端发出后立即就可以达到另一端,称为即时消息通讯(同步通信);
消息从某一端发出后,首先进入一个容器进行临时存储,当达到某种条件后,再由这个容器发送给另一端,称为延迟消息通讯 (异步通信)
而容器的一个具体实现就是MQ(Message Queue);

MQ是干什么用的?
应用解耦、异步、流量削锋、数据分发、错峰流控、日志收集等等…

主流MQ
当前市面上mq的产品很多,比如RabbitMQ、Kafka、ActiveMQ、ZeroMQ和阿里巴巴捐献给Apache的RocketMQ。甚至连redis这种NoSQL都支持MQ的功能。

RabbitMQ 概述
RabbitMQ是一种基于erlang语言开发的流行的开源消息中间件,或者说是一个消息队列系统.它是对AMQP协议的实现,支持多种客户端,可以对来自客户端的异步消息进行存储转发,在易用性、扩展性、高可用性等方面表现不俗.

AMQP协议
一个提供统一消息服务的应用层标准高级消息队列协议,是一个通用的应用层协议
消息发送与接受的双方遵守这个协议可以实现异步通讯。这个协议约定了消息的格式和工作方式。

Erlang语言
Erlang语言最初用于交换机领域的架构模式,这样使得RabbitMQ在Broker之间进行数据交互的性能非常优秀(Erlang有着和原生Socket一样的延迟)。

RabbitMQ的优势:

  • 可靠性(Reliablity):使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。

  • 灵活的路由(Flexible Routing):在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。

  • 消息集群(Clustering):多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。

  • 高可用(Highly Avaliable Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

  • 多种协议(Multi-protocol):支持多种消息队列协议,如STOMP、MQTT等。

  • 多种语言客户端(Many Clients):几乎支持所有常用语言,比如Java、.NET、Ruby等。

  • 管理界面(Management UI):提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。

  • 跟踪机制(Tracing):如果消息异常,RabbitMQ提供了消息的跟踪机制,使用者可以找出发生了什么。

  • 插件机制(Plugin System):提供了许多插件,来从多方面进行扩展,也可以编辑自己的插件

RabbitMQ 应用场景

  • 解耦: 在单体应用通常可以使用内存队列,如Java的BlockingQueue来进行不同模块间的信息传递.而将单体应用拆分为分布式系统之后,可以通过RabbitMQ这种进程间队列来在各子系统之间进行消息传递,从而达到解耦的作用;

  • 流量削峰: RabbitMQ还可以被用在高并发系统当中的流量削峰,即将请求流量数据临时存放到RabbitMQ当中,从而避免大量的请求流量直接达到后台服务,把后台服务冲垮.通过使用RabbitMQ来存放这些请求流量,后台服务从RabbitMQ中消费数据,从而达到流量削峰的目的.

  • 消息通讯: 除了系统解耦和流量削峰外,RabbitMQ也常用于消息通讯,即可以用于实现IM聊天系统.

rabbitmq原理
RabbitMQ三两事_第1张图片

  • Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
  • Broker:标识消息队列服务器实体.
  • Virtual Host:每一个RabbitMQ服务器都能创建多个虚拟消息服务器,我们称之为虚拟主机.每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的交换机、队列、绑定等,拥有自己的权限机制.vhost相对于RabbitMQ就像虚拟机之于物理机一样.他们通过在各个实例间提供逻辑上的分离,允许不同的应用程序安全保密的运行数据,这很有用,它既能将同一个Rabbit的众多客户区分开来,又可以避免队列和交换器的命名冲突.RabbitMQ提供了开箱即用的默认的虚拟主机“/”,如果不需要多个vhost可以直接使用这个默认的vhost,通过使用缺省的guest用户名和guest密码来访问默认的vhost.
  • Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
  • Banding:绑定,用于消息队列和交换机之间的关联。一个绑定就是基于路由键将交换机和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
  • Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
  • Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟链接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。
  • Connection:网络连接,比如一个TCP连接。
  • Consumer:消息的消费者,表示一个从一个消息队列中取得消息的客户端应用程序。
  • Message:消息,消息是不具名的,它是由消息头和消息体组成。消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。

RabbitMQ三两事_第2张图片
RabbitMQ的Exchange类型
Exchange分发消息时,根据类型的不同分发策略有区别。目前共四种类型:direct、fanout、topic、headers(headers匹配AMQP消息的header而不是路由键(Routing-key),此外headers交换器和direct交换器完全一致,但是性能差了很多,目前几乎用不到了。所以直接看另外三种类型。)。

  • Direct exchange 直接交换器(默认)
    原理是通过消息中的routing key,与binding 中的binding-key 进行比对,若二者匹配,则将消息发送到这个消息队列。

  • Fanout exchange 广播式交换器
    每个发到fanout类型交换器的消息都会分到所有绑定的队列上去。fanout交换器不处理该路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。

  • topic exchange 主题交换器
    topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键(routing-key)和绑定键(bingding-key)的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:"#“和”*"。#匹配0个或多个单词,匹配不多不少一个单词。

  1. direct是将消息放到exchange绑定的一个queue里(一对一);

  2. fanout是将消息放到exchange绑定的所有queue里(一对所有);

  3. topic类型的exchange可以实现(一对部分)把消息放到exchange绑定的一部分queue里,或者多个routing key可以路由到一个queue里.

rabbitmq消息的可靠性

1️⃣设置交换机、队列和消息都为持久化;
2️⃣生产者消息确认机制;
3️⃣消费者消息确认机制;
4️⃣死信队列.

1.交换机的持久化


ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel =connection.createChannel();
 
#该交换机默认没有持久化
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

使用这种方法声明的交换机,默认不是持久化的,在服务器重启之后,交换机会消失
另一种方法声明交换机:

try {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection=factory.newConnection();
    Channel channel = connection.createChannel();
  
  #该交换机实现了持久化
  channel.exchangeDeclare(EXCHANGE_NAME,"fanout",true);
} catch (IOException e) {
    e.printStackTrace();
} catch (TimeoutException e) {
    e.printStackTrace();
}

exchangeDeclare() 第三个参数durable,如果为true时则表示要做持久化,当服务重启时,交换机依然存在.
2. 队列的持久化
与交换机的持久化相同,队列的持久化也是通过durable参数实现的,默认生成的随机队列不是持久化的.

onnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

#第2个参数用于进行持久化设置
#第3个参数是否为排他队列。如果一个队列被声明为排他队列,
那么这个队列只能被第一次声明他的连接所见,并在连接断开的时候自动删除
#第3个参数是自动删除.true,当没有任何消费者订阅该队列时,队列会被自动删除
#第四个参数是其它参数.
channel.queueDeclare(QUEUE_NAME, true, false, false, null);

3. 消息的持久化
消息的持久化是指当消息从交换机发送到队列之后,被消费者消费之前,服务器突然宕机重启,消息仍然存在.
消息持久化的前提是队列持久化,假如队列不是持久化,那么消息的持久化毫无意义.

通过如下代码设置消息的持久化:

channel.basicPublish(EXCHANGE_NAME,"",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

注意:
持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,只有在内存吃紧的时候才会从内存中清除.

非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间.

以上就是关于RabbitMQ中持久化的一些内容,但是并不会严格的100%保证信息不会丢失.
4.消息确认机制

  • 消息发送确认
  1. 通过实现ConfirmCallBack接口,消息发送到交换机Exchange后触发该回调.
  2. 通过实现ReturnCallback接口,如果消息从交换机发送到对应的队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发该回调). 使用该功能需要开启确认,spring-boot中配置如下:
spring.rabbitmq.publisher-returns = true
  • 消息接收确认
  1. spring.rabbitmq.listener.simple.acknowledge-mode = AUTO
    AcknowledgeMode.NONE: 不确认
    AcknowledgeMode.AUTO: 自动确认
    AcknowledgeMode.MANUAL: 手动确认
  2. 手动确认

2.1成功确认

void basicAck(long deliveryTag, boolean multiple) throws IOException;

#deliveryTag:该消息的index;
#multiple: 是否批量. true: 将一次性ack所有小于deliveryTag的消息.
#消费者成功处理后,调用
#channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)
#方法对消息进行确认.

2.2 失败确认

void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
 
#deliveryTag:该消息的index.
#multiple:是否批量. true:将一次性拒绝所有小于deliveryTag的消息.
#requeue:被拒绝的消息是否重新进入队列.
void basicReject(long deliveryTag, boolean requeue) throws IOException;
 
#deliveryTag:该消息的index.
#requeue: 被拒绝的是否重新入队列.

区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息.

5.死信队列

TTL

TTL(Time To Live):生存时间。RabbitMQ支持消息的过期时间,一共两种。

在消息发送时可以进行指定。通过配置消息体的properties,可以指定当前消息的过期时间。
在创建Exchange时可进行指定。从进入消息队列开始计算,只要超过了队列的超时时间配置,那么消息会自动清除。

死信队列DLX

死信队列(DLX Dead-Letter-Exchange):利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。

DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。

当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。

可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ3.0之前支持的immediate参数的功能。

消息变成死信的几种情况:

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

死信队列设置:需要设置死信队列的exchange和queue,然后通过routing key进行绑定。只不过我们需要在队列加上一个参数即可。

  • x-dead-letter-exchange: 用来设置死信后发送的交换机
  • x-dead-letter-routing-key: 用来设置死信的routingKey
Map<String, Object> arguments = Maps.newHashMapWithExpectedSize(3);
arguments.put("x-message-ttl", dlx-ttl);
arguments.put("x-dead-letter-exchange","exchange-name");
arguments.put("x-dead-letter-routing-key", "routing-key");
Queue ret = QueueBuilder.durable("queue-name".withArguments(arguments).build();

只需要通过监听该死信队列即可处理死信消息。还可以通过死信队列完成延时队列。

消费端自定义监听(推模式和拉模式pull/push)
一般通过while循环进行consumer.nextDelivery()方法进行获取下一条消息进行那个消费。(通过while将拉模式模拟成推模式,但是死循环会耗费CPU资源。)
通过自定义Consumer,实现更加方便、可读性更强、解耦性更强的方式。(现默认使用的模式,直接订阅到queue上,如果有数据,就等待mq推送过来)

Basic.Consume将信道(Channel)置为接收模式,直到取消队列的订阅为止。
在接受模式期间,RabbitMQ会不断的推送消息给消费者。
当然推送消息的个数还是受Basic.Qos的限制。
如果只想从队列获得单条消息而不是持续订阅,建议还是使用Basic.Get进行消费。
但是不能将Basic.Get放在一个循环里来代替Basic.Consume,这样会严重影响RabbitMQ的性能。
如果要实现高吞吐量,消费者理应使用Basic.Consume方法。

RabbitMQ三两事_第3张图片

你可能感兴趣的:(RabbitMQ三两事)