大家好,我是苍何。

经过前面两篇文章,相信大家对 RocketMQ 的整体架构以及主题 Topic 有了一定的了解,也理清了消费者、生产者、broker、nameserver 这几个组件的概念及作用。

图解RocketMQ之消息模型详解(1)_java-rocketmq

RocketMQ 我们通常叫消息队列,不知道大家有没有想过,为何叫消息队列呢?怎么不叫消息组件或者消息中间件呢?(当然有这么叫的,但少的就像我的头发一样😂)

这其实还要和我们这篇文章的主题(消息模型)有关,其实在早些时候,用来做消息中间件的组件确实是通过队列的方式来实现的,所以消息队列这一叫法多少有些历史原因。但如今很多消息中间件底层不仅仅是通过一个队列来简单实现消息存储的。

下面是本篇主要内容,方便小伙伴们快速查看。

图解RocketMQ之消息模型详解(1)_消息队列_02

消息队列两大基础模型

放眼整个消息队列,需要着重理解两个基础模型,他们分别是队列模型主题模型

队列模型

我们知道队列是一种数据结构,秉持着先进先出的特性。为了更好的理解,我们还是请出炒菜大师鸡毛,这不暑假来了嘛,鸡毛和朋友们来贵州某景区旅游。

队列就像游客排队在售票窗口买票一样,排在前面的游客先买,买完就走,而排在后面的鸡毛就得乖乖等他前面的人都买完,才能轮到他。

图解RocketMQ之消息模型详解(1)_java-rocketmq_03

而这里的队列模型本质和队列数据结构并无什么差别,消息在队列中也是先进先出,先来的消息先被消费,后来的消息后被消费。

图解RocketMQ之消息模型详解(1)_rocketmq_04

一开始消息队列就是这么设计的,消息从生产者发出后,到了消息队列,消费者们就依次取出消息消费。一切都那么顺理成章,消息被一个消费者消费完了也就完了,消息会被删除,其余消费者无法再消费这条消息。

这在早期的时候是满足需求的,消息被消费完后自然会被删除,防止堵塞在队列中影响后续消费者进行消费。

图解RocketMQ之消息模型详解(1)_主题模型_05

那如果这时候,消费者 B 也要消费刚刚消费者 A 消费完的消息,该怎么办呢?聪明的你估计很快想到,那可以增加多个队列啊,复制同一份消息到多个队列,A 消费完队列 1 的消息,B 接着消费队列 2 的消息,不就可以啦?

图解RocketMQ之消息模型详解(1)_主题模型_06

这样问题是可以解决啦,但一份消息被保存在了多个队列上,不说别的,光资源占用和性能就极大有影响,而且随着消费者的增多,队列将会增多,冗余的消息会越来越多,雪球越滚越大,最后很难 hold 住,且大大有违背消息中间件的核心之一——解耦

那么有没有什么更好的办法呢?那当然有,那就是接下来的要讲的主题模型,也可以叫做发布-订阅模型。

主题模型

主题模型其实和我们设计模式中的观察者模式很像,消费者不再争抢着去队列中消费消息,而是去订阅消息,而订阅的依据是啥呢?其实就是上一篇中说的主题(Topic),

在主题模型中,生产消息的为发布者(Publisher),消费消息的为订阅者(Subscriber),存放消息的逻辑容器为主题(Topic)

这样,发布者和订阅者之间只需要基于主题,比如我往 normal-topic 这个主题发布消息,那么订阅了 normal-topic 这个主题的就能收到这个消息,而我向 vip-topic 这个主题发布消息,订阅了该主题的就能消费该主题的消息。

这样就能满足,不同消费者消费同一消息,且相互各不影响。是不是就方便很多了呢?

图解RocketMQ之消息模型详解(1)_java_07

RocketMQ 中的消息模型

RocketMQ 中的消息模型就是通过主题模型(也即发布订阅模型)来实现的,那么内部具体是如何实现的呢?

我们上一篇中说到 topic其实是一个逻辑概念,真正存放消息的最小单元其实是队列,那么问题很多的小明他就问了,消费者怎么知道该从哪里消费消息?一次消费完后如何保证下次不会再重复消费该消息呢?如果业务进行故障恢复,如何重新消费消息?

要想理清这些,就得了解 RocketMQ 的消息进度原理

别急,我们先来理清楚 2 个概念,消息点位消费点位

消息点位

消息实际存储在队列中,每个消息在队列中都会有一个唯一的 long 类型的坐标,有点类似于数组下标用来定位消息在队列中的位置,这个坐标称之为消息点位(Offset)。

图解RocketMQ之消息模型详解(1)_rocketmq_08

队列中最早的一条消息叫最小消费位点(MinOffset),相对应的,最新的一条消息就叫最大消费位点,按照道理来说,队列是可以无限扩充的,也即理论上最小和最大消费点位之间可以被无限拉大。但无穷的东西都是不存在的,当然还是会受限于宿主机(也就是服务器)内部的存储。一个 1 核 1 G 的服务器,你说能大到哪里去?😂

不够存咋办?RocketMQ 内部会滚动删除队列中最小的消息,所以要想消息长久保存,要合理配置好资源。

消费点位

上面说消息点位其实是确定消息在队列中的位置坐标,每个主题的队列都可以被多个消费者订阅,如果消息被消费者消费完后就删除了,那下一个消费者就消费不到同样的消息了。

所以消费者消费完消息后不能删除,需要记录当前消费的消息的坐标位置,其实可以看成是消费记录,这个其实就是消费点位(ConsumerOffset)。

消费点位和消息点位的关系如下图:

图解RocketMQ之消息模型详解(1)_消息队列_09

可以看出,其实消费点位记录的就是消息点位坐标,且介于最大消息点位和最小消息点位之间。且是存在服务端的和客户端无关,因此 RocketMQ 支持消费者的消息进度恢复。

消费者消费完一条消息就会记录一个消费点位,这样就能满足不同消费者可以消费同一条消息了。

图解RocketMQ之消息模型详解(1)_rocketmq_10

如上图所示,鸡毛和狗毛分别可以消费不同主题下的不同消息,且仅需要记录下各自的消费点位即可:

  • nomal-topic-鸡毛-3
  • nomal-topic-狗毛-3
  • vip-topic-鸡毛-2
  • vip-topic-狗毛-1

而且鸡毛和狗毛都可以同时消费 nomal-topic 这个主题下的 M 3 消息且消息消费完也不会消失,是不是很完美?

没错,相当完美,但实际企业级项目中,消费者并不只有单个,通常是以消费者组的形式存在,不同的消费者组可能位于不同的集群或者不同的机房,而生产者通常也会有多个,所以实际上,topic 是需要维护多个队列的。

至于有多少个队列,我们上一篇中说过,是可以在 topic 创建和编辑的时候配置的,消费者多就需要多配置,但至少要有一个队列

而生产者具体发送消息到哪个队列取决于 RocketMQ 的机制,默认是轮询方式,也就是根据消息产出的先后,依次向 topic 下的队列发送。

我们用一张图来系统性的分析企业级 RocketMQ 的具体部署架构吧,当然了熟悉 PmHub 的小伙伴应该也知道,PmHub 也完全按照企业级架构部署的(毕竟我们主打的就是企业级项目👍)

图解RocketMQ之消息模型详解(1)_主题模型_11

消费点位的设计非常灵活,当然了对于我们面试来说,同样也是有很多文章可以挖的,比如 RocketMQ 如何处理消息堆积,如何实现重复消费或者跳过部分消息不消费,我们将在下一篇继续深挖。

我是苍何,这是图解 RocketMQ 教程的第三篇,我们下篇见~