这是 RocketMQ 系列的收官篇。前五篇走完了「一条消息的生命周期」,也讲了各种特殊消息,但你会发现:防丢失的手段散在存储篇,防重复的手段散在发送篇和消费篇,堆积、顺序更是各讲各的。本篇把它们全部收拢------围绕第一篇埋下的 MQ 四大问题(消息丢失、重复消费、消息堆积、顺序问题),逐个讲透「问题在哪发生、防线是什么、为什么能防住」,最后输出一张可以直接对照的可靠性全景矩阵。
一、可靠性全景:四个问题,一个目标
先退回到第一篇问的那个问题:我们为什么需要 MQ? 三大功能(异步、解耦、削峰填谷)背后,其实藏着一个交换条件------用 MQ 换来的每一分好处,都要付出可靠性的代价。消息一旦交给 Broker,它就脱离了你本地事务的控制,接下来可能发生四类事故:
- 消息丢失:消息发出去了,但谁也没消费到。
- 重复消费:一条消息被消费了两次,业务结果错了。
- 消息堆积:消息源源不断进来,消费跟不上,越积越多。
- 顺序问题:消息消费的先后和业务要求的先后不一致。
前五篇里,这些问题的防线其实已经零零散散出现过:存储篇的「同步刷盘 + 主从复制」防丢失,发送篇的「Key + 消息表」防重复,消费篇的「幂等」兜底,特殊消息篇的「哈希路由」保顺序。本篇要做的事,就是把这些散点连成一张网------每个问题对应哪些防线,防线之间如何配合,缺了哪一环会出什么事。
先看这张全景图,后面逐章展开:

二、消息丢失:全链路三段防线
一条消息从生产到消费,跨越了三段路程:生产者 → Broker → 消费者。消息丢失可能发生在任意一段,所以防丢失必须三段都守住,任何一段失守消息就没了。
1. 生产端:同步发送的 ack 语义
第一段是「生产者 → Broker」。消息什么时候算「发出去成功」?这取决于你用的发送方式(第二篇讲过三种):
- 同步发送 :Broker 落盘成功才返回 ack,生产者拿到 ack 才认为发送成功。这是唯一能确认「消息真的到了」的方式。
- 异步发送:Broker 收到就回调成功,不保证落盘。万一后续落盘失败,消息就无声无息丢了,生产者毫不知情。
- 单向发送:发出去就不管了,连成功与否都不知道,只适合日志、监控这类丢了无所谓的消息。
所以生产端的防线就一句话:对不能丢的业务消息,必须用同步发送,把「Broker 确认落盘」作为发送成功的唯一标准。
2. Broker 端:刷盘 × 复制的两道闸门
第二段是「Broker 内部」。消息到了 Broker,可能丢在两个环节:
- 落盘环节:消息先写 PageCache,异步刷盘时只有「机器宕机/内核崩溃」才丢,窗口最多 10ms;同步刷盘则完全落盘才返回。丢不丢,取决于刷盘策略(第三篇)。
- 宕机环节:单机落盘成功了,机器本身挂了怎么办?靠主从复制。异步复制下,Master 挂了而 Slave 还没追上,会丢「尚未同步」的那一小段;同步复制则 Slave 确认写完才算成功。
把这两个维度组合起来,就是第三篇的可靠性矩阵:
| 组合 | 可靠性 | 适用场景 |
|---|---|---|
| 同步刷盘 + 同步复制 | 最高,任何单点故障都不丢 | 金融、强一致核心链路 |
| 异步刷盘 + 同步复制 | 高,Slave 有完整副本 | 一般核心业务 |
| 异步刷盘 + 异步复制 | 默认,容忍两个小窗口 | 大部分日常业务 |
注意一个容易被忽略的点:这两道闸门防的是不同的事。刷盘防「机器还活着但内存数据没了」,复制防「整台机器没了」。只守一道,另一类故障照样丢消息。
3. 消费端:先消费、后提交 offset
第三段是「Broker → 消费者」。这一段其实设计上就偏向「宁可重复、不可丢失」------消费端先消费消息,之后才把 offset 提交给 Broker。提交失败、或者消费者宕机,Broker 只当作你还没消费完,下次从旧位点重新拉。消息最多被消费两遍,但绝不会因为「提交了 offset 而消息没消费」而漏掉。
这就是整条链路防丢失的完整图景:

cketmq-loss-defense.jpg&pos_id=img-3BNCKZf1-1789919481809)
小结:防丢失没有银弹,它是三段防线各司其职的结果------生产端靠同步发送确认,Broker 端靠刷盘 + 复制双闸门,消费端靠「先消费后提交」的语义兜底。
三、重复消费:两个根源与幂等兜底
防丢失的代价就是防重复变难。上一节说消费端「宁可重复、不可丢失」------那重复到底从哪来?两个根源。
1. 根源一:生产端重试
第二篇讲过,生产者发送失败会重试(默认 2 次)。问题是:Broker 可能已经落盘成功,只是 ack 在网络中丢了。生产者以为失败、重发一遍,Broker 就存了两条一模一样的消息。
防法:给消息带一个全局唯一的 Key(如订单号),Broker 端或消费端用「消息表」去重------生产前先插一条消息记录,重复的 Key 插入失败就说明已发过,直接放弃。这是第二篇的伏笔,现在回收。
2. 根源二:消费端重复
消费端重复的机制第四篇讲透了,这里只收拢两条:
- 提交 offset 失败:消费完了,offset 没提交上,下次又从旧位点拉一遍。
- Rebalance 队列交接:队列换了主人,新消费者从「上次提交位点」开始拉,旧主人没消费完的部分又拉一遍。
这里要澄清一个容易误解的点:现代客户端提交 offset 前会检查该队列是否还属于自己 ,不属于就放弃提交------这个机制防的是「offset 覆盖」,即防止旧主人把位点回写成更早的值、让已提交的消费进度倒退。它并不能防止重复消费------根源二的重读依然会发生(新主人从上次提交位点开始拉,旧主人没消费完的消息照样被拉一遍)。它只是「不让位点回退」的底线,重复消费本身还得靠下面的幂等兜底。
3. 最终防线:幂等消费
前两个根源只能「降低概率」,无法根除------因为「至少一次投递」是 MQ 的语义底座。所以最终防线是业务侧幂等:让同一条消息消费两次,业务结果只生效一次。第四篇给了四种落地方法:
- 唯一键插入:业务表加唯一键,第二次插入冲突直接返回成功。
- 状态 CAS :
UPDATE ... WHERE status = '待支付',第二次更新影响行数为 0,天然幂等。 - Redis 记录:消费过的 Key 写进 Redis,下次先查再消费。
- 去重表:Key 做唯一主键,插入失败说明已消费过。
一句话总结全篇的可靠性格局:丢失靠链路防线,重复靠幂等兜底------前者是 MQ 的责任,后者是业务的责任。
四、消息堆积:最常见、也最容易被轻视的事故
如果说前面两章 讲的是「怎么不丢、不重复」(第二章防丢失、第三章防重复),这一节讲的是「怎么不被压垮」。堆积是生产环境里最常发生的事故------它不声不响,等业务方发现时,往往已经延迟了几个小时。这一节重点展开:什么是堆积、多少才算堆积、堆积有什么危害、怎么处理。
1. 堆积的本质:生产速率 > 消费速率
先想一个问题:消息到达 Broker 后,如果消费者一直不拉、或者拉得不够快,会发生什么?消息在队列里只进不出,越积越多------这就是堆积。
所以堆积的本质是一个速率不等式:
生产速率 > 消费速率 → 消息必然堆积(Diff 持续增长)
生产速率 = 消费速率 → 动态平衡
生产速率 < 消费速率 → 堆积自然消化
这个视角非常关键:堆积不是「有没有」的问题,而是「持续多久」的问题 。瞬时积压 1 万条不可怕,只要消费速率能追上,几分钟就消化完了;可怕的是生产一直快于消费,积压会像滚雪球一样只增不减。所以判断堆积,永远要看趋势,而不是看某一个瞬间的数字。
2. 怎么量化堆积:三个数字
RocketMQ 里堆积是有精确度量的,Broker 为每个队列维护两个位点:
- maxOffset:该队列当前写到了哪个位点(最新消息的位置)。
- consumerOffset:消费者当前消费到了哪个位点(已提交的进度)。
两者的差就是滞后量(Diff / lag),也就是这个队列「积压了多少条消息」:
Diff = maxOffset - consumerOffset
总积压量 = 所有队列的 Diff 之和。在哪里看?rocketmq-dashboard 控制台 的「消费进度」页,或者命令行 mqadmin consumerProgress -g <消费组>------它会列出每个 Topic、每个队列的 maxOffset、consumerOffset 和 Diff,以及消费 TPS。这是排查堆积的第一手数据。
3. 多少才算堆积:三个视角
这是很多人最纠结的问题------「Diff 到 1000 算不算堆积?10000 呢?」。正确答案是:RocketMQ 没有官方统一的「堆积阈值」,因为堆积与否根本不是一个绝对数字,必须结合消费速率和业务容忍度来判断。这里有三个视角,层层递进:
视角一:动态视角(最本质)------消费跟不跟得上。
当前消费速率能否吃掉积压?如果消费 TPS 持续小于生产 TPS,Diff 单调增长 → 堆积在恶化;
如果消费 TPS 大于生产 TPS,Diff 会自己收敛 → 暂时积压,不算事故。
这才是「堆积」的定义本身:积压并不可怕,可怕的是一直消不掉。
视角二:业务视角------预计消化时间能不能接受。
把积压量换算成「消化时间」:
预计消化时间 ≈ 总 Diff ÷ 消费 TPS
如果预计几十分钟能消化完、业务等得起,那这不算堆积;如果预计要几个小时甚至几天,业务早就超时了------这就是堆积。比如订单超时关闭场景,延迟 30 分钟消息就失去意义,那「预计消化时间 > 30 分钟」就是堆积。
视角三:静态视角(监控用)------经验阈值触发告警。
日常监控需要一个「拍脑袋但可操作」的数字。业界经验:单队列 Diff 超过 1000 条,或总量超过 10000 条,就触发告警让人去看一眼------注意这个数字没有官方依据,只是「值得看一眼」的信号,真正要不要处理,还是要回到视角一和视角二判断。
一个常见误区:消费者重启时,consumerOffset 可能短暂显示为 0(还没拉取更新),Diff 会假性暴涨------所以看 Diff 一定要等消费者跑起来、处于稳态之后,并且要结合趋势判断,不能看重启瞬间的假数据。
4. 堆积的危害:不只是「慢」
堆积的危害远超「消费延迟」本身,有三层:
- 第一层:业务时效性失效。消息到得太晚,业务结果已经没意义了------订单超时该关的没关,缓存该刷的没刷。
- 第二层:触发文件清理,直接丢消息(最严重) 。第三篇讲过,CommitLog 按 72 小时和磁盘水位 75% 双条件清理。堆积导致 CommitLog 膨胀、磁盘写满,Broker 会强制删除最旧的 CommitLog 文件------里面的消息哪怕还没被消费,也一样被删掉。这是堆积最可怕的后果:不是延迟,是实实在在丢消息。
- 第三层:重试风暴,雪上加霜。消费者处理不过来,消息消费超时 → 返回 RECONSUME_LATER → 进重试队列,延迟等级逐级拉长(第四篇)。重试队列又占用新的存储和消费能力,越堆越多,形成恶性循环。
5. 处理方案:四种手段,各有前提
处理堆积,思路永远是同一个公式的两端------要么提高消费速率(右边),要么降低生产速率(左边)。下面四种方案逐个分析,重点讲清「怎么生效、前提是什么、代价是什么」。
方案一:扩容消费者实例(横向扩消费)
原理:集群模式下,消费者组把队列切分给每个消费者并行消费(第四篇 Rebalance)。消费并行度 = min(消费者数, 队列数)------N 个消费者同时拉 N 组队列,消费能力近似提升 N 倍。
前提(关键):队列数要够分 。消费者数量不能超过队列数量,否则多出来的消费者分不到队列、只能空闲。所以扩容消费者前,先确认 队列数 > 当前消费者数,有余量才有效。
代价:新增消费者实例会触发全组 Rebalance 重新分配队列,可能引发一次重复消费(第四篇的根源二),而且涉及机器/容器成本。所以扩消费者要挑低峰期做,尽量等积压消化到接近稳态再动。
方案二:增加消费者的消费线程(纵向扩消费)
原理:单个消费者内部是一个线程池 并发消费(第四篇),线程池大小默认 consumeThreadMin = 20、consumeThreadMax = 64。把线程数调大,单实例并行消费的消息数就变多,消费 TPS 直接提升。
前提一:先排除「消费逻辑本身慢」 ------如果是慢 SQL、锁竞争、下游超时导致单条处理太慢,加线程只是让更多线程卡在慢逻辑上,无效(怎么排查见下一节)。
前提二:线程数不是越大越好,受两个硬约束------
- CPU 核数:线程过多,操作系统频繁上下文切换,CPU 全耗在调度上,消费反而变慢。
- 业务下游能力:消费逻辑往往要查数据库、调下游接口------线程数超过数据库连接池上限或下游 QPS 上限,请求全部排队等资源,TPS 上不去,白白占着线程。
所以正确的调法是:看 CPU、数据库连接池、下游 QPS 这些资源能承担多大并发,把线程数调到「资源可承担」的上限并留出余量------而不是盲目从 20 加到 200。
代价:几乎为零(改配置重启即可),是四种方案里最轻量、最优先试的手段------前提是单消费者还有余力(CPU/连接池没打满)。
方案三:扩容队列(提高并行度上限)
原理:队列数决定了「可并行消费者数」的上限------消费者数量 ≤ 队列数量。所以队列越多,未来能挂的消费者就越多,并行度天花板越高。
前提(关键,和方案一相反):队列数在 Topic 创建时就该规划好,运行期基本不能动。原因有两点:
- RocketMQ 4.x 不支持运行中平滑扩容队列数------强行改
readQueueNums/writeQueueNums,已投递的消息路由全部变化,历史消息可能消费不到。 - 顺序消息更是一票否决:第五篇讲过,队列数一变,哈希全部重算,顺序链直接断掉。
所以扩容队列不是救火手段,而是预防手段:在 Topic 设计阶段,根据「预期峰值消费并行度」把队列数规划够(一般建议队列数远大于预期消费者数,留出扩容余量)。等真堆积了才想起加队列,已经晚了。
方案四:生产方减少投递(削峰限流)
原理:回到速率不等式------既然堆积是「生产 > 消费」,除了提高消费速率,也可以降低生产速率,让不等式翻转过来。
手段:
- 生产者限流:发送端用信号量或令牌桶限流,控制投递速率,峰值时让多余的请求在生产者侧排队等待,而不是一股脑砸进 Broker。
- 业务降级:非核心消息(统计、日志、通知类)在高峰时降级------直接丢弃、降采样、或者延迟到低谷再发。核心消息保速率,非核心让路。
- 错峰投递:业务侧把大批量任务拆散到不同时间段发,避免整点/秒杀式集中流量。
代价:限流会损失吞吐、降级会损失数据完整性------所以生产方限流是「业务能接受损失」时的最后手段,一般和方案一、二配合使用(高峰限流入,低谷加速消费出,这才是真正的削峰填谷)。
补充手段(别忽视):
- 批量消费 :
consumeMessageBatchMaxSize(默认 1)调大,一次拉取处理多条,减少逐条处理的框架开销------对单条处理很轻的业务收益明显。 - 优化消费逻辑本身 :这是堆积排查里最优先的一步(见下一节决策流程)------很多时候消费慢不是资源不够,而是业务逻辑里有个慢 SQL、锁竞争、下游接口超时 5 秒。先看消费端日志定位它,比盲目加线程有效得多。
- 重置位点跳过过期消息 :如果积压的消息已经过期、对业务没有意义(比如几小时前的实时统计),直接用
resetOffsetByTime把消费位点跳到最新,跳过这堆垃圾,而不是硬着头皮消费完。
6. 排查决策流程
实际遇到堆积,按这个流程走,别慌------每一步都在把问题范围缩小:
第 1 步:先确认「是不是堆积」。 看控制台的 Diff、消费 TPS、生产 TPS。Diff 在涨才是堆积(在恶化);Diff 在降说明消费在追、积压会自己消化,暂时观察即可。记住排除消费者重启瞬间的假性暴涨。
第 2 步:定位根源在「生产」还是「消费」。 这是两条完全不同的排查路径:
- 生产 TPS 异常暴涨 → 生产侧问题,直接走方案四(限流/降级/错峰),同时查是不是业务方有 bug 在狂发消息。
- 消费速率未达预期 → 消费侧问题,进第 3 步。
第 3 步:消费侧,先排查「消费逻辑本身」,再看资源。 消费速率上不去,最常见的根因不是资源不够,而是单条消息处理得太慢------慢 SQL、锁竞争(数据库锁/分布式锁抢占困难)、下游接口超时。先看消费端日志、慢查询、下游耗时,把这个查清楚:
- 如果是逻辑慢 → 先优化消费逻辑(加索引、批量、异步化、合理的超时设置),这是治本;此时加线程只是让更多线程卡在慢逻辑上,白费资源。
- 逻辑没问题 → 才是资源/规模问题,按下面的顺序递进:
- CPU、数据库连接池还有盈余 → 方案二:加消费线程,把并发拉到「资源可承担」的上限。
- 线程加满还不够、队列数有富余 → 方案一:扩消费者实例。
- 队列数已到上限、是长期问题 → 方案三:这是设计问题,重建 Topic 提前扩队列(临时先用方案二/四压住,挑低峰期操作)。
完整决策流程见下图:

五、顺序问题:从乱序到分区有序
第四个问题------顺序------前面已经讲得很透了,这里只做收口。
乱序的两端根源(第五篇):生产端轮询投递,同一业务的消息散到不同队列;消费端多线程并发,线程调度和投递顺序无关。两端叠加,即使消费者串行也是乱的。
但绝大多数业务根本不需要全局顺序 ------同一笔订单的「下单 → 支付 → 发货」必须有序,不同订单之间谁先谁后无所谓。所以 RocketMQ 提供的是分区有序,不是全局有序:
- 生产端:哈希路由(同一业务 Key 进同一队列)+ 同步发送 + 分布式锁,保证同一队列内按序落盘。
- 消费端:按队列加锁、单线程消费,保证同一队列内按序消费。
两个「同一队列内有序」叠加,就保证了同一业务 Key 的消息严格有序------这就是第五篇整套方案的逻辑。
已经乱序了怎么办:如果当初没做顺序消息、消息已经乱序,补救手段是业务侧的------消息里带业务时间戳,消费时校验先后、过期消息丢弃;或者用状态机,只有合法的状态流转才执行(比如「已支付」之后来的「下单」消息直接忽略)。这些手段治标,根治还是得在源头用顺序消息。
一句话收口:顺序是有代价的(重试阻塞、不能扩缩容),先想清楚业务到底要不要,要了就用分区有序,不要就别付这个代价。
六、可靠性全景图与 Checklist
最后,把前五篇的所有防线收进一张表。这张表就是整个系列的浓缩------每个问题、发生在哪、防线是什么、原理在第几篇。
| 问题 | 发生环节 | 防线/手段 | 原理出处 |
|---|---|---|---|
| 消息丢失 | 生产 → Broker | 同步发送,以落盘 ack 为成功标准 | 第二篇 |
| 消息丢失 | Broker 落盘 | 同步/异步刷盘(异步丢窗口 ≤10ms) | 第三篇 |
| 消息丢失 | Broker 宕机 | 主从复制(同步复制零丢失) | 第三篇 |
| 消息丢失 | 消费阶段 | 先消费后提交 offset,宁可重复不丢失 | 第四篇 |
| 重复消费 | 生产重试 | 消息 Key + 去重表 | 第二篇 |
| 重复消费 | offset 提交失败 / Rebalance | 提交前检查队列归属 + 业务幂等 | 第四篇 |
| 重复消费 | 业务消费 | 幂等四件套:唯一键 / CAS / Redis / 去重表 | 第四篇 |
| 消息堆积 | 速率失衡 | 扩消费者(≤队列数)/ 加线程(≤CPU/连接池)/ 扩队列(提前规划)/ 生产限流 | 本篇 |
| 消息堆积 | 磁盘膨胀 | 提前规划队列数、监控 Diff 趋势告警 | 本篇 |
| 顺序问题 | 生产端 | 哈希路由 + 同步发送 + 分布式锁 | 第五篇 |
| 顺序问题 | 消费端 | 按队列加锁 + 单线程消费 | 第五篇 |
对应的 checklist,可以在部署和排查时逐项打勾:
部署前自查:
- 不能丢的消息是否都用了同步发送?
- 核心 Topic 是否配了同步复制?是否需要同步刷盘?
- 队列数是否按峰值并行度规划,留了扩容余量?
- 顺序消息场景是否确认队列数不会再变?
- 消费端是否做了幂等(唯一键或去重表)?
- 是否配置了 Diff 监控告警(单队列 1000 条 / 总量 10000 条)?
线上排查时:
- 先看 Diff 趋势(涨还是降),排除重启假数据?
- 消费 TPS 追得上生产 TPS 吗?
- 单条消费慢的根因是什么(慢 SQL / 锁竞争 / 下游超时)?
- 堆积是否已触发 CommitLog 清理(丢消息风险)?
- 处理手段按「先优化逻辑 → 加线程 → 扩消费者 → 生产限流」的顺序,挑低峰期操作?
小结
到这里,六篇系列就闭环了。回顾整个旅程:第一篇 用「一条消息的生命周期」串起架构全景;第二篇 讲清发送的三种方式与重试/故障转移;第三篇 揭开存储的秘密------CommitLog 顺序写、PageCache、零拷贝、刷盘与复制;第四篇 走完消费全流程------Rebalance、长轮询、offset、重试死信、幂等;第五篇 横向展开特殊消息------顺序、延迟、事务、过滤;本篇把散落的防线收拢成一张可靠性全景。
回到第一篇的那个问题------「MQ 带来异步、解耦、削峰填谷,代价是什么?」现在有了完整答案:代价是你要为丢失、重复、堆积、顺序四个问题各准备一条防线。好在 RocketMQ 已经把这些防线都搭好了,你要做的,是理解每条防线为什么存在、在什么场景生效,然后正确地使用它们。这就是这六篇想让你带走的东西。