什么是MQ?
消息总线(Message Queue),是一种跨进程、异步的通信机制,用于上下游传递消息。由消息系统来确保消息的可靠传递。
MQ是干什么用的?
应用解耦、异步、流量削锋、数据分发、错峰流控、日志收集等等...
MQ衡量标准
服务性能、数据存储、集群架构
ActiveMQ
ActiveMQ是apache出品,最流行的,能力强劲的开源消息总线,并且它一个完全支持JMS规范的消息中间件。其丰富的API、多种集群构建模式使得它成为业界老牌消息中间件,在中小型企业中应用广泛。
是其性能稍差,在面对高并发的情况下,会出现消息阻塞、堆积、延迟等问题。
默认采用了基于内存的kahaDB进行存储,如果需要保证消息的可靠性,也可以选择关系行数据库进行存储。
集群架构模式如下:
Master-Slave模式:通过zookeeper对主从进行管理,正常情况下,从节点不会提供服务。当主节点出现问题后,zookeeper会高效的将主节点下掉,从节点来提供服务。
NetWork模式:两套主从Master-Slave节点。由网络联通,将其变为分布式的集群架构。
Kafka
Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点就是基于Pull的模式来处理消息消费,追求高吞吐量
,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求
,适合产生大量数据的互联网服务的数据收集业务。能够支持廉价的服务器上以每秒100k条数据的吞吐量。(有ack机制,可以保证不丢失,不能保证不重复。)
高效的读写基于操作系统低层的Page Cache。仅仅使用内存管理,不存在内存和磁盘之间的IO操作。
集群架构模式如下:
通过replicate进行节点间数据的复制,尽量保证数据的可靠性。
RocketMQ
RocketMQ是阿里开源的消息中间件,目前也已经孵化为Apache顶级项目,它是纯Java开发,具有高吞吐量、高可靠性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,它对消息的可靠传输以及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binlog分发等场景。
在2.0版本,RocketMQ集群也是通过Zookeeper进行管理。在3.0之后,放弃Zookeeper,使用NameServer进行集群的管理和协调。
能够保障消息的顺序消费,提供了丰富的消息拉取等处理模式,消费者可以高效进行水平扩展,能够承载上亿级别数据量级。
可以支持多种集群架构模式:Master-Slave模式、双Master-Slave模式、多主多从模式等等。
支持多种刷盘策略:同步双写、异步复制。借助了零拷贝等技术。
集群架构模式如下:
技术背景知识介绍
AMQP高级消息队列协议
AMQP(Advanced Message Queuing Protocol)
高级消息队列协议:高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言灯条件的限制。
AMQP中消息的路由过程和JMS存在一些差别。AMQP中增加了Exchange和Binging的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而Binding决定交换器的消息应该发送到哪个队列。
Erlang语言
Erlang语言
最初用于交换机领域的架构模式,这样使得RabbitMQ在Broker之间进行数据交互的性能非常优秀(Erlang有着和原生Socket一样的延迟)。
RabbitMQ
RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在不同的应用之间共享数据(跨平台跨语言)。RabbitMQ是使用Erlang语言编写,并且基于AMQP协议实现。
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的整体架构
RabbitMQ的消息流转
RabbitMQ各组件功能
Broker:
标识消息队列服务器实体.Virtual Host:
虚拟主机。标识一批交换机、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在链接时指定,RabbitMQ默认的vhost是 /。Exchange:
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Queue:
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。Banding:
绑定,用于消息队列和交换机之间的关联。一个绑定就是基于路由键将交换机和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Channel:
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟链接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。Connection:
网络连接,比如一个TCP连接。Publisher:
消息的生产者,也是一个向交换器发布消息的客户端应用程序。Consumer:
消息的消费者,表示一个从一个消息队列中取得消息的客户端应用程序。Message:
消息,消息是不具名的,它是由消息头和消息体组成。消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。
RabbitMQ的多种Exchange类型
Exchange分发消息时,根据类型的不同分发策略有区别。目前共四种类型:direct、fanout、topic、headers(headers匹配AMQP消息的header而不是路由键(Routing-key),此外headers交换器和direct交换器完全一致,但是性能差了很多,目前几乎用不到了。所以直接看另外三种类型。)。
direct
消息中的路由键(routing key)如果和Binding中的binding key一致,交换器就将消息发到对应的队列中。路由键与队列名完全匹配。
fanout
topic
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进行绑定。只不过我们需要在队列加上一个参数即可。
只需要通过监听该死信队列即可处理死信消息。还可以通过死信队列完成延时队列。
消费端ACK与NACK
消费端 进行消费的时候,如果由于业务异常可以进行日志的记录,然后进行补偿。由于服务器宕机等严重问题,我们需要手动进行ACK保障消费端消费成功。
消费端重回队列是为了对没有成功处理消息,把消息重新返回到Broker。一般来说,实际应用中都会关闭重回队列,也就是设置为false。
生产者Confirm机制
- 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
- 生产者进行接受应答,用来确认这条消息是否正常的发送到了Broker,这种方式也是消息的可靠性投递的核心保障!
如何实现Confirm确认消息?
2、在channel上开启监听:addConfirmListener,监听成功和失败的处理结果,根据具体的结果对消息进行重新发送或记录日志处理等后续操作。
Return消息机制
Return Listener用于处理一些不可路由的消息
我们的消息生产者,通过指定一个Exchange和Routing,把消息送达到某一个队列中去,然后我们的消费者监听队列进行消息的消费处理操作。
但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候我们需要监听这种不可达消息,就需要使用到Returrn Listener。
基础API中有个关键的配置项Mandatory
:如果为true,监听器会收到路由不可达的消息,然后进行处理。如果为false,broker端会自动删除该消息。
通过chennel.addReturnListener(ReturnListener rl)传入已经重写过handleReturn方法的ReturnListener。
消费端自定义监听(推模式和拉模式pull/push)
- 一般通过while循环进行consumer.nextDelivery()方法进行获取下一条消息进行那个消费。(通过while将拉模式模拟成推模式,但是死循环会耗费CPU资源。)
- 通过自定义Consumer,实现更加方便、可读性更强、解耦性更强的方式。(现默认使用的模式,直接订阅到queue上,如果有数据,就等待mq推送过来)
如何保证幂等性
- 唯一ID+指纹码机制,利用数据库主键去重
- 利用redis的原子性去实现
如何保证可靠性?
什么是生产端的可靠性投递?
- 保证消息的成功发出
- 保障MQ节点的成功接受
- 发送端收到MQ节点(Broker)确认应答
- 完善消息的补偿机制
解决方案
消息落库,对消息状态进行变更。
消息的延迟投递,做二次确认,回调检查。
消费端如何限流
当海量消息瞬间推送过来,单个客户端无法同时处理那么多数据,严重会导致系统宕机。这时,需要削峰。
Channel模式和Connection模式
参考:https://www.jianshu.com/p/2c2a7cfdd38a
Connection和Channel是spring-amqp中的概念,并非rabbitmq中的概念,官方文档对Connection和Channel有这样的描述:
HANNEL模式:
程序运行期间ConnectionFactory会维护着一个Connection,所有的操作都会使用这个Connection,但一个Connection中可以有多个Channel,操作rabbitmq之前都必须先获取到一个Channel,否则就会阻塞(可以通过setChannelCheckoutTimeout()设置等待时间),这些Channel会被缓存(缓存的数量可以通过setChannelCacheSize()设置);
CONNECTION模式:
这个模式下允许创建多个Connection,会缓存一定数量的Connection,每个Connection中同样会缓存一些Channel,除了可以有多个Connection,其它都跟CHANNEL模式一样。
关于CONNECTION模式中,可以存在多个Connection的使用场景,官方文档的描述:
setChannelCacheSize:设置每个Connection中(注意是每个Connection)可以缓存的Channel数量,注意只是缓存的Channel数量,不是Channel的数量上限,操作rabbitmq之前(send/receive message等)要先获取到一个Channel,获取Channel时会先从缓存中找闲置的Channel,如果没有则创建新的Channel,当Channel数量大于缓存数量时,多出来没法放进缓存的会被关闭。
注意,改变这个值不会影响已经存在的Connection,只影响之后创建的Connection。
有时会出现connection closed错误。rabbitTemplate作者对于这种问题的解决方案,他给的方案很简单,单纯的增加connection数:
setChannelCheckoutTimeout:当这个值大于0时,channelCacheSize不仅是缓存数量,同时也会变成数量上限,从缓存获取不到可用的Channel时,不会创建新的Channel,会等待这个值设置的毫秒数,到时间仍然获取不到可用的Channel会抛出AmqpTimeoutException异常。
同时,在CONNECTION模式,这个值也会影响获取Connection的等待时间,超时获取不到Connection也会抛出AmqpTimeoutException异常。
RabbitMQ集群
RabbitMQ会始终记录以下四中类型的内部元数据:
- 队列元数据:包括队列名称和他们的属性,比如是否可持久化,是否可持久化,是否自动删除。
- 交换器元数据:交换器名称、类型、属性。
- 绑定元数据:内部是一张表格,记录如何将消息路由到队列。
- vhost元数据:为vhost内部的队列、交换器、绑定提供命名空间和安全属性。
当在集群中声明队列、交换器、绑定的时候,这些操作会直到所有集群节点都成功提交元数据变更后才返回。集群中有内存节点和磁盘节点两种类型,内存节点虽然不写入磁盘,但是它的执行比磁盘节点要好。内存节点可以提供出色的性能,磁盘节点能保障配置信息在节点重启后仍然可用,那集群中如何平衡这两者呢?
RabbitMQ 只要求集群中至少有一个磁盘节点,所有其他节点可以是内存节点,当节点加入或离开集群时,它们必须要将该变更通知到至少一个磁盘节点。如果只有一个磁盘节点,刚好又是该节点崩溃了,那么集群可以继续路由消息,但不能创建队列、创建交换器、创建绑定、添加用户、更改权限、添加或删除集群节点。换句话说集群中的唯一磁盘节点崩溃的话,集群仍然可以运行,但直到该节点恢复,否则无法更改任何东西。
来源:oschina
链接:https://my.oschina.net/u/2269430/blog/4455083