欢迎大家到我的博客阅读 Yin's Blog | Yin's Blog (yinkai.cc)
这篇文章,我们来讲解一下消息队列中像队列、主题、分区等基础概念。<!--more-->只有搞清楚他们,才能进行后续的学习。
每种消息队列都有自己的一套消息模型,像队列、主题、分区这些名词概念,在不同的产品中都会涉及,但含义又不太一样。之所以没有一套适用的标准,是因为标准的进化跟不上消息队列的演变速度,故这些东西没有标准。
要想搞清楚队列、主题、分区是什么东西,需要从消息队列的演变说起。
主题和队列有什么区别?
在互联网的架构师圈中流传着这样一句话:”好的架构不是设计出来的,而是演进出来的“。
最初的消息队列,就是一个严格意义上的队列。在维基百科中,队列的定义是这样的:
队列是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
其中比较有几个关键的点,第一个是先进先出,在消息入队出队过程中,需要保证这些消息严格有序,按照什么顺序进队,就要按照什么顺序出队。队列中的”读“就是出队,”写“就是入队。
早期的消息队列就是按照队列的数据结构来进行设计的。生产者发送消息就是入队操作,消费者接收消息就是出队操作,服务端存放消息的容器就称为 ”队列“。
如果有多个生产者往同一个队列里发送消息,这个队列中可以消费的消息,就是这些生产者发送的消息的合集,顺序就是这些生产者发送消息的顺序。如果多个消费者从同一个队列里接收消息,这些消费者属于竞争关系,每个消费者只能收到队列中的一部分消息,即每一条消息只能被一个消费者消费。
如果需要实现将一份数据发送给多个消费者,例如,对于一份订单数据,风控系统、分析系统、支付系统等都需要接收消息。这个时候,单个队列就无法满足了,一个可行的办法是,为每个消费者创建一个单独的队列,让生产者给每个消费者都发送一份数据。显然这个方法是很笨的,同样的一份消息数据被复制到多个队列中是很浪费资源的。更重要的是,这样就违背了消息队列 ”解耦“ 这个设计初衷,因为生产者需要提前知道有哪些消费者。
为解决这个问题,就演化出了另一种模型:“ 发布 - 订阅模型(Publish-Subscribe Pattern”。
在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。
在消息队列的历史上很长一段时间里,队列模型 和 发布-订阅模型 是并存的,有些消息队列同时支持这两种模型,让业务方自己根据场景进行选择。
对比一下两种模型:
-
生产者----发布者
-
消费者----订阅者
-
队列 ----- 主题
它们最大的区别就是:一份数据能不能被多次消费。
在这种发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。也就是说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。
现代的消息队列产品使用的消息模型大多是这种发布 - 订阅模型,当然也有例外。
RabbitMQ 的消息模型
RabbitMQ 就是少数依然坚持适用队列模型的产品之一。那它是怎么解决多个消费者的问题的呢?这里实际上是将 RabbitMQ 的一个特色模块利用起来了,它位于生产者和队列之间,生产者不关心将消息发送到哪个队列,而是发送给 Exchange,由 Exchange 上配置的路由规则来决定将消息发送到哪些队列中。
同一个消息如果需要多个消费者消费,则可以通过配置 Exchange 将消息发送到多个队列,每个队列中都存一份完整的消息数据,可以为一个消费者提供服务。这样就实现了 发布-订阅模型 中,”一个消息被多个消费者消费“ 的功能。具体的配置,可以查看 官网
RocketMQ 的消息模型
RocketMQ 使用的消息模型是标准的 发布 - 订阅模型,在其术语中,生产者、消费者和主题与上面讲的完全一致。
但 RocketMQ 中也有队列这个概念,并且十分重要。它的具体作用在后面会说起,我们先来看看消息队列的消费机制。
几乎所有的消息队列产品都是使用 ”请求 - 确认“ 机制,确保消息不会在传递过程中由于网络问题或服务器故障丢失。具体的做法与 TCP 连接过程中确认应答类似。在生产端,生产者先将消息发送给服务端,也就是 Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。
如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。
这样的机制保证了消息传递过程中的可靠性。但是引入这个机制,就存在一个问题:为了确保消息的有序性,在一条消息被成功消费前,下一条消息不能被消费,否则就会出现消息空洞,违背了有序性这个原则。
"消息空洞"指的是消息队列中存在一些未被消费的消息序号或标识,这些消息应该按照特定顺序被处理,但由于某些原因,其中的一些消息尚未被消费。这种情况可能会违反消息传递的有序性,因为一些消息被提前消费,而其他消息被延迟或遗漏。
也就是说,每个主题任意时刻只能有一个消费者消费,那就无法同过水平扩展消费者的数量来提升消费端总体的性能。因此,RocketMQ 在主题下引入了队列的概念。
每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。
RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间的消费进度没有关联,即一个消息被 Consumer Group 1 消费过,也会再给 Consumer Group 2 消费。
在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息是不会马上进行删除的,这就需要 RocketMQ 为每个消费组在队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,这个位置之后的消息都没有被消费过,每消费一条消息,该位置就加一。
可以对照这张图加深理解。
Kafka 的消息模型
Kafka 的消息模型和 RocketMQ 完全一样,唯一的区别就是,在 Kafka 中,队列这个概念的名词不一样,Kafka 中对应的名称是 ”分区((Partition)“,含义和功能是没有任何区别的。
小结
本文讲述了队列和主题的区别,这两个概念背后对应着两种不同的消息队列模型:队列模型 和 发布-订阅模型。
常用的消息队列中,RabbitMQ 采用队列模型,但它可以通过 Exchange 模块实现 发布-订阅 的功能。RocketMQ 和 Kafka 采用的是 发布-订阅模型,并且二者的消息模型是基本一致的。
最后需要清楚的是,本文介绍的消息模型和相关概念只是业务层面的模型,不同的消息队列的底层实现肯定有很大的区别,只是从业务上来看,它们的类似的。