关于rocketmq的readQueue和writeQueue
TL;DR RocketMQ的readQueue和writeQueue分离是为了实现无损扩缩容
RocketMQ 的 MessageQueue 有一个独特设计:将队列拆分为 readQueueNums(读队列数)和 writeQueueNums(写队列数)。这两个值在绝大多数情况下必须相等,一旦不等就会产生严重问题——那为什么要拆开?答案在于平滑扩缩容。
MessageQueue 是什么?
在 RocketMQ 中,Topic 是消息的逻辑分类,而 MessageQueue 是 Topic 的物理分片,类似于 Kafka 的 Partition。关键约束:Consumer 的数量不能超过 Queue 的数量——因为每个 Queue 同一时间只能被一个 Consumer 消费。
正常状态下:readQueue = writeQueue
正常情况下,readQueueNums == writeQueueNums,生产者向所有 Queue 写入消息,消费者从所有 Queue 读取消息,一切正常。
但如果两者不一致:
| 状态 | 后果 |
|---|---|
| writeQueue > readQueue | 部分 Queue 有写无读 → 消息永远不会被消费,积压 |
| readQueue > writeQueue | 部分 Consumer 空跑 → 空转浪费资源,永远收不到消息 |
既然一致时没问题、不一致会出事,那拆分开的意义是什么?
核心价值:平滑扩缩容
扩容流程(以 4 → 8 为例)
没有 read/write 分离时的困境:修改 Queue 数量是瞬间生效的,会出现以下问题:
- 如果先通知 Producer 新的 Queue 数,Producer 开始向新 Queue 写消息,但 Consumer 还没感知到新 Queue → 消息有写无读
- 如果先通知 Consumer,Consumer 开始监听新 Queue,但 Producer 还没开始写 → Consumer 空跑
有 read/write 分离后的两步式扩容:
| 步骤 | 操作 | 状态 | 效果 |
|---|---|---|---|
| Step 1 | readQueueNums: 4→8 |
write=4, read=8 | 新增 4 个 Consumer,开始监听新 Queue(此时新 Queue 还没有消息,空跑无害) |
| Step 2 | writeQueueNums: 4→8 |
write=8, read=8 | Producer 开始向新 Queue 写消息,Consumer 已经在等了,消息立刻被消费 |
整个过程没有消息丢失,也没有 Consumer 空转浪费。
如果把两个步骤反过来(先改 write 再改 read),效果也一样——因为 Consumer 扩容是最慢的(需要启动新实例),所以先完成扩容、再开始写消息是最合理的顺序。
缩容流程(以 8 → 4 为例)
| 步骤 | 操作 | 状态 | 效果 |
|---|---|---|---|
| Step 1 | writeQueueNums: 8→4 |
write=4, read=8 | Producer 停止向即将下线的 Queue 写消息(但 Consumer 还在读,现有消息不会丢失) |
| Step 2 | readQueueNums: 8→4 |
write=4, read=4 | 等待旧 Queue 中的消息全部消费完后,Consumer 缩小 |
对比:Kafka 的分区扩缩容
Kafka 的 Partition 数量只能增加,不能减少。扩容操作相对简单——增加 Partition 后,Consumer 通过 Rebalance 自动感知。但缩容在 Kafka 中是不支持的操作,一旦 Partition 数量设大了就只能接受。
RocketMQ 通过 readQueue/writeQueue 分离,支持了双向平滑扩缩容,这是在阿里大规模生产环境中打磨出来的设计智慧。
图解
1 | 正常状态:write=4, read=4 |
总结
readQueueNums 和 writeQueueNums 的分离是 RocketMQ 实现无损扩缩容的关键设计。日常运维中这两个值应该保持相等,只在执行扩缩容操作的短暂窗口内允许它们不同。这个设计看似违反了”读写应该一致”的直觉,但正是这种”允许短暂的、可控的不一致”,换来了扩缩容过程的完全平滑。




