03丨消息模型:主题和队列

414 阅读8分钟

前言

你好,我是喜欢刨根问底的小明不明。在学习如何使用新事物之前,了解它的业务模型,必然是第一步,下面我将以技术演化的方式带大家体会消息模型——主题和队列,在这之前让我们先来看一下

学习新事物方法:先业务模型后实现

  • 想要学会使用某个工具,关键在于了解其业务模型。 比如: 你不会用洗衣机,看一下说明书,启动电源->设置->开始洗衣,就可以使用了,这就是很简单的业务模型
  • 想要学会实现某个工具,要在了解业务模型的基础上,了解具体实现原理。 比如: 你想要造一台洗衣机,你首先得知道洗衣机要有哪些功能,要有怎么样的操作流程,然后才开始设计实现方案

注意: 业务模型是一致的不代表实现是一致的,好比,不同品牌不同型号的洗衣机即使操作方式一样,实现的方式也一定有所区别,寿命,使用体验也会有区别。

消息模型

让我们从业务模型的角度以技术演化的方式走进消息队列的大门。

队列模型:最初的消息队列,就是一个严格意义上的队列。

先看一下维基百科对队列的定义

队列(queue),计算机科学中的一种抽象资料型别,是先进先出( "先进先出算法")(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。

关键词:

先进先出,意味着需要保证这些消息严格有序

队列模型.png

分析:

  • 生产:如果有多个生产者,队列中的消息就是所有生产者生产的消息的合集
  • 消息顺序:生产者发送消息的自然顺序。
  • 消费:如果有多个消费者消费同一个队列,那消费者之间就是竞争关系,一条消息只能被一个消费者消费。

现在来了需求:一个消息需要被多个消费者消费。如果是你,你会如何设计?

很自然的想法

每个消费者对应一个队列,如果要一条消息被多个消费者消费那就投递到多个队列。

你可以停下来,思考一下这样设计有什么弊端。

弊端

  • 浪费资源:一份消息数据被复制到多个队列会浪费资源
  • 违背了消息队列“解耦”这个设计初衷:生产者必须知道要有多少个消费者

为了解决多个消费者的问题演化出了RabbitMQ在队列模型的基础上的实践,也演化出了发布-订阅模型

RabbitMQ的消息模型

核心思想:Exchange 上配置的策略来决定将消息投递到哪些队列中。

分析

  • 同一份消息要被多个消费者消费,需要发送到多个队列中
  • 一个队列对应一个消费者

总结 (所以第一梯队MQ最慢

  • 没有解决消息拷贝带来的性能损失
  • 解决了生产者消费者之间的耦合问题。

现代主流消费模型:发布 - 订阅模型(可以重复消费)

发布 - 订阅模型.png

在发布-订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。

发布-订阅模型和队列模型的本质区别

  • 队列模型:消息数据不可重复消费
  • 发布-订阅模型:消息数据可重复消费

发布-订阅模型简单实现

分析:

  • 生产者根据topic投递到对应队列
  • 消费者根据订阅的topic接收消息(请注意是接收,因为消费获取消息的方式有两种Pull和Push后面的文章会进行分析)
  • 为了保证消息的重复消费,每个主题队列都会维护一个offset记录某个consumer的消费偏移量。

总结

根据Topic解耦了生产者消费者通过Offset保证了每个消息的重复消费

为了提升可靠性,消息队列实现的复杂度也相应提高

几乎所有消息队列都采用ACK(请求——确认机制),确保消息不会在传递过程中由于网络或服务器故障丢失。

mq ack机制.png

  • 生产端,生产者先将消息发送给服务端(消息队列),服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;
  • 消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。

请求确认机制,引入了新复杂度

为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则。

也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。

kafka,rocketMQ为了解决这个问题,采用了一样的方法解决这个问题,但是叫法有所区别,在kafka中叫分区,在rocketMQ叫队列

kafka和rocketMQ对发布订阅模型的实践

核心思想:每个主题包含多个队列,通过多个队列来实现实例并行生产和消费。

分析:

  • 生产者可以并发地投递消息到一个主题的不同队列中。
  • 消费者组可以对应一个微服务,消费者可以对应一个微服务实例。
  • 消费者组之间可以重复消费消息,消费者之间竞争消息。
  • 每个Topic的队列数量可以根据数据量和消费速度来合理配置。
  • 每队列每消费组维护一个消费位置(offset),记录这个消费组在这个队列上消费到哪儿了。

总结(有得必有失):

  • 该方法减弱了有序性,从主题级别的有序性,减弱到了一个主题下某个队列的有序性
  • 该方法解决了并发生产、并发消费的问题。

不要求严格顺序,能否做到单个队列的并行消费呢?

在消费的时候,为了保证消息的不丢失和严格顺序,每个队列只能串行消费,无法做到并发,否则会出现消费空洞的问题。那如果放宽一下限制,不要求严格顺序,能否做到单个队列的并行消费呢?

大概思路

把消息队列的先进先出,改成数组的随机访问,用offset来控制消息组具体要消费哪条消息,mq不主动删除消息,消息有过期时间,如果到了过期时间,只能确认不能重新该消费,只保留最大可设置天数的消息。超过该天数则删除,还要维护客户端确认信息,如果有客户端没确认,需要有重发机制。

总结

从一开始的队列模型中遇到了需要多个消费的问题,演化出了RabbitMQ的方案,发布订阅模型。

再到,发布订阅模型遇到了在有序性前提下,并发生产、并发消费的问题,演化出了kafka和rocketmq的方案。

从技术演化的技术看技术发展,对我们掌握技术本质有着很重要的意义。

kafka与rocketmq 业务模型虽然一直,但是他们的应用场景不一样,原因在于具体实现的不同。

但从业务模型切入,了解技术,是掌握技术使用的捷径。

参考资料