MQ发展到现在共有两种模型:队列模型、发布-订阅模型
队列模型
它允许多个生产者往同一个队列发送消息。但多个消费者之间是竞争的关系,也就是说一条消息只能被其中一个消费者接收到,读完即被删除。
发布-订阅模型
如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息,队列模型是无法满足这个需求的。
一个可行的方案是:为每个消费者创建一个单独的队列,让生产者发送多份。这种做法比较笨,而且同一份数据会被复制多份,也很浪费空间。而且还有一个问题,生产者必须要知道有多少个消费者,为每个消费者单独发送一份消息,这实际上违背了消息队列「解耦」这个设计初衷。
为了解决这个问题,就演化出了另外一种消息模型:发布-订阅模型。
在发布-订阅模型中,消息的发送方称为发布者(Publisher)
,消息的接收方称为订阅者(Subscriber)
, 服务端存放消息的容器称为主题(Topic)
。发布者将消息发送到主题中,订阅者在接收消息之前需要先「订阅主题」。「订阅」在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。
实际上,在这种发布-订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。也就是说,发布-订阅模型在功能层面上是可以兼容队列模型的。(这两种消息模型其实并没有本质上的区别,都可以通过一些扩展或者变化来互相替代。)
发布-订阅模型和队列模型的唯一不同点在于:一份消息数据是否可以被多次消费。
主流MQ的消息模型实现
现在主流消息队列使用的消息模型大多是发布-订阅模型,除了RabbitMQ。
RabbitMQ
RabbitMQ采用的消息模型是「队列模型」。那么他如何实现同一消息被多个消费者消费呢?
实际上,RabbitMQ引入了「交换机」(exchange)
这一概念来解决这一问题。
在RabbitMQ中,exchange
位于生产者和队列之间,生产者并不关心将消息发送给哪个队列,而是将消息发送给exchange
,由exchange
上配置的策略来决定将消息投递到哪些队列中。这样RabbitMQ就可以实现发表-订阅的功能了。
RocketMQ
RocketMQ采用的是「发布-订阅」模型。
为了保证消息消费时的可靠性,需要引入ACK机制,而ACK又要求有序,这就意味着在第一条消费被成功消费前,下一条消息是不能被消费的。为了解决这一问题,RocketMQ引入了队列(queue)
的概念。
每个主题包含多个队列,通过多个队列来实现多实例并行的生产和消费。
从这里也可以看出,RocketMQ只能在队列层面上保证消息的有序性,主题层面是无法保证消息的严格顺序的。
在RocketMQ中,订阅者的概念是通过消费组(Consumer Group)
来体现的。每个消费组都消费Topic中一份完整的消息,不同消费组之间消费进度彼此不受影响。也就是说,一条消息被消费组A消费过,也会再给消费组B消费。
在消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者C1消费了,那同组的其他消费者就不会再收到这条消息。
在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个 offset
,这个 offset
之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息, offset
就加一。
这个消费位置是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。
Kafka
Kafka采用的是「发布-订阅」模型。
Kafka的消息模型和RocketMQ是完全一样的(因为RocketMQ就是借鉴了Kafka的设计理念)唯一的区别是,在Kafka 中,队列这个概念的名称不一样,Kafka中对应的名称是分区(Partition)
,含义和功能是没有任何区别的。
参考自李玥老师《消息队列高手课》极客时间专栏,跟着老师一套学下来收获很大,墙裂推荐==