从零开始讲清如何设计一个消息队列系统(MQ):通俗易懂、细节到位
大约 5 分钟
从零开始讲清如何设计一个消息队列系统(MQ):通俗易懂、细节到位
什么是消息队列?一句话说清楚
消息队列(Message Queue)是一个系统中用来传话的“快递中转站”。
它负责把“消息”从发送方(生产者)传递到接收方(消费者)—— 而且还能:
- 保证不会丢件(消息不丢失)
- 顺序到达(支持顺序消费)
- 如果收件人晚点来也没关系(异步解耦)
举个例子:
淘宝订单支付成功后,会发生很多事:
- 给我们发短信
- 给仓库发发货请求
- 给商家发账单
但我们点完“支付”并不需要等这些操作都做完才能返回页面,这就靠了消息队列:
支付系统 -> 发一条“支付成功”的消息 -> 各方系统慢慢接收处理面试题:我们来设计一个 MQ 系统,该怎么说?
一定要从这4个维度入手:
| 设计模块 | 职责 | 我们要考虑的点 |
|---|---|---|
| Producer | 生产消息 | 发送是否成功?是否重复? |
| Broker | 消息中转站 | 怎么存?能不能丢?顺序? |
| Consumer | 消费者 | 怎么消费?是否重复? |
| Registry | 注册中心 | 谁在哪?怎么发现彼此? |
系统流程全图
[Producer] -> [Broker] -> [Consumer]
^
[Registry]我们逐个拆开讲:
Producer:怎么把消息送出去?
它是消息的生产者,像做菜的一方。
要实现的功能:
- 能把一条消息发送到 Broker
- 要知道消息有没有成功送达(ack)
- 要支持“重发但不重复”
设计要点:
- 使用 TCP(Netty)建立连接
- 发完后等待 broker 返回 ACK
- 如果超时,自动重试(保证“最终送达”)
Broker:消息的中转站(核心)
消息队列的“心脏”,设计重点全在这!
它要做 5 件事:
| 功能 | 描述 |
|---|---|
| 接收消息 | 接收来自 Producer 的消息 |
| 缓存消息 | 先写入内存队列,快速暂存 |
| 持久化存储 | 写入磁盘,确保断电不丢 |
| 分发消息 | 把消息发送给消费者 |
| 管理消费进度 | 记录哪个消息被谁读了 |
消息存储设计(Broker 里)
可以模仿 Kafka 的设计:
/store/
├── topicA/
│ ├── log-0001.data
│ └── log-0002.data
└── topicB/- 所有消息顺序写入
.data文件(类似日志) - 每个 Topic 分区存储
- 内存队列做缓存(ConcurrentLinkedQueue)
- 定期 flush 到磁盘
为什么写磁盘也很快?
因为是顺序写,不像数据库那样随机插入!
Consumer:谁来读消息?怎么控制?
消息的“收件人”
消费有两种方式:
- 主动拉(pull):消费者定时去拉
- 被动推(push):Broker 推给我们
消费状态要记录:
- 每个 Consumer Group 有一个 offset
- 每个 topic/partition 有唯一消费进度
Registry:大家在哪?怎么通信?
一个 MQ 系统不能硬编码 IP,需要服务发现机制:
| 服务 | 作用 |
|---|---|
| 注册中心 | 每个 Producer / Broker / Consumer 启动时注册 |
| 心跳机制 | 保持“在线状态” |
| 服务发现 | Producer 可以拉到 Broker 列表 |
| 可选组件 | Nacos / Zookeeper / Eureka / etcd |
协议设计(怎么打包发消息?)
我们自定义一个简单协议结构:
| 魔数 | 版本 | 消息类型 | 数据长度 | 数据体 |
4B 1B 1B 4B N 字节- 类型:1=发送消息,2=拉取消息,3=ACK,4=心跳
- 数据体一般是 JSON 或 Protobuf 编码
我们必须考虑的系统问题:
消息不能丢
- 内存队列 + 日志文件双重写入
- 写成功后才 ack 给 producer
消息不能重复消费
- 使用唯一消息 ID 去重
- offset 要持久化(存 Redis 或本地)
支持顺序
- 同一个 Topic 分多个 Partition
- 每个 Partition 保证消息有序
多线程并发消费
- 每个 Partition 一个消费线程,互不干扰
面试中这么答更有说服力
我们会把 MQ 拆成 Producer、Broker、Consumer、Registry 四个模块。Broker 是核心,负责缓存、存储、转发、ack、offset 管理等。网络通信基于 Netty,自定义协议传输。每条消息写入时写入内存队列和磁盘日志,保证高性能和可靠性。消费端拉取时使用 offset 管理,防止重复消费,同时支持按 Partition 顺序消费。服务发现通过注册中心实现系统扩展性和高可用。
最小项目结构(我们可以手撸个 demo)
my-mq/
├── net/ ← Netty 网络通信
├── store/ ← 消息落盘
├── queue/ ← 内存队列
├── broker/ ← 消息转发逻辑
├── consumer/ ← 消费者客户端
├── producer/ ← 生产者客户端
└── registry/ ← 注册中心模拟总结
| 目标 | 设计要点 |
|---|---|
| 不丢消息 | 写日志 + Ack 确认机制 |
| 不重复消费 | offset + 去重 ID |
| 支持高并发写入 | 内存队列 + 异步写磁盘 |
| 高可用 | 多 Broker + 注册中心发现 |
工程里的关键补充点
可靠性与一致性
- 至少一次与至多一次要选清楚,默认至少一次,业务需要去重
- ACK 要与落盘绑定,避免“已确认但未落盘”的丢失
- 消息体要有唯一 ID,方便追踪与幂等
消费性能与背压
- 推模式要做消费速率保护,避免 Broker 压垮 Consumer
- 拉模式要控制拉取批次与频率,避免空转消耗
- 消费线程池要有队列长度保护
存储与堆积
- 分区数量规划要跟吞吐与并发消费能力匹配
- 冷热分层存储,避免单机磁盘打满
- 堆积时先限流再扩容,避免链路雪崩
监控与告警
- 监控延迟、堆积量、失败率、重试次数
- 关键 Topic 要有阈值告警与自动降级方案
版本与兼容
- 协议与消息体字段保持向后兼容
- 消费端升级要支持多版本并行
