Message Queue
消息队列是一种服务间的异步通信方式,生产者生产消息并将其发布到消息队列当中,如果有消费者订阅了这个消息队列,那么消息就会被发送到这个消费者应用中
消息队列中的消息也是按照"先进先出"的方式被消费的,这与数据结构当中的队列很相似,先到的消息也是最先被处理的,处理之后就相当于“出队列“,消息就会被删除掉了
消息队列的常用协议
消息中间件协议基本上都是基于TCP/IP的应用层协议,常见的消息中间件协议有如下的几种:
-
AMQP协议AMQP(Advanced Message Queuing Protocol)是应用层标准高级消息队列协议,提供统一的消息服务;是应用层协议的一个开放标准,为面向消息中间件设计;基于此协议的客户端和消息中间件传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制优点:可靠、通用。
-
MQTT协议MQTT(Message Queuing Telemetry Transport-消息队列遥测传输),是由IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter 让房屋联网) 的通信协议。优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统
-
STOMP协议STOMP(Streaming Text Orientated Message Protocol-流文本定向消息协议)是一种为MOM(Message Oriented Middleware,面向消息的中间件) 设计的简单文本协议。STOMP 提供一个可互操作的连接格式,允许客户端与任意STOMP 消息代理(Broker)进行交互。优点:命令模式(非topic\queue 模式)
-
XMPP协议XMPP(Extensible Messaging and Presence Protocol-可扩展消息处理现场协议) 是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。优点:通用公开、兼容性强、可扩展、安全性高,但
XML编码格式占用带宽大。 -
JMS(Java Message Service)Java消息服务的应用程序接口,由sun公司提出,并定义了接口,包括create,send,recieve实现它的接口即可使用;这个消息服务只支持Java语言,不能使用其他的语言JMS只定义了JAVA编程接口协议,而没有定义消息的具体格式。因此,JMS适用范围较小,只能够用于特定的客户端和broker。JMS没有定义命令交互、消息路由。最后,在JMS中,消息发送者需要知道消息目的的拓扑。在AMQP中,路由逻辑被封装在了exchange之中,从而保护了消息的发送者。 -
其他基于
TCP/IP自定义的协议有些特殊框架(如:
Redis、kafka、zeroMq等根据自身需要未严格遵循MQ规范,而是基于TCP\IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。
常见的Message Queue
ActiveMQ:是基于JMS Apache的一种消息队列RocketMQ:是基于JMS Apache消息队列kafaka:分布式消息系统,吞吐量高,每秒并发数十万,但是它没有严格遵循MQ的规范,而是基于TCP/IP开发的一套协议RabbitMQ:RabbitMQ是用Erlang开发的基于AMQP协议的消息队列
AMQP中的基本组件
AMQP协议当中的一些核心概念:
-
Broker:接收和分发消息的应用,然后将消息发送至消息接受者或者其他的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之后首先就是到exchange,然后exchange根据分发规则,匹配查询表中的routing key将消息发送到相应的队列当中,常用的类型有:- direct (point-to-point)
- topic (publish-subscribe)
- fanout (
multicast)
-
Queue:这是消息到达的最终目的地,到达queue的消息是已经准备好被消费的消息,一个消息可以被exchange copy发送至多个queue -
Binding:它是exchange和queue之间的虚拟连接,binding中可以包含routing key;Binding信息被保存到exchange中的查询表中,用于message的分发依据
RabbitMQ中的五种消息模型
RabbitMQ和其他的消息中间件一样都有一些术语,比如:
- 生产者(Producer):指发送消息的应用程序
- 队列(Queue):一个队列就类似于邮局中的一个邮箱,当然,在队列中也可以存储未被消费的消息,消息存储的上限就是消息中间件所在主机的内存和磁盘的大小;可以同时有多个生产者向同一个队列发送消息,同样也可以有多个消费者消费同一个队列的消息
- 消费者(Consumer):订阅了一个队列等待接收消息的应用程序
注:一个应用既可以是生产者,也可以是消费者
RabbitMQ中的消息模型:
-
简单模型(Hello-world)
`P`:生产者,发送消息的服务
`C`:消费者,接收消息的服务
其中红色区域可以理解为`Queue`,类似于一个邮箱,用来存储消息:
- 消息来了不必强求马上去拿
- 最大容量可以和磁盘一样
- 允许多个消费者监听一个队列,争抢消息
-
Worker Queue
`Worker Queue`中也只有一个队列,但它是一种竞争消费的模式;在同一个队列上同时绑定了多个消费者,消费者争抢这一个队列中的消息,避免消息堆积
比如,短信服务集群就是这样,消息来了之后争抢者消费掉;这其实就是同一服务的多个实例而已,就是启动多个消费者客户端
###### 消息分发策略
出现两个消费者之后就需要确定消息的分发策略了,`RabbitMQ`支持两种消息分发的策略:
- 轮询(Round-robin ):消息队列会按顺序将消息发给每一个消费者,所以一般情况下,每个消费者都会得到相同数量的消息
- 公平分发(Fair Dispatch):在轮询的情况下,消息队列仅仅是盲目的将第`n`条消息发送给第`n`个消费者,即便该消费者处理消息的速度很慢;而公平分发策略下,会设置一个预取的值(`prefetch`),如下所示:

比如,现在设置`prefetch=1`,那么消息队列就一次给该消费者一条消息,换句话说,如果之前的这条消息没有被处理和确认,那么就不会向该消费者再派发新的消息,而是将消息发送到已完成处理的消费者中,这种模式就相当于能者多劳一样
-
发布/订阅
(Publish/Subscribe)之前的两种消息模型是生产者直接将消息发送到消息队列,接下来介绍的”发布/订阅“模型是与之前完全不同的消息发送方式
这种模型下,生产者从来都不会把消息直接发送到一个队列当中,并且大部分情况下都不知道消息被发送到了哪个队列
发布订阅模型中引入了一个新的组件,交换机(Exchange),生产者只是将消息发送到交换机,而交换机非常简单,它一边接收生产者发送的消息,一边会将消息推送到队列当中交换机推送消息到队列需要根据一定的规则,而这个这个规则就是通过
ExchangeType定义的,目前有四种exchangeTyp:-
Fanout(广播):交换机会广播所有接收到的消息到所有的队列当中,如下所示:
-
在广播模型(`fanout`)中即便指定`routingKey`,也会被自动忽略掉:
```
err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
err = ch.Publish(
"logs", // exchange
"err.log", // routing key,此处的 key 就会被忽略掉
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
```
- `Direct`(定向):`exchange`会把消息发送到队列的`BindingKey`与消息的`RoutingKey`相匹配的队列当中,如下所示:
上图中`direct exchange x` 有两个队列与其绑定,第一个队列与`orange bandingkey`进行绑定,第二个队列有两个`bandingKey`与其绑定,一个是`black`,另一个是`green`
当发布消息到`rouingkey`为`orange`的`exchange`时,`exchange`就会将消息推送到`C1`;当发布消息到`routingkey`为`black`或`green`的`exchange`时,`exchange`会将消息推送到`C2`,其他的消息都会被丢弃调
当然,也可以用同一个`BindingKey`绑定多个队列,如下图所示:
> 注:`routingkey` 和`bindingKey`其实是一个东西,只是为了避免与`Channel.Publish`混淆,所以叫做了`bindingKey`
- `Topic`(通配符):将消息发送给符合`routingKey`格式的队列;`topic`的`exchange`不能是任意的`routingKey`,必须是用点分割的单词,比如:`logs.error,logs.info`等,单词上限为`256`字节,`bindingKey`也是同样的形式
`Topic`和`Direct`模式类似,只不过`Topic`可以使用通配符进行匹配,它有如下两种匹配方式:
0. `*`:星号匹配任何一个单词,比如:`logs.*`可以匹配`logs.error`和`logs.info`
0. `#`:井号可以匹配一个或多个单词,比如:`logs.#`可以匹配`logs.error`和`logs.app.error`
这有一个简单的例子:
在上面的例子中`Q1`与`*.orange.*`绑定,`Q2`与`*.*.rabbit`和`lazy.#`绑定;当有一条`routingkey`为`quick.orange.rabbit`的消息时,会发送到`Q1`和`Q2`队列;而当`rougingKey=quick.orange.fox`的消息到来时会发送到`Q1`队列;而`lazy.brown.fox`的消息会发送到`Q2`队列
而如果出现了与上面的`bindingKey`不匹配的消息时,比如:`quick.orange.male.rabbit`,那么此条消息就会被直接抛弃掉
> 注:当一个队列绑定`bandingkey=#`时,那么他就会接收所有的消息,此时就类似于`fanout`模式
>
> 当`topic`模型下,`bandingKey`不包含`#`和`*`时,那么就是指定了一个具体的`bangingKey`,此时就类似于`driect`模式
- `headers exchange`:用于通过`Header`中的属性来实现消息的路由,而会自动忽略掉`routingKey`,根据消息`Headers`属性和`Binding Headers`属性的匹配规则路由消息
向Headers Exchange发送消息时,可以在Headers中定义键值对。Headers Exchange将根据消息Headers属性键值对和绑定属性键值对的匹配情况路由消息。
匹配算法由一个特殊的绑定属性键值对控制。该属性为**x-match**,只有以下两种取值:
- all:所有除**x-match**以外的绑定属性键值对必须和消息Headers属性键值对匹配才会路由消息。
- any:只要有一组除**x-match**以外的绑定属性键值对和消息Headers属性键值对匹配就会路由消息
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
参考链接: