1. 为什么我决定自己写一个消息队列,而不是继续用 RabbitMQ
我先交代一下背景。当时我们团队维护的订单服务已经跑了两三年,流量高峰期每秒大概有三五千次写入,RabbitMQ 单机部署,集群方案用的是镜像队列。这套架构看起来挺稳妥,直到某次大促值班夜里,我盯着监控面板看到队列积压从几百条一路飙到几十万条,消费者 CPU 打满,磁盘 IO 报警,然后整个链路开始雪崩式超时。
那次事故排查了很久,根因其实不在消费者代码,而是镜像队列在节点间同步消息时带宽被占满,加上大量确认消息回传导致网络风暴。RabbitMQ 确实很成熟,功能丰富,但它的定位是通用消息中间件,不是为超高吞吐极致优化的。我当时就在想,如果只针对内部订单系统这一个使用场景,砍掉一堆用不上的功能,把每条消息的路径做到最短,是不是能做出一个更轻量、可控性更强的高性能消息队列?
于是就有了这个项目。我花了几周时间,从零实现了一个高性能消息队列,支撑了日常每秒两万条以上的消息吞吐,高峰压测到六万条每秒,端到端延迟中位数在 3 毫秒左右。整个项目代码量不大,核心逻辑可以放在一篇文章里讲清楚,很适合那些想深入理解消息队列底层原理、或者需要在特定场景下做定制化消息组件的人参考。
先声明,我做的不是另一个 RabbitMQ 或 Kafka 的替代品。它解决的是特定场景下的特定问题:单机部署、内存优先、允许少量消息丢失、需要一个极简的消息路由机制。如果你的需求是复杂路由、消息持久化、事务消息、延迟队列这些企业级功能,直接用现成的中间件更合适。但如果你想理解消息队列高性能的底层逻辑,或者想在嵌入式、边缘计算这类资源受限的环境中塞一个轻量消息组件,这篇文章应该能帮到你。
我会从整体设计思路讲起,然后拆解队列存储结构、高并发读写优化、消费与确认机制这几个核心模块,最后会分享我做压测时的数据和踩过的坑。全程以实际代码和实测数据说话。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 设计取舍:高性能消息队列的架构边界
动手写之前,我先把需求边界划清楚了。一个高性能消息队列,不能上来就追求大而全,否则性能一定做不上去。性能优化的第一步永远是砍需求,确定哪些功能不做,比确定哪些功能要做更重要。
2.1 先确定使用场景:我的消息队列服务什么
我的核心场景是订单系统内部的事件流转,比如订单创建后通知库存服务扣减库存、通知积分服务发放积分、通知物流服务创建运单。这类消息有几个特点:
- 消息量峰值高:大促时每秒几万条是常态。
- 消息体积小:一条订单事件大概一两百字节,最多不超过 4KB。
- 允许少量丢失:如果服务崩溃,最坏情况下丢失几秒内的消息是可以接受的,因为订单状态本身在业务数据库里,重启后可以做对账补发。
- 路由规则简单:基本就是按事件类型分发几个队列,不需要复杂的 Topic 层级和通配符匹配。
基于这些特点,我直接放弃了持久化到磁盘的完整方案,选择内存优先 + 异步批量落盘兜底的模式。这和 Kafka 的设计有本质区别:Kafka 为了保证大吞吐和持久性,使用磁盘顺序写,但代价是引入了页缓存、批量刷盘、副本同步等一整套机制,代码量和复杂性成倍增加。我只是单机内部使用,不需要跨机器的副本复制,所以把这些机制全部去掉。
2.2 高性能的核心原则:零拷贝路径、无锁设计、批量操作
定了场景之后,我给自己列了三条性能铁律,整个实现都是围绕它们展开的。
第一条是数据路径上不要有无关操作。每条消息从生产者调用到消费者拿到,中间只允许经过必要的几道工序:写入队列、通知消费者、读取消息。不做过多的序列化转换,不做持久化等待,不做复杂的路由匹配。
第二条是能无锁就无锁,必须加锁就把锁的粒度降到最小。高并发场景下锁竞争是最大的性能杀手之一。我用环形队列加原子变量替代了传统的链表加互斥锁,用内存屏障处理生产者与消费者之间的同步,把锁竞争彻底从消息路径上移除。
第三条是能批量就批量。单条消息的操作效率永远上不去,因为每一次系统调用、每一次内存拷贝都是有固定开销的。我在生产者端加了批量发送缓冲,在消费者端加了批量拉取,让固定开销被摊薄到一批消息上。
这三条原则听起来简单,实际执行过程中每一步都有不少细节。下面几节我会展开讲具体怎么落地。
2.3 架构图:生产者、队列核心、消费者三层模型
从模块划分上看,这个队列可以分成三大部分:
- 生产者 SDK:提供异步发送接口,内部包含批量缓冲区和发送线程。
- 队列核心引擎:负责消息的存储和索引,内部是一个定长环形缓冲区,配合读写指针和信号量实现。
- 消费者 SDK:提供拉取接口,支持批量获取消息,也支持简单的发布订阅模式。
这三层都在同一进程内存空间中(本项目中我用的是嵌入式模式,直接在一个 JVM 进程里跑,后续也拆了一个独立的 TCP 服务端版本,但那版主要为了验证网络端的性能损失,不是主角)。嵌入式模式下,生产者和消费者直接操作同一块内存区的数据结构,性能是最大化了的。
3. 核心存储结构:用定长数组环形队列替代链表队列
我先把消息队列最核心的存储结构说清楚。大多数初学者写消息队列,第一反应是定义一个 List 或者 LinkedList,然后往里面塞消息对象。这个做法在低并发、小数据量时没问题,但一旦数据量大起来就开始暴露问题:内存碎片、GC 压力、节点指针膨胀。
3.1 为什么不直接用链表队列
链表队列的每一条消息,至少需要一个节点对象,这个节点里除了消息数据外,还要存指向下一个节点的指针(可能是 next 引用,也可能是偏移量)。以 Java 为例,一个节点对象额外占用 16 到 24 字节的头信息,加上引用,意味着每条消息至少浪费三四十字节。如果消息本身就是一两百字节,这个浪费比例是很大的。
更大的问题在于内存分配频繁。每入队一条消息就要创建一个新节点,产生一次内存分配;出队之后节点失去引用,又要等 GC 回收。在高吞吐场景下,每秒几万次的内存分配和释放,会让 JVM 的 Young GC 变得极其频繁,表现为整条链路的轻微停顿和 CPU 飙高。
还有一个隐蔽的缺点:链表节点在内存中分布是不连续的,遍历时 CPU 缓存命中率很差。现代 CPU 每秒能处理几十亿次指令,但如果数据不在缓存里,从主内存加载数据的延迟是缓存命中时的几十倍。这个问题在链表结构下基本无解。
3.2 环形队列的读写指针设计
我的选择是使用一个定长数组实现的环形队列。核心代码简化如下:
java复制public class RingBuffer<T> {
private final Object[] buffer;
private final int capacity;
// 先不考虑扩充,队列满时直接丢弃新消息或阻塞,根据配置决定
private final AtomicLong writeIndex = new AtomicLong(0);
private final AtomicLong readIndex = new AtomicLong(0);
public RingBuffer(int capacity) {
// 这里确保容量是2的幂,方便取模
this.capacity = tableSizeFor(capacity);
this.buffer = new Object[this.capacity];
}
public boolean offer(T item) {
long wIndex = writeIndex.get();
long rIndex = readIndex.get();
if (wInde
