MQ消息队列的本质
一、本质
MQ消息队列,既然是消息队列,直观理解它包含了两个关键词:消息和队列
- 消息:就是要传输的数据
- 队列: 一种先进先出数据结构 (数据从队尾入队,从队头出队,当然也可以存放数据)
对于 MQ 来说,不管是 Kafka、RocketMQ 还是其他消息队列,它们的本质模型都是:
- 生产者—发送消息
- 队列—存储消息/转发消息
- 消费者—消费消息
二、演进
之前进过的Kafka、RocketMQ ,它们都是MQ本质的模型上做了扩展,加入一些新的概念,比如:topic主题 、partition分区 、consumerqueue队列等。
这个就和之前进过的架构从来不是设计出来的,而是演进而来的,消息模型也是一样(演进)。
2.1、点对点队列模型
这个类比java中的BlockingQueue 阻塞队列,就是Queue原始的模型,消息先进先出,出队就从对头删除。
总结:允许多个生产者往同一个队列发送消息。如果有多个消费者竞争,那么一条消息只能被其中一个消费者消费,读完即被删除。
2.2、发布订阅模型
如果需要将消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。这个就是发布订阅模型,消息存储在topic主题,消费者要先订阅topic主题。
总结:当只有一个消费者时,和点对点队列模型是一致的,即兼容了点对点的队列模式。唯一的不同就是消息可以被多次消费。
因为发布订阅模型兼容了队列模型,这也解释了为什么Kafka和RocketMQ 都是基于topic主题的发布订阅模型,而RabbitMQ 中的Exchange 交换机,也是发布订阅模型的一种实现。
三、场景
通过上面的对MQ本质的分析,它其实解决的是生产者与消费者之间的通信问题。
相比传统的通信方式比如HTTP(rest)、RPC等, 由一次通信变成了两次通信,相当于同步通信变成了异步通信,而且生产者只和队列耦合,这样生产者和消费者就解耦。由于队列本身能存储消息,所以对于大流量场景进行限流保护,即所谓的流量削峰。然后利用队列本身的顺序性实现顺序消息等等。
这就是MQ 的应用场景:系统解耦、异步通信和流量削峰。
四、手写 MQ
通过上面分析MQ的模型就是;两次通信 + 消息转储
-
1、直接基于成熟的 RPC 框架(netty),实现两个接口:发消息和读消息。
-
2、消息直接放在本地内存中,数据结构可以用 JDK 自带的 BlockingQueue 。
这个实现的只是一个简单的MQ功能。
面对高并发场景,如何满足高性能以及高可用和高扩展性?
这就涉及到了三类角色:
1、Broker:MQ 中最核心的部分,是 MQ 的服务端,核心逻辑几乎全在这里,它为生产者和消费者提供 RPC 接口,负责消息的存储、备份和删除,以及消费关系的维护等。
2、Producer:MQ 的客户端之一,调用 Broker 提供的 RPC 接口发送消息。
3、Consumer:MQ 的另外一个客户端,调用 Broker 提供的 RPC 接口接收消息,同时完成消费确认。
4.1、详细设计
RPC 通信
如果不重复造轮子,直接利用成熟的 RPC 框架 Dubbo,也可以基于 Netty 来做底层通信。
存储设计
消息的存储是 MQ 的核心,如果可靠性要求不高的话直接用内存或者分布式缓存也可以,目前主流的方案是:追加写日志文件(数据) + 索引文件的方式 ,索引设计上可以考虑稀疏索引,查找消息是二分查找和跳表,也可以pagecache和零拷贝提升磁盘IO性能。
高可用设计
分为Broker 服务的高可用以及存储设计的高可用。
Broker 服务的高可用,只需要保证 Broker 可水平扩展进行集群部署即可,通过服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制来保证。
存储设计的高可用有两个思路:
- 分区 + 多副本模式,但是需要考虑分布式场景下数据复制和一致性方案(类似 Zab、Raft等协议),并实现自动故障转移;
- 主流的 DB、分布式文件系统、带持久化能力的 KV 系统,它们都有自己的高可用方案。
消费关系管理
由于 Broker 是集群部署的,所以消费关系通常维护在公共存储上,可以基于 Zookeeper、Apollo、Nacos 等配置中心来管理以及进行变更通知
高性能设计
Reactor 网络 IO 模型、业务线程池的设计、生产端的批量发送、Broker 端的异步刷盘、消费端的批量拉取等等。
五、总结
- 要从功能性需求(收发消息)和非功能性需求(高性能、高可用、高扩展等)两方面入手。
- 功能性需求不是重点,能覆盖 MQ 最基础的功能即可,至于延时消息、事务消息、重试队列等高级特性只是锦上添花的东西。
- 最核心的是能结合功能性需求,理清楚整体的思路,然后顺着这个思路去考虑非功能性的诉求如何满足,这才是技术难点以及核心价值。