什么是队列(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的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个或多个单词,匹配不多不少一个单词。
direct是将消息放到exchange绑定的一个queue里(一对一);
fanout是将消息放到exchange绑定的所有queue里(一对所有);
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.消息确认机制
spring.rabbitmq.publisher-returns = true
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参数的功能。
消息变成死信的几种情况:
死信队列设置:需要设置死信队列的exchange和queue,然后通过routing key进行绑定。只不过我们需要在队列加上一个参数即可。
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方法。