深入浅出消息队列----【必须掌握的两个基础模式】
本文仅是文章笔记,整理了原文章中重要的知识点、记录了个人的看法
文章来源:编程导航-鱼皮【yes哥深入浅出消息队列专栏】
一、队列模式
队列是一种数据结构,它的特性是先进先出,就跟平时打饭一样,排在前面的同学打完走了,后面的同学顶上去。
而消息队列直观上看来就是消息排成了队列,被消费了,这个消息就出队,后续的消息顶上。
刚开始的消息队列就是这样设计的,生成者发送的消息排列成队列,然后消费者们竞争消费排在队列上的消息。
为什么称之为竞争?
因为按照队列的特性,消息被消费了就出队了,所以一个消息只能被消费一次,因此消费者之间是竞争关系。
这就叫队列模式。
但是,一个消息可能多个消费者都感兴趣,但他们之间又不是竞争消费的关系,即这些消费者都想消费这个消息。
这个时候,队列模式就不合适了,因为在队列模式中,消息被消费后就出队了,而出队的消息如何再被别的消费者消费呢?
自然而然的可以想到将队列复制为多份,也就是多队列!
这样一条消息就被冗余到多个队列中,每个队列都包含全量的消息,这就满足了多个消费者之间不是竞争消费的关系。
但是这样的方式对存储不是很友好,因为随着消费者们的增加,队列会越来越多,冗余的消息也会越来越多。
于是,就演化出了发布-订阅模式。
二、发布-订阅模式
发布-订阅模式,顾名思义,生产者发布消息,消费者订阅消息,那么订阅的依据是什么呢?就是之前提到的 Topic(主题)。
发布-订阅模式想要实现的功能是:比如我向 Topic-LOL 主题发布消息,那么订阅了这个主题的消费者都能收到这个消息,向 Topic-DOTA 主题发布消息,那么订阅了 DOTA 主题的消费者都能收到和 DOTA 相关的消息。
从概念上说发布-订阅模式完美的解决了一个消息可以被多个消费者同时消费的诉求。
具体是如何实现的呢?
这里就需要引入消息位置(offset)的概念,这个概念可以类比理解为数组的下标。
要实现消息可以被多个消费者消费,那么只需维护每个消费者已经消费到的位置,每当消费者消费一条消息,消费位置就+1,然后消费者根据记录的消息位置去消费对应的数据。
这就跟遍历数组一样了,通过下标 +1 来访问后面的数据。
这就可以满足不同消费者消费同一条消息,且不影响他们之间的消费进度的需求。
如上图所示,需要记录的消息位置如下:
- Topic-LOL-鱼皮-3
- Topic-LOL-猪皮-5
- Topic-DOTA-鱼皮-1
- Topic-DOTA-猪皮-5
当鱼皮消费完 Topic-LOL 第三条消息后,将位置 +1,3 + 1 = 4,这样就可以顺利的访问到第四条消息。
假设后面又来了个蛇皮来消费消息,我们也不需要复制消息,只需要多记录一个蛇皮的消息位置即可。
这样即可当消费者变得很多的时候,对存储也不会有太大的影响。
但是上面的消费者只有一个,那一个消费组内的消费者如何消费消息呢?
让他们竞争同一个消费位置吗?那岂不是需要等上一个消费者消费完了,组内其他消费者才能消费下一条消息?
这样效率就很低了。所以还需要引入一个新的概念,在 RocketMQ 中叫队列(这个跟数据结构中的队列要区分下),在 kafka 中叫分区。
可以发现,生产者发往 Topic 的消息并不是在一个队列中,而是在多个队列中。
这样属于一个消费组的消费者们可以专门负责主题里面的一个队列,比如下图:
然后我们消费点位的记录维度就变成了 Topic-消费组-队列。
如上图所示,消费位置记录如下:
- Topic-LOL-鱼皮组-队列1-3
- Topic-LOL-鱼皮组-队列2-4
- Topic-LOL-猪皮组-队列1-1
- Topic-LOL-猪皮组-队列2-3
- Topic-DOTA-鱼皮组-队列1-1
- Topic-DOTA-鱼皮组-队列2-2
- Topic-DOTA-猪皮组-队列1-3
- Topic-DOTA-猪皮组-队列2-4
这样,就完美的解决了之前队列模式中一个消息只能被一个消费者消费的问题,也实现了消费者组之间消费互不影响,且消费组内多个消费者之间的消费也互不影响。
这个模式就很完美~
如果鱼皮组扩张了,比如又招了一个人,鱼皮-3,那此时咋办呢?
我们对应的主题也可以增加一个队列,比如 Topic-LOL。
注意,这里增加的队列,并不会将全部的消息复制一份,而是将消息平均分到每个队列中,因此,即使增加了队列,也不会增加存储的负担。
但是猪皮组还是两个人,现在有三个队列怎么办呢?
很简单,只要猪皮-1同时消费队列1和队列2就可以了。
至此,就清楚的了解了企业级消息队列的发布-订阅模式的核心原理:即 Topic 下分队列(分区),然后维护每个消费者组在每个 Topic 下的每个队列的消息位置(offset)来控制消息消费的进度。
消息位置的灵活性不仅仅能区分不同消费组或者消费者们的消费进度,还能实现重复消费或者跳过部分消息不消费的功能。
如鱼皮-1已经消费到 Topic-LOL-队列1-20,即第20条消息,但是鱼皮1一不小心把之前关于 Topic-LOL 的消费得到的结果数据丢失了,如果按照队列模式那就找不到消息了,因为消息已经出队没了。
而在发布-订阅模式中,仅需把这个消息位置变更成 Topic-LOL-队列1-0,这样又可以让鱼皮-1重新消费,只需要简单地改一条数据就能实现这样的功能。
假设 Topic-LOL-队列-1中的第21-30这10条消息是错误的,我们可以修改当前的消息点位成 Topic-LOL-队列1-30,这样鱼皮-1就直接跳过了这10条错误消息,从31条消息开始消费。
三、生产者如何确定消息发往哪个队列?
提个 Topic 里面有多个队列,那么生产者怎么知道要发到哪个队列?
一个很简单的办法就是轮询,比如生产者-A,要往 Topic-LOL 发送消息,那么第一条发给队列-1,第二条发给队列-2,第三条发给队列-3,第四条发给队列-1,第五条发给队列-2 … 如此往复即可。
这样每个队列的消息量会很平均,对应的消费者的工作量也会均衡。
当然也可以指定发往某个队列,比如有关匹配的消息都发往队列-1,有关大乱斗的消息都发往队列-2,有关云顶之弈的消息都发往队列-3.