如何设计一个消息队列?

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

本文已经收录到Github仓库该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点欢迎star~

Github地址https://github.com/Tyson0314/Java-learning


如果让你来设计一个 MQ该如何下手需要考虑哪些问题又有哪些技术挑战

对于 MQ 来说不管是 RocketMQ、Kafka 还是其他消息队列**它们的本质都是一发一存一消费。**下面我们以这个本质作为根一起由浅入深地聊聊 MQ。

从 MQ 的本质说起

将 MQ 掰开了揉碎了来看都是「一发一存一消费」再直白点就是一个「转发器」。

生产者先将消息投递一个叫做「队列」的容器中然后再从这个容器中取出消息最后再转发给消费者仅此而已。

上面这个图便是消息队列最原始的模型它包含了两个关键词消息和队列。

1、消息就是要传输的数据可以是最简单的文本字符串也可以是自定义的复杂格式只要能按预定格式解析出来即可。

2、队列大家应该再熟悉不过了是一种先进先出数据结构。它是存放消息的容器消息从队尾入队从队头出队入队即发消息的过程出队即收消息的过程。

原始模型的进化

再看今天我们最常用的消息队列产品RocketMQ、Kafka 等等你会发现它们都在最原始的消息模型上做了扩展同时提出了一些新名词比如主题topic、分区partition、队列queue等等。

要彻底理解这些五花八门的新概念我们化繁为简先从消息模型的演进说起道理好比架构从来不是设计出来的而是演进而来的

2.1 队列模型

最初的消息队列就是上一节讲的原始模型它是一个严格意义上的队列Queue。消息按照什么顺序写进去就按照什么顺序读出来。不过队列没有 “读” 这个操作读就是出队从队头中 “删除” 这个消息。

这便是队列模型它允许多个生产者往同一个队列发送消息。但是如果有多个消费者实际上是竞争的关系也就是一条消息只能被其中一个消费者接收到读完即被删除。

2.2 发布-订阅模型

如果需要将一份消息数据分发给多个消费者并且每个消费者都要求收到全量的消息。很显然队列模型无法满足这个需求。

一个可行的方案是为每个消费者创建一个单独的队列让生产者发送多份。这种做法比较笨而且同一份数据会被复制多份也很浪费空间。

为了解决这个问题就演化出了另外一种消息模型发布-订阅模型。

在发布-订阅模型中存放消息的容器变成了 “主题”订阅者在接收消息之前需要先 “订阅主题”。最终每个订阅者都可以收到同一个主题的全量消息。

仔细对比下它和 “队列模式” 的异同生产者就是发布者队列就是主题消费者就是订阅者无本质区别。唯一的不同点在于一份消息数据是否可以被多次消费。

2.3 小结

最后做个小结上面两种模型说白了就是单播和广播的区别。而且当发布-订阅模型中只有 1 个订阅者时它和队列模型就一样了因此在功能上是完全兼容队列模型的。

这也解释了为什么现代主流的 RocketMQ、Kafka 都是直接基于发布-订阅模型实现的此外RabbitMQ 中之所以有一个 Exchange 模块其实也是为了解决消息的投递问题可以变相实现发布-订阅模型。

包括大家接触到的 “消费组”、“集群消费”、“广播消费” 这些概念都和上面这两种模型相关以及在应用层面大家最常见的情形组间广播、组内单播也属于此范畴。

所以先掌握一些共性的理论对于大家再去学习各个消息中间件的具体实现原理时其实能更好地抓住本质分清概念。

透过模型看 MQ 的应用场景

目前MQ 的应用场景非常多大家能倒背如流的是系统解耦、异步通信和流量削峰。除此之外还有延迟通知、最终一致性保证、顺序消息、流式处理等等。

那到底是先有消息模型还是先有应用场景呢答案肯定是先有应用场景也就是先有问题再有消息模型因为消息模型只是解决方案的抽象而已。

MQ 经过 30 多年的发展能从最原始的队列模型发展到今天百花齐放的各种消息中间件平台级的解决方案我觉得万变不离其宗还是得益于消息模型的适配性很广。

我们试着重新理解下消息队列的模型。它其实解决的是生产者和消费者的通信问题。那它对比 RPC 有什么联系和区别呢

通过对比能很明显地看出两点差异

1、引入 MQ 后由之前的一次 RPC 变成了现在的两次 RPC而且生产者只跟队列耦合它根本无需知道消费者的存在。

2、多了一个中间节点「队列」进行消息转储相当于将同步变成了异步。

再返过来思考 MQ 的所有应用场景就不难理解 MQ 为什么适用了因为这些应用场景无外乎都利用了上面两个特性。

举一个实际例子比如说电商业务中最常见的「订单支付」场景在订单支付成功后需要更新订单状态、更新用户积分、通知商家有新订单、更新推荐系统中的用户画像等等。

引入 MQ 后订单支付现在只需要关注它最重要的流程更新订单状态即可。其他不重要的事情全部交给 MQ 来通知。这便是 MQ 解决的最核心的问题系统解耦。

改造前订单系统依赖 3 个外部系统改造后仅仅依赖 MQ而且后续业务再扩展比如营销系统打算针对支付用户奖励优惠券也不涉及订单系统的修改从而保证了核心流程的稳定性降低了维护成本。

这个改造还带来了另外一个好处因为 MQ 的引入更新用户积分、通知商家、更新用户画像这些步骤全部变成了异步执行能减少订单支付的整体耗时提升订单系统的吞吐量。这便是 MQ 的另一个典型应用场景异步通信。

除此以外由于队列能转储消息对于超出系统承载能力的场景可以用 MQ 作为 “漏斗” 进行限流保护即所谓的流量削峰。

我们还可以利用队列本身的顺序性来满足消息必须按顺序投递的场景利用队列 + 定时任务来实现消息的延时消费 ……

MQ 其他的应用场景基本类似都能回归到消息模型的特性上找到它适用的原因这里就不一一分析了。

总之就是建议大家多从复杂多变的实践场景再回归到理论层面进行思考和抽象这样能吃得更透。

如何设计一个 MQ

了解了上面这些理论知识以及应用场景后下面我们再一起看下到底如何设计一个 MQ

4.1 MQ 的雏形

我们还是先从简单版的 MQ 入手如果只是实现一个很粗糙的 MQ完全不考虑生产环境的要求该如何设计呢

文章开头说过任何 MQ 无外乎一发一存一消费这是 MQ 最核心的功能需求。另外从技术维度来看 MQ 的通信模型可以理解成两次 RPC + 消息转储。

有了这些理解我相信只要有一定的编程基础不用 1 个小时就能写出一个 MQ 雏形

1、直接利用成熟的 RPC 框架Dubbo 或者 Thrift实现两个接口发消息和读消息。

2、消息放在本地内存中即可数据结构可以用 JDK 自带的 ArrayBlockingQueue 。

4.2 写一个适用于生产环境的 MQ

当然我们的目标绝不止于一个 MQ 雏形而是希望实现一个可用于生产环境的消息中间件那难度肯定就不是一个量级了具体我们该如何下手呢

1、先把握这个问题的关键点

假如我们还是只考虑最基础的功能发消息、存消息、消费消息支持发布-订阅模式。

那在生产环境中这些基础功能将面临哪些挑战呢我们能很快想到下面这些

1、高并发场景下如何保证收发消息的性能

2、如何保证消息服务的高可用和高可靠

3、如何保证服务是可以水平任意扩展的

4、如何保证消息存储也是水平可扩展的

5、各种元数据比如集群中的各个节点、主题、消费关系等如何管理需不需要考虑数据的一致性

可见高并发场景下的三高问题在你设计一个 MQ 时都会遇到「如何满足高性能、高可靠等非功能性需求」才是这个问题的关键所在。

2、整体设计思路

先来看下整体架构会涉及三类角色

另外将「一发一存一消费」这个核心流程进一步细化后比较完整的数据流如下

基于上面两个图我们可以很快明确出 3 类角色的作用分别如下

1、Broker服务端MQ 中最核心的部分是 MQ 的服务端核心逻辑几乎全在这里它为生产者和消费者提供 RPC 接口负责消息的存储、备份和删除以及消费关系的维护等。

2、Producer生产者MQ 的客户端之一调用 Broker 提供的 RPC 接口发送消息。

3、Consumer消费者MQ 的另外一个客户端调用 Broker 提供的 RPC 接口接收消息同时完成消费确认。

3、详细设计

下面再展开讨论下一些具体的技术难点和可行的解决方案。

难点1RPC 通信

解决的是 Broker 与 Producer 以及 Consumer 之间的通信问题。如果不重复造轮子直接利用成熟的 RPC 框架 Dubbo 或者 Thrift 实现即可这样不需要考虑服务注册与发现、负载均衡、通信协议、序列化方式等一系列问题了。

当然你也可以基于 Netty 来做底层通信用 Zookeeper、Euraka 等来做注册中心然后自定义一套新的通信协议类似 Kafka也可以基于 AMQP 这种标准化的 MQ 协议来做实现类似 RabbitMQ。对比直接用 RPC 框架这种方案的定制化能力和优化空间更大。

难点2高可用设计

高可用主要涉及两方面Broker 服务的高可用、存储方案的高可用。可以拆开讨论。

Broker 服务的高可用只需要保证 Broker 可水平扩展进行集群部署即可进一步通过服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制来保证。

存储方案的高可用有两个思路1参考 Kafka 的分区 + 多副本模式但是需要考虑分布式场景下数据复制和一致性方案类似 Zab、Raft等协议并实现自动故障转移2还可以用主流的 DB、分布式文件系统、带持久化能力的 KV 系统它们都有自己的高可用方案。

难点3存储设计

消息的存储方案是 MQ 的核心部分可靠性保证已经在高可用设计中谈过了可靠性要求不高的话直接用内存或者分布式缓存也可以。这里重点说一下存储的高性能如何保证这个问题的决定因素在于存储结构的设计。

目前主流的方案是追加写日志文件数据部分 + 索引文件的方式很多主流的开源 MQ 都是这种方式索引设计上可以考虑稠密索引或者稀疏索引查找消息可以利用跳转表、二份查找等还可以通过操作系统的页缓存、零拷贝等技术来提升磁盘文件的读写性能。

如果不追求很高的性能也可以考虑现成的分布式文件系统、KV 存储或者数据库方案。

**难点4消费关系管理

**

为了支持发布-订阅的广播模式Broker 需要知道每个主题都有哪些 Consumer 订阅了基于这个关系进行消息投递。

由于 Broker 是集群部署的所以消费关系通常维护在公共存储上可以基于 Zookeeper、Apollo 等配置中心来管理以及进行变更通知。

难点5高性能设计

存储的高性能前面已经谈过了当然还可以从其他方面进一步优化性能。

比如 Reactor 网络 IO 模型、业务线程池的设计、生产端的批量发送、Broker 端的异步刷盘、消费端的批量拉取等等。

4.3 小结

再总结下要回答好如何设计一个 MQ

1、需要从功能性需求收发消息和非功能性需求高性能、高可用、高扩展等两方面入手。

2、功能性需求不是重点能覆盖 MQ 最基础的功能即可至于延时消息、事务消息、重试队列等高级特性只是锦上添花的东西。

3、最核心的是能结合功能性需求理清楚整体的数据流然后顺着这个思路去考虑非功能性的诉求如何满足这才是技术难点所在。

参考https://toutiao.io/posts/ix9hfyh/preview


最后给大家分享一个Github仓库上面有大彬整理的300多本经典的计算机书籍PDF包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等可以star一下下次找书直接在上面搜索仓库持续更新中~

Github地址https://github.com/Tyson0314/java-books

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6