消息中间件概念
消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
面向消息的系统(消息中间件)是在分布式系统中完成消息的发送和接收的基础软件。
异步处理、流量削峰、限流、缓冲、排队、最终一致性、消息驱动等需求的场景都可以使用消息中间件。
自定义消息中间件
- **BlockingQueue(阻塞队列)**是java中常见的容器,在多线程编程中被广泛使用。
- 当队列容器已满时生产者线程被阻塞,直到队列未满后才可以继续put;
- 当队列容器为空时,消费者线程被阻塞,直至队列非空时才可以继续take。
主流消息中间件及选型
选取原则
- 消息传输的可靠性:保证消息不会丢失。
- 支持集群,包括横向扩展,单点故障都可以解决。
- 性能要好,要能够满足业务的性能需求。
Rabbitmq
优点
- 轻量级,快速,部署使用方便
- 支持灵活的路由配置。RabbitMQ中,在生产者和队列之间有一个交换器模块。根据配置的路由规则,生产者发送的消息可以发送到不同的队列中。路由规则很灵活,还可以自己实现。
- RabbitMQ的客户端支持大多数的编程语言。
缺点
- 如果有大量消息堆积在队列中,性能会急剧下降
- RabbitMQ的性能在Kafka和RocketMQ中是最差的,每秒处理几万到几十万的消息。如果应用要求高的性能,不要选择RabbitMQ。
- RabbitMQ是Erlang开发的,功能扩展和二次开发代价很高。
RocketMQ
优点
- RocketMQ是一个开源的消息队列,使用java实现。借鉴了Kafka的设计并做了很多改进。RocketMQ主要用于有序,事务,流计算,消息推送,日志流处理,binlog分发等场景。
- RocketMQ几乎具备了消息队列应该具备的所有特性和功能。
- java开发,阅读源代码、扩展、二次开发很方便。
- 对电商领域的响应延迟做了很多优化。在大多数情况下,响应在毫秒级。如果应用很关注响应时间,可以使用RocketMQ。
- 性能比RabbitMQ高一个数量级,每秒处理几十万的消息。
缺点
跟周边系统的整合和兼容不是很好。
Kafka
- Kafka的可靠性,稳定性和功能特性基本满足大多数的应用场景。
- 跟周边系统的兼容性是数一数二的,尤其是大数据和流计算领域,几乎所有相关的开源软件都支持Kafka。
- Kafka高效,可伸缩,消息持久化。支持分区、副本和容错。
- Kafka是Scala和Java开发的,对批处理和异步处理做了大量的设计,因此Kafka可以得到非常高的性能。它的异步消息的发送和接收是三个中最好的,但是跟RocketMQ拉不开数量级,每秒处理几十万的消息。
- 如果是异步消息,并且开启了压缩,Kafka最终可以达到每秒处理2000w消息的级别。
由于是异步和批处理的,延迟也会高,不适合电商场景。
消息队列的应用场景
系统应该如何应对高并发的读请求
- 使用缓存策略(Redis)将请求挡在上层中的缓存中
- 能静态化的数据尽量做到静态化
- 加入限流(比如对短时间之内来自某一个用户,某一个IP、某个设备的重复请求做丢弃处理)
系统应该如何应对高并发的写请求
生成订单,扣减库存,用户这些操作不经过缓存直达数据库。如果在 1s内,有 1 万个数据连接同时到达,系统的数据库会濒临崩溃。如何解决这个问题呢?我们可以使用 消息队列。
消息队列的作用:
- 削去秒杀场景下的峰值写流量——流量削峰
- 通过异步处理简化秒杀请求中的业务流程——异步处理
- 解耦,实现秒杀系统模块之间松耦合——解耦
削去秒杀场景下的峰值写流量
将秒杀请求暂存于消息队列,业务服务器响应用户“秒杀结果正在处理中。。。”,释放系统资源去处理其它用户的请求。
削峰填谷,削平短暂的流量高峰,消息堆积会造成请求延迟处理,但秒杀用户对于短暂延迟有一定容忍度。
通过异步处理简化秒杀请求中的业务流程
如主要流程是生成订单、扣减库存;次要流程比如购买成功之后会给用户发优惠券,增加用户的积分。
此时秒杀只要处理生成订单,扣减库存的耗时,发放优惠券、增加用户积分异步去处理了。
解耦,实现模块之间的松耦合
将秒杀数据同步给数据团队,有两种思路:
- 使用 HTTP 或者 RPC 同步调用,即提供一个接口,实时将数据推送给数据服务。
系统的耦合度高,如果其中一个服务有问题,可能会导致另一个服务不可用。 - 使用消息队列
将数据全部发送给消息队列,然后数据服务订阅这个消息队列,接收数据进行处理。
JMS规范和AMQP协议
JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM,Message oriented Middleware)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。与具体平台无关的API,绝大多数MOM提供商都支持。
它类似于JDBC(Java Database Connectivity)。
AMQP全称高级消息队列协议(Advanced Message Queuing Protocol),是一种标准,类似于JMS,兼容JMS协议。目前RabbitMQ主流支持AMQP 0-9-1,3.8.4版本支持AMQP 1.0。
AMQP中的概念
Publisher:消息发送者,将消息发送到Exchange并指定RoutingKey,以便queue可以接收到指定的消息。
Consumer:消息消费者,从queue获取消息,一个Consumer可以订阅多个queue以从多个queue中接收消息。
Server:一个具体的MQ服务实例,也称为Broker。
Virtual host:虚拟主机,一个Server下可以有多个虚拟主机,用于隔离不同项目,一个Virtualhost通常包含多个Exchange、Message Queue。
Exchange:交换器,接收Producer发送来的消息,把消息转发到对应的Message Queue中。
Routing key:路由键,用于指定消息路由规则(Exchange将消息路由到具体的queue中),通常需要和具体的Exchange类型、Binding的Routing key结合起来使用。
Bindings:指定了Exchange和Queue之间的绑定关系。Exchange根据消息的Routing key和Binding配置(绑定关系、Binding、Routing key等)来决定把消息分派到哪些具体的queue中。这依赖于Exchange类型。
Message Queue:实际存储消息的容器,并把消息传递给最终的Consumer。
Rabbitmq架构和实战
整体逻辑架构
Rabbitmq Exchange类型
RabbitMQ常用的交换器类型有: fanout
、 direct
、 topic
、 headers
四种。
Fanout
会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
Direct
direct类型的交换器路由规则很简单,它会把消息路由到那些BindingKey
和RoutingKey
完全匹配的队列中。
Topic
topic类型的交换器在direct匹配规则上进行了扩展,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,这里的匹配规则稍微不同,它约定:
BindingKey和RoutingKey一样都是由".“分隔的字符串;BindingKey中可以存在两种特殊字符”*“和“#”,用于模糊匹配,其中”*“用于匹配一个单词,”#"用于匹配多个单词(可以是0个)。
Headers
headers类型的交换器不依赖于路邮件的匹配规则来路由信息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送的消息到交换器时,RabbitMQ会获取到该消息的headers,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果匹配,消息就会路由到该队列。headers类型的交换器性能很差
,不实用。
Rabbitmq数据存储
存储机制
Rabbitmq消息有两种类型:
- 持久化消息和非持久化消息。
- 这两种消息都会被写入磁盘。
持久化消息在到达队列时写入磁盘,同时会内存中保存一份备份,当内存吃紧时,消息从内存中清除。这会提高一定的性能。
非持久化消息一般只存于内存中,当内存压力大时数据刷盘处理,以节省内存空间。
RabbitMQ存储层包含两个部分:队列索引和消息存储。
- 队列索引:rabbit_queue_index
索引维护队列的落盘消息的信息,如存储地点、是否已被给消费者接收、是否已被消费者ack等。
每个队列都有相对应的索引。 - 消息存储:rabbit_msg_store
消息以键值对的形式存储到文件中,一个虚拟主机上的所有队列使用同一块存储,每个节点只有一个。存储分为持久化存储
(msg_store_persistent)和短暂存储
(msg_store_transient)。持久化存储的内容在broker重启后不会丢失,短暂存储的内容在broker重启后丢失。
队列结构
通常队列由rabbit_amqqueue_process和backing_queue这两部分组成,
- rabbit_amqqueue_process
负责协议相关的消息处理
,即接收生产者发布的消息、向消费者交付消
息、处理消息的确认(包括生产端的confirm和消费端的ack)等。 - backing_queue是
消息存储的具体形式和引擎
,并向rabbit_amqqueue_process提供相关的接口以供调用。
如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么该消息会直接发送给消费者,不会经过队列这一步。当消息无法直接投递给消费者时,需要暂时将消息存入队列,以便重新投递。
RabbitMQ队列的4种状态:
- alpha:消息索引和消息内容都存内存,最耗内存,很少消耗CPU
- beta:消息索引存内存,消息内存存磁盘
- gama:消息索引内存和磁盘都有,消息内容存磁盘
- delta:消息索引和内容都存磁盘,基本不消耗内存,消耗更多CPU和I/O操作
消息存入队列后,不是固定不变的,它会随着系统的负载在队列中不断流动,消息的状态会不断发生变化。
持久化的消息,索引和内容都必须先保存在磁盘上,才会处于上述状态中的一种
gama状态只有持久化消息才会有的状态。
在运行时,RabbitMQ会根据消息传递的速度定期计算一个当前内存中能够保存的最大消息数量
(target_ram_count),如果alpha状态的消息数量大于此值,则会引起消息的状态转换,多余的消息可能会转换到beta、gama或者delta状态。区分这4种状态的主要作用是满足不同的内存和CPU需求
。
对于普通没有设置优先级和镜像的队列来说,backing_queue的默认实现是rabbit_variable_queue,其内部通过5个子队列Q1、Q2、delta、Q3、Q4来体现消息的各个状态。
消费者获取消息也会引起消息的状态转换。
当消费者获取消息时
- 首先会从Q4中获取消息,如果获取成功则返回。
- 如果Q4为空,则尝试从Q3中获取消息,系统首先会判断Q3是否为空,如果为空则返回队列
为空,即此时队列中无消息。 - 如果Q3不为空,则取出Q3中的消息;进而再判断此时Q3和Delta中的长度,如果都为空,则
可以认为 Q2、Delta、 Q3、Q4 全部为空,此时将Q1中的消息直接转移至Q4,下次直接从
Q4 中获取消息。 - 如果Q3为空,Delta不为空,则将Delta的消息转移至Q3中,下次可以直接从Q3中获取消息。
在将消息从Delta转移到Q3的过程中,是按照索引分段读取的,首先读取某一段,然后判断读
取的消息的个数与Delta中消息的个数是否相等,如果相等,则可以判定此时Delta中己无消
息,则直接将Q2和刚读取到的消息一并放入到Q3中,如果不相等,仅将此次读取到的消息转
移到Q3。
Rabbitmq工作流程详解
生产者发送消息的流程
- 生产者连接Rabbitmq,建立TCP连接(Connection),开启信道(Channel)
- 生产者声明一个Exchange(交换器),并设置相关属性,比如交换器类型、是否持久化等。
- 生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
- 生产者通过bindingKey(绑定key)将交换器和队列绑定(binding)起来
- 生产者发送消息至Rabbitmq Broker,其中包含routingkey(路由键)、交换器等信息
- 相应地交换器根据接收到的routingkey查找相匹配的队列。
- 如果找到,则将从生产者发送过来的消息存入相应的队列中。
- 如果没找到,则根据生产者配置的属性选择丢弃还是退回给生产者。
- 关闭信道
10.关闭连接
消费者接收消息的过程
- 消费者连接到Rabbitmq Broker,建立一个连接TCP(Connection),开启一个信道(Channel) 。
- 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置响应的回调函数,以及做一些准备工作。
- 等待RabbitMQ Broker回应并投递响应队列中的消息,消费者接收消息。
- 消费者确认(ack)接受到的消息
- Rabbitmq从队列中删除相应已经被确认的消息。
- 关闭信道
- 关闭连接
Connection和Channel关系
生产者和消费者,需要与RabbitMQ Broker 建立TCP连接,也就是Connection 。一旦TCP 连接建立起来,客户端紧接着创建一个AMQP 信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection 之上的虚拟连接, RabbitMQ 处理的每条AMQP 指令都是通过信道完成的
。
为什么不直接使用TCP连接,而是使用信道?
RabbitMQ 采用类似NIO的做法,复用TCP 连接,减少性能开销,便于管理。
当每个信道的流量不是很大时,复用单一的Connection 可以在产生性能瓶颈的情况下有效地节省TCP 连接资源。