一、常用Message Queue对比

RabbitMQ

RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负 载均衡或者数据持久化都有很好的支持。

Redis

Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能, 所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试 数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于 RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ 的出队性能则远低于Redis。

ZeroMQ

ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合 多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因 为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但 是ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。其中,Twitter的Storm中默认使用ZeroMQ作为数据流的传 输。


ActiveMQ

ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。

Kafka/Jafka

Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,而Jafka是在Kafka之上孵 化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到 10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现复杂均衡;支持Hadoop 数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行 加载机制来统一了在线和离线的消息处理,这一点也是本课题所研究系统所看重的。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

二、Message Queue介绍

2.1 RabbitMQ

redis MQ功能 redis mq 效率_Erlang

1)Erlang

Erlang是一种通用的面向并发的编程语言,区别于传统的面向对象语言,是一个结构化,动态类型编程语言,内建并行计算支持。它是基于进程并发的,这些进程是轻量级的并且互相协作有条理的处理着事务,这些进程对于操作系统而言是透明的,对操作系统来说只有一个进程在运行。其次,每个进程拥有自己的独立内存,在进程间通信中完全依赖于消息传递,每个进程都拥有自己独立的邮箱并通过模式匹配的方式来查找要处理的消息,然后分别进行异步处理。这减轻了进程之间的耦合度,提高了独立性。Erlang还具有可靠的容错机制,由于进程之间相对独立,于是Erlang里可以用一些进程去链接或监视另外一些进程,当这些被监视的进程因为错误而产生异常退出的时候,负责监视的程序就会收到这些进程退出的消息并对这些进程进行相应的处理。

Erlang可以利用监控树执行一对一或者一对多的监控。最重要的是,对多核CPU的支持,在Erlang中对于多核的操作不需要开发人员来管理,对开发人员来说是完全透明的,我们只需要和往常一样编写程序就可以。最后Erlang是支持热代码升级的,在Erlang中可以实现不停机就进行代码的升级,来实现软件运行中的热升级。Erlang的版本管理中,可以保留一个模块的2种不同的版本,支持对版本的回滚。Erlang是一种弱类型语言,在匹配的时候可以较为方便的调整消息的内容或者模式的要求,但发生错误的时候,这些错误的隐蔽性较强。

Erlang是属于IO密集型的语言,适合分布式特征明显的项目,对于计算密集型的要求,会比较吃力。它非常适 合于构建分布式,实时软并行计算系统。使用Erlang编写出的应用运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于Erlang来说仅仅 只是一两个环节,比起C程序的线程切换要高效得多得多了。使用Erlang来编写分布式应用要简单的多,因为它的分布式机制是透明的:对于程序来说并不知道自己是在分布式运行。Erlang运行时环境是一个虚拟机,有点像Java虚拟机,这样代码一经编译,同样可以随处运行。

2)AMQP协议

AMQP协议,即Advanced Message Queuing Protocol,其是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计,旨在实现一种标准消息中间件技术。它是一个二进制协议,拥有一些现代特点:多信道、协商式、异步、安全、跨平台、中立、高效,基于此协议的客户端与消息中间件可传递消息。

redis MQ功能 redis mq 效率_服务器_02


其中:

Broker: 接收和分发消息的应用,RabbitMQ Server就是Message Broker。

Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。

Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。

Channel(信道): 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。

Exchange(消息分发节点): 它是生产者发布消息的地方,message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。

Queue(队列): 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。队列是消息最终到达并由消费者接收的地方。

Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。Binding表明了以何种方式将消息从交换路由到特定队列。

相关术语说明:

连接(Connection):一个网络连接,比如TCP/IP套接字连接。
会话(Session):端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。
信道(Channel):多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。
客户端(Client):AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。
服务器(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程。也称为“消息代理”。
端点(Peer):AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。
搭档(Partner):当描述两个端点之间的交互过程时,使用术语“搭档”来表示“另一个”端点的简记法。比如我们定义端点A和端点B,当它们进行通信时,端点B是端点A的搭档,端点A是端点B的搭档。
片段集(Assembly):段的有序集合,形成一个逻辑工作单元。
段(Segment):帧的有序集合,形成片段集中一个完整子单元。
帧(Frame):AMQP传输的一个原子单元。一个帧是一个段中的任意分片。
控制(Control):单向指令,AMQP规范假设这些指令的传输是不可靠的。
命令(Command):需要确认的指令,AMQP规范规定这些指令的传输是可靠的。
异常(Exception):在执行一个或者多个命令时可能发生的错误状态。
类(Class):一批用来描述某种特定功能的AMQP命令或者控制。
消息头(Header):描述消息数据属性的一种特殊段。
消息体(Body):包含应用程序数据的一种特殊段。消息体段对于服务器来说完全透明——服务器不能查看或者修改消息体。
消息内容(Content):包含在消息体段中的的消息数据。
交换器(Exchange): 服务器中的实体,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
交换器类型(Exchange Type):基于不同路由语义的交换器类。
消息队列(Message Queue): 一个命名实体,用来保存消息直到发送给消费者。
绑定器(Binding): 消息队列和交换器之间的关联。
绑定器关键字(Binding Key):绑定的名称。一些交换器类型可能使用这个名称作为定义绑定器路由行为的模式。
路由关键字(Routing Key):一个消息头,交换器可以用这个消息头决定如何路由某条消息。
持久存储(Durable):一种服务器资源,当服务器重启时,保存的消息数据不会丢失。
临时存储(Transient):一种服务器资源,当服务器重启时,保存的消息数据会丢失。
持久化(Persistent): 服务器将消息保存在可靠磁盘存储中,当服务器重启时,消息不会丢失。
非持久化(Non-Persistent):服务器将消息保存在内存中,当服务器重启时,消息可能丢失。
消费者(Consumer):一个从消息队列中请求消息的客户端应用程序。
生产者(Producer):一个向交换器发布消息的客户端应用程序。
虚拟主机(Virtual Host):一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。客户端应用程序在登录到服务器之后,可以选择一个虚拟主机。
下面这些术语在AMQP规范的上下文中没有特别的意义:
主题: 通常指发布消息;AMQP规范用一种或多种交换器来实现主题。
服务:通常等同于服务器。AMQP规范使用“服务器”这个术语来兼容IETF的标准术语,并且明确了协议中每个部分的角色(两方也可能是AMQP服务)。
消息代理: 等同于服务器。AMQP规范使用术语“客户端”和“服务器”来兼容IETF的标准术语。

3)典型的“生产/消费”消息模型

redis MQ功能 redis mq 效率_服务器_03


过程:

生产者发送消息到broker server(RabbitMQ)。在Broker内部,用户创建Exchange/Queue,通过Binding规则将两者联系在一起。Exchange分发消息,根据类型/binding的不同分发策略进行区别/分类后,消息最后发到Queue中,等待消费者取走。

3种Exchange类型:

**1》Direct型:**一对一的关系

redis MQ功能 redis mq 效率_redis MQ功能_04


工作流程:Message中的“routing key”如果和Binding中的“binding key”一致, Direct exchange则将message发到对应的queue中。

2》Fanout(分发/分列模式): 一对多

工作流程:每个发到Fanout类型Exchange的message都会分到所有绑定的queue上去。

redis MQ功能 redis mq 效率_redis MQ功能_05


**3》Topic(主题)模式:**按消息预先定义的类别/主题,分类发送message,这也是目前主要的应用模式。

redis MQ功能 redis mq 效率_消息中间件_06


工作流程:根据routing key,及通配规则,Topic exchange将分发到目标queue中。

注:
Routing key中可以包含两种通配符,类似于正则表达式:

“#”通配任何零个或多个word
“*”通配任何单个word

4)RabbitMQ中的成员

Producer(生产者): 将消息发送到Exchange
Exchange(交换器):将从生产者接收到的消息路由到Queue
Queue(队列):存放供消费者消费的消息
BindingKey(绑定键):建立Exchange与Queue之间的关系(Exchange将什么样的消息路由到Queue)
RoutingKey(路由键):Producer发送消息与路由键给Exchange,Exchange将判断RoutingKey是否符合BindingKey,如何则将该消息路由到绑定的Queue
Consumer(消费者):从Queue中获取消息

Exchange是接受生产者消息并将消息路由到消息队列的关键组件。ExchangeType和Binding决定了消息的路由规则。所以生产者想要发送消息,首先必须要声明一个Exchange和该Exchange对应的Binding。可以通过 ExchangeDeclare和BindingDeclare完成。

声明一个Exchange需要三个参数:ExchangeName,ExchangeType和Durable(持久化配置)。

1)ExchangeName是该Exchange的名字,该属性在创建Binding和生产者通过publish推送消息时需要指定。
2)ExchangeType,指Exchange的类型,在RabbitMQ中,有三种类型的Exchange:direct ,fanout和topic,不同的Exchange会表现出不同路由行为。
3)Durable是该Exchange的持久化属性,声明一个Binding需提供一个QueueName,ExchangeName和BindingKey。

Consumer消息的接收消息的2种方式:

1) 通过basic.consume 订阅队列.channel将进入接收模式直到你取消订阅.订阅模式下Consumer只要上一条消息处理完成(ACK或拒绝),就会主动接收新消息.如果消息到达queue就希望得到尽快处理,也应该使用basic.consume命令.
2) 不需要一直保持订阅,只要使用basic.get命令主动获取消息即可.当前消息处理完成之后,继续获取消息需要主动执行basic.get 不要"在循环中使用basic.ge"t当做另外一种形式的basic.consume,因为这种做法相比basic.consume有额外的成本:basic.get本质上就是先订阅queue取回一条消息之后取消订阅.Consumer吞吐量大的情况下通常都会使用basic.consume.

如果一个rabbit queue有多个consumer,具体到队列中的某条消息只会发送到其中的一个Consumer。

消息确认机制:

所有接收到的消息都要求发送响应消息(ACK).这里有两种方式一种是Consumer使用basic.ack明确发送ACK,一种是订阅queue的时候指定auto_ack为true,这样消息一到Consumer那里RabbitMQ就会认为消息已经得到ACK.

要注意的是这里的响应和消息的发送者没有丝毫关系,ACK只是Consumer向RabbitMQ确认消息已经正确的接收到消息,RabbitMQ可以安全移除该消息,仅此而已.

消息确认未正确响应:

如果Consumer接收了一个消息就还没有发送ACK就与RabbitMQ断开了,RabbitMQ会认为这条消息没有投递成功会重新投递到别的Consumer.如果你的应用程序崩掉了,你可以设置备用程序来继续完成消息的处理.

如果Consumer本身逻辑有问题没有发送ACK的处理,RabbitMQ不会再向该Consumer发送消息.RabbitMQ会认为这个Consumer还没有处理完上一条消息,没有能力继续接收新消息.我们可以善加利用这一机制,如果需要处理过程是相当复杂的,应用程序可以延迟发送ACK直到处理完成为止.这可以有效控制应用程序这边的负载,不致于被大量消息冲击.

拒绝消息处理:

由于要拒绝消息,所以ACK响应消息还没有发出,所以这里拒绝消息可以有两种选择:
1.Consumer直接断开RabbitMQ 这样RabbitMQ将把这条消息重新排队,交由其它Consumer处理.这个方法在RabbitMQ各版本都支持.这样做的坏处就是连接断开增加了RabbitMQ的额外负担,特别是consumer出现异常每条消息都无法正常处理的时候.
  1. RabbitMQ 2.0.0可以使用 basic.reject 命令,收到该命令RabbitMQ会重新投递到其它的Consumer.如果设置requeue为false,RabbitMQ会直接将消息从queue中移除.
    其实还有一种选择就是直接忽略这条消息并发送ACK,当你明确直到这条消息是异常的不会有Consumer能处理,可以这样做抛弃异常数据.为什么要发送basic.reject消息而不是ACK?RabbitMQ后面的版本可能会引入"dead letter"队列,如果想利用dead letter做点文章就使用basic.reject并设置requeue为false.

消息持久化
消息的持久化需要在消息投递的时候设置delivery mode值为2.由于消息实际存储于queue之中,"皮之不存毛将焉附"逻辑上,消息持久化同时要求exchange和queue也是持久化的.这是消息持久化必须满足的三个条件.
持久化的代价就是性能损失,磁盘IO远远慢于RAM(使用SSD会显著提高消息持久化的性能) , 持久化会大大降低RabbitMQ每秒可处理的消息.两者的性能差距可能在10倍以上.

消息恢复
consumer从durable queue中取回一条消息之后并发回了ACK消息,RabbitMQ就会将其标记,方便后续垃圾回收.如果一条持久化的消息没有被consumer取走,RabbitMQ重启之后会自动重建exchange和queue(以及bingding关系),消息通过持久化日志重建再次进入对应的queues,exchanges.

Queue:消息实际存放的地方

Queues是Massage的落脚点和等待接收的地方,消息除非被扔进黑洞否则就会被安置在一个Queue里面.Queue很适合做负载均衡,RabbitMQ可以在若干consumer中间实现轮流调度(Round-Robin).

创建队列:

consumer和producer都可以创建Queue,如果consumer来创建,避免consumer订阅一个不存在的Queue的情况,但是这里要承担一种风险:消息已经投递但是consumer尚未创建队列,那么消息就会被扔到黑洞,换句话说消息丢了;避免这种情况的好办法就是producer和consumer都尝试创建一下queue. 如果consumer在已经订阅了另外一个Queue的情况下无法完成新Queue的创建,必须取消之前的订阅将Channel置为传输模式(“transmit”)才能创建新的Channel.
创建Queue的时候通常要指定名字,名字方便consumer订阅.即使你不指定Rabbit会给它分配一个随机的名字,这在使用临时匿名队列完成RPC-over-AMQP调用时会非常有用.
创建Queue的时有两个非常有用的选项:
exclusive—When set to true, your queue becomes private and can only be consumed by your app. This is useful when you need to limit a queue to only one consumer.
auto-delete—The queue is automatically deleted when the last consumer unsubscribes.

如果要创建只有一个consumer使用的临时queue可以组合使用auto-delete和 exclusive.consumer一旦断开连接该队列自动删除.
重复创建Queue会怎样?如果Queue创建的选项完全一致的话,RabbitMQ直接返回成功,如果名称相同但是创建选项不一致就会返回创建失败.如果是想检查Queue是否存在,可以设置queue.declare命令的passive 选项为true:如果队列存在就会返回成功,如果队列不存在会报错且不会执行创建逻辑.

消息是如何从动态路由到不同的队列的?

bindings and exchanges

消息如何发送到队列

消息是如何发送到队列的?这就要说到AMQP bindings and exchanges. 投递消息到queue都是经由exchange完成的,和生活中的邮件投递一样也需要遵循一定的规则,在RabbitMQ中规则是通过routing key把queue绑定到exchange上,这种绑定关系即binding.消息发送到RabbitMQ都会携带一个routing key(哪怕是空的key),RabbitMQ会根据bindings匹配routing key,如果匹配成功消息会转发到指定Queue,如果没有匹配到queue消息就会被扔到黑洞.

如何发送到多个队列

消息是分发到多个队列的?AMQP协议里面定义了几种不同类型的exchange:direct, fanout, topic, and headers. 每一种都实现了一种 routing 算法. header的路由消息并不依赖routing key而是去匹配AMQP消息的header部分,这和下面提到的direct exchange如出一辙,但是性能要差很多,在实际场景中几乎不会被用到.

direct exchange routing key //完全匹配才转发
fanout exchange //不理会routing key,消息直接广播到所有绑定的queue
topic exchange //对routing key模式匹配

exchange持久化

创建queue和exchange默认情况下都是没有持久化的,节点重启之后queue和exchange就会消失,这里需要特别指定queue和exchange的durable属性.

Consumer是直接创建TCP链接到RabbitMQ吗?下面就是答案:

Channel

无论是要发布消息还是要获取消息 ,应用程序都需要通过TCP连接到RabbitMQ.应用程序连接并通过权限认证之后就要创建Channel来执行AMQP命令.Channel是建立在实际TCP连接之上通信管道,这里之所以引入channel的概念而不是直接通过TCP链接直接发送AMQP命令,是出于两方面的考虑:建立上成百上千的TCP链接,一方面浪费了TCP链接,一方面很快会触及系统瓶颈.引入了Channel之后多个进程与RabbitMQ的通信可以在一条TCP链接上完成.我们可以把TCP类比做光缆,那么Channel就像光缆中的一根根光纤.

5)RabbitMQ工作流程图

redis MQ功能 redis mq 效率_服务器_07


redis MQ功能 redis mq 效率_服务器_08

2.2 Redis

Redis是一个开源的使用ANSI C语言编写的,基于内存亦可持久化的日志型、Key-Value数据库,并可提供多种语言的API,是目前应用广泛最受欢迎的NoSQL数据库之一。其又常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis 的常用应用场景:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。

1)Redis的数据结构

redis MQ功能 redis mq 效率_消息中间件_09

2)Redis特性

A)事务:
命令序列化,按顺序执行
原子性
三阶段: 开始事务 - 命令入队 - 执行事务
命令:multi/exec/discard

B)发布订阅(Pub/Sub):

Pub/sub是一种消息通讯模式

Pub发送消息, Sub接受消息

Redis客户端可以订阅任意数量的频道

“fire and forgot”, 发送即遗忘

命令:Publish/Subscribe/Psubscribe/UnSub

redis MQ功能 redis mq 效率_服务器_10


C)Stream:

Redis 5.0新增

等待消费

消费组(组内竞争)

消费历史数据

FIFO

redis MQ功能 redis mq 效率_服务器_11


Redis5.0中发布的Stream类型,用来实现典型的消息队列,它几乎满足了消息队列一般具备的全部内容:消息ID的序列化生成、消息遍历、消息的阻塞和非阻塞读取、消息的分组消费、未完成消息的处理、消息队列监控;其中,每个Stream都有唯一的名称,它就是Redis的key;每个Stream都可以挂多个消费组,每个消费组都有一个Stream内唯一的名称,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组(Consumer Group)的状态都是独立的,相互不受影响,即同一份Stream内部的消息会被每个消费组都消费到。而消费组内的消费者者也有一个组内唯一名称,这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。。Redis“击穿”:在Redis获取某一key时, 由于key不存在, 而必须向DB发起一次请求的行为, 称为“Redis击穿”。

redis MQ功能 redis mq 效率_持久化_12


Redis“雪崩”:Redis缓存层由于某种原因宕机后,所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。