RocketMQ 顺序消息与多线程分片:生产、存储、消费的一致性设计

2026年6月13日星期六·6653 字·33 分钟·
-浏览量
纯文本
学习文档RocketMQ消息队列顺序消费分布式一致性高并发
AI 摘要- DeepSeek

从生产端分片键路由、Broker 队列 FIFO 与消费端三把锁讲起,给出提升顺序消费吞吐的四个方案、各自乱序风险与幂等对账兜底设计。

封面图
RocketMQ 顺序消息与多线程分片:生产、存储、消费的一致性设计 - 封面图
提示

顺序消费以队列为单位:同一条队列同一时刻只被一个消费线程串行处理,因此多线程分片的并行度上限就是队列总数。本文拆解生产端分片键路由、Broker 队列 FIFO、消费端三把锁的完整链路,回答三个问题:怎么在保证顺序的前提下提高并行度、顺序的代价是什么、一致性为什么必须落在业务幂等上。

核心摘要#

  • 核心思路:顺序保证 = 生产端按分片键(ShardingKey)把同一业务实体的消息路由到同一队列 + Broker 队列内 FIFO + 消费端对队列加锁后串行消费,三层缺一不可。多线程分片发生在两个维度:队列之间天然并行,队列内部只能靠业务侧二次拆分。
  • 一致性结论:RocketMQ 只提供 at-least-once 投递,不提供 exactly-once,官方立场是「允许重复 + 业务幂等」。幂等去重必须与业务操作放同一事务,否则去重记录本身就是新的不一致源。
  • 阅读重点:消费端三把锁的分工、并行度 = 队列数的模型、位点提交时机、乱序窗口的三个来源。
  • 明确不覆盖:全局顺序(单队列)方案、RocketMQ 事务消息的源码实现、RocketMQ 5.x Proxy 部署架构。

引子:状态机消费乱序#

订单服务通过 RocketMQ 广播订单状态变更(创建 → 已支付 → 已发货 → 已完成),下游履约服务并发消费。某天客服反馈:订单 A 显示「已完成」,履约系统里还是「已支付」。查日志,「已发货」那条消息的数据库更新慢了 300 ms,被「已完成」反超。

保序需要三个假设同时成立,而默认配置下三个都不成立:生产端假设「发送顺序 = 业务顺序」,但多线程业务代码组装顺序不可控;存储假设「消息按业务顺序落盘」,但发送顺序本身已乱;消费端假设「到达顺序 = 处理顺序」,但默认并发消费是线程池抢任务。本文按这三个环节依次给出解法。

先定义顺序的边界#

范围保证实现代价
全局顺序整个 Topic 严格 FIFO只允许 1 个队列、1 个生产者实例、串行发送、1 个消费者实例。吞吐等于单队列上限,绝大多数业务不可接受
分区顺序同一分片键内 FIFO,键之间无序多队列并行,是标准方案
无序不保证默认并发消费

分区顺序的正确性前提是分片键选对:键必须是「需要有序的那个实体」的标识,订单状态流转用订单号。用操作流水号做键是常见错误:每次操作的流水号都不同,等于没有分片,顺序保证完全落空。键的哈希还要足够离散,避免集中到少量队列形成热点(官方 5.x 对消息组的建议同样适用于 4.x 的分片键)。

全链路机制#

消费端

Broker 存储

生产端

发送

其他 key 的消息

分片键提取

orderId = 10086

MessageQueueSelector

hash(10086) % 8 = 6

MessageQueue 0

MessageQueue 6

10086 的消息全部落在这里

ConsumeQueue 内 FIFO

MessageQueue 7

Rebalance:队列分配给实例

Broker 队列锁 + 本地锁

同队列同时只有一个线程

ConsumeRequest 循环

逐条串行调用业务逻辑

消费端

Broker 存储

生产端

发送

其他 key 的消息

分片键提取

orderId = 10086

MessageQueueSelector

hash(10086) % 8 = 6

MessageQueue 0

MessageQueue 6

10086 的消息全部落在这里

ConsumeQueue 内 FIFO

MessageQueue 7

Rebalance:队列分配给实例

Broker 队列锁 + 本地锁

同队列同时只有一个线程

ConsumeRequest 循环

逐条串行调用业务逻辑

生产端:分片键路由与串行发送#

MessageQueueSelector#

生产端通过 MessageQueueSelector 按业务键选队列,同一键的所有消息命中同一队列,这是顺序的起点:

SendResult send(MultiOrder order) {
Message msg = new Message("ORDER_STATUS_TOPIC", "TagA",
order.getOrderId(), JSON.toJSONBytes(order));
return producer.send(msg, (mqs, message, arg) -> {
long orderId = (long) arg;
// & Integer.MAX_VALUE 而非 Math.abs:防止 Integer.MIN_VALUE 取 abs 仍为负
int index = (int) ((Long.hashCode(orderId) & Integer.MAX_VALUE) % mqs.size());
return mqs.get(index);
}, order.getOrderId());
}

两个细节:Math.abs(Integer.MIN_VALUE) 仍返回负数,取模得到 -1 直接数组越界,用 & Integer.MAX_VALUE 抹掉符号位是标准写法;取模基数是队列总数,扩缩容时同一个键会命中新队列,这是扩容乱序的根源(见「方案一」)。Spring 环境下 RocketMQTemplate.syncSendOrderly(destination, msg, hashKey) 内置了同样语义的哈希路由,能接受固定哈希时可直接用。

串行发送:单一生产者 + 同键串行#

MessageQueueSelector 只保证「落点相同」,不保证「落点内先后」。官方把生产顺序性拆成两个条件:同一分片键的消息由同一个生产者实例发出;同键消息串行发送,多线程并行发送时到达顺序取决于线程调度,无法判定。工程上要么在实体维度串行化(同一订单的处理被分布式锁串行化时,发送天然有序,推荐先检查业务是否已有这个串行点),要么把消息交给按分片键分组的单线程发送器。

发送失败:重试仍发同一队列,但要业务兜底#

顺序消息的发送路径(sendSelectImpl)里 selector 只调用一次,重试复用同一个队列。这不会因换队列破坏顺序,代价是队列所在 Broker 宕机时重试只会反复失败。因此:同步发送,拿到 SendResult 才算成功(sendMsgTimeout 默认 3000 ms);重试耗尽后落待发送表,由定时任务补偿重发。

补偿重发必须保留原始分片键(仍走 selector)与原始幂等键。发送超时不等于发送失败,原消息可能已投递成功;恢复任务若重新生成新消息 ID,幂等键就对不上,重复消息会绕过去重直接执行。保留原幂等键(订单号 + 目标状态),重复投递才会被幂等表挡住。

本地事务与发送的原子性:事务消息#

「先提交本地事务,再发消息」在两步之间崩溃时,下游永远不知道这笔变更。事务消息(半消息机制)把两步绑成原子操作:

业务库Broker生产者业务库Broker生产者alt[本地事务成功][本地事务失败]第 3、4 步之间实例宕机时1. 发送半消息(对消费者不可见)2. 半消息写入成功3. 执行本地事务(订单状态落库)4a. 提交(commit),消息对消费者可见4b. 回滚(rollback),半消息删除5. 定时回查(默认间隔 60 s,最多 15 次)6. 返回本地事务的最终状态
业务库Broker生产者业务库Broker生产者alt[本地事务成功][本地事务失败]第 3、4 步之间实例宕机时1. 发送半消息(对消费者不可见)2. 半消息写入成功3. 执行本地事务(订单状态落库)4a. 提交(commit),消息对消费者可见4b. 回滚(rollback),半消息删除5. 定时回查(默认间隔 60 s,最多 15 次)6. 返回本地事务的最终状态

回查依赖业务实现 checkLocalTransaction,用事务 ID 反查本地事务状态,查不到返回 UNKNOW 等下一轮,不要乱返回 ROLLBACK。回查有上限(transactionCheckMax 默认 15 次,超限后半消息被丢弃),所以回查查询必须可靠。事务消息只保证「本地事务 + 发消息」的原子性,下游消费成功靠消费端幂等。

Broker 端:队列内 FIFO 与可配置的可靠性#

存储端行为简单:单个队列内严格按写入顺序排列(ConsumeQueue 按序索引 CommitLog),消费按存储顺序投递。不同分片键的消息可以混在同一队列且不保证连续:队列内部是「各自有序的消息流交织」,消费端按序读、按键各自保序,两者不冲突。

Broker 不给两样东西:跨队列无序,同键消息落进两个队列后不做任何合并,扩容、路由错误都会造成跨队列乱序;不感知业务语义,物理顺序与状态机推进是两回事,这层语义必须业务自己维护(消息体带状态版本号)。

「不丢」则是配置出来的,默认配置有丢失窗口:flushDiskType 默认 ASYNC_FLUSH,消息写入 PageCache 即返回成功,断电丢失未刷盘的秒级数据,改 SYNC_FLUSH 落盘完成才返回 ACK,吞吐相应下降;brokerRole 默认 ASYNC_MASTER 主从异步复制,主节点宕机丢未同步的消息,改 SYNC_MASTER 同步双写(5.x 可用 DLedger / Controller 模式做多副本)。对状态流消息,丢一条就是下游的永久版本缺口,这类 Topic 至少配置同步复制,残余风险靠对账兜底。

消费端:三把锁与并行度模型#

顺序消费的三把锁#

DefaultMQPushConsumer 默认注册 MessageListenerConcurrently,拉到的消息丢进消费线程池(默认 20 线程),同队列的两条消息被两个线程同时拿走,谁先执行完取决于线程调度(引子里 300 ms 反超的来源)。把监听器换成 MessageListenerOrderly 后,ConsumeMessageOrderlyService(基于 4.9.x 源码)用三层锁把队列变成串行单元:

锁位置解决的问题
Broker 端队列分布式锁消费实例定时(默认 20 s)向 Broker 发 LockBatchMQ 请求,锁默认 60 s 过期集群模式下保证一个队列同一时刻只分配给消费组内一个实例
MessageQueue 本地锁ConsumeRequest.run() 中对队列对象 synchronized消费线程池有 20 个线程,可能多个线程被派到同一队列;此锁保证实例内同队列只有一个线程在消费
ProcessQueue.consumeLock调用业务监听器前 ReentrantLock 加锁防止消费过程中 Rebalance 移除队列导致在游离对象上消费

锁是刻意拆细的:队列本地锁覆盖「判断 + 取消息 + 消费」整个循环,消费锁只覆盖业务监听器执行期间,Rebalance 移除队列只需等当前这条消息消费完。串行的核心在 ConsumeRequest.run() 的 while 循环:锁内反复「取一批消息 → 加消费锁 → 调用业务监听器 → 按返回值决定继续或延迟重试」,单线程逐条推进;连续消费超过 60 s 会主动让出线程,避免饿死线程池。

由此得出关键推论:顺序消费的正确性依赖锁与路由信息的持续一致,乱序窗口有三个来源:锁过期(网络抖动、GC 停顿超过 60 s)、锁切换(Rebalance)、Broker 主从切换后的路由刷新。这是官方文档「极端情况下可能乱序」的出处,也是消费幂等必须存在的第二个理由(第一个是重复投递)。

并行度模型:队列数就是天花板#

整个消费组的有效并行消费单元数 ≤ Topic 队列总数
单个队列吞吐 ≈ 1000 ms / 单条业务处理耗时
消费组总吞吐 ≈ 队列数 × 单队列吞吐
例:单条消息处理 40 ms → 单队列约 25 TPS
8 个队列 → 约 200 TPS
32 个队列 → 约 800 TPS

两条纪律:调大 consumeThreadMax 不提升单队列吞吐,线程数只用于让多个队列同时被消费,超过实例分到的队列数就是空转;队列数少于实例数时多余实例分不到队列(同组内队列不共享),扩实例前核对队列数 ≥ 实例数。队列数是创建 Topic 时的容量决策,改动会引发分片重分布。

提升吞吐:四个方案与各自的代价#

单队列 25 TPS 的估算意味着日订单 200 万(峰值 500 TPS)需要 32 个队列。怎么扩、或者不扩队列怎么提速:

方案一:扩队列,处理扩容窗口的跨队列乱序#

扩队列是唯一能线性提升吞吐的手段,但取模基数变化会让同键消息跨队列:旧队列还压着「已支付」,「已发货」已进新队列,两个队列并行消费,顺序被打穿。严格做法是停写、等旧队列存量排空再切流;业务上往往退而求其次:接受窗口内乱序,靠状态版本号挡住过期状态(见「状态机校验」),扩容完成后窗口自动关闭。

方案二:消费端内存队列二次分发#

同键串行不必绑死在 MQ 队列上。监听器拿到一批消息后按键二次分发到固定槽位的内存队列,每个槽位单线程:同键仍在同一槽位内串行,不同键并行。把 consumeMessageBatchMaxSize 调大(如 16),监听器同步等待整批完成后返回 SUCCESS,单队列吞吐从 25 TPS 升到约 25 × 16(受槽位数与键离散度限制),队列数不动。

两个纪律:必须同步等待整批完成再返回 SUCCESS:Push 模式下返回即提交位点,异步投递后宕机,未落库的消息真丢,只能靠对账找回;慢键是队列下限,批内一条 5 s 的消息拖住整批,要单独优化或拆分。

方案三:把 IO 拆出顺序链路#

顺序链路里真正需要串行的往往只有「按版本落库」一步。消费端只做轻量校验 + 事件按序写入事件表(单条 2~5 ms),重活(外部调用、算账、通知)由异步 worker 按版本推进,不依赖 MQ 顺序。单队列吞吐升到 200~500 TPS,热点键(大商家占满队列)也得到缓解。代价是多一张事件表和一个 worker,重活的失败重试要自己设计。

方案四:升级 5.x 消息组 + Pop 消费#

生产端给消息设置 MessageGroup(语义等价于分片键),Topic 声明为 FIFO 类型;消费端 Pop 模式不要求实例绑定队列,同一消息组的消息处理期间锁定在一个消费者上。收益是乱序窗口比队列锁切换更小,消费者可水平扩、不受队列数约束;代价是一次性迁移成本,且 SimpleConsumer 一次拉多条时顺序性要业务自己保证。

选型对比#

方案单队列吞吐提升乱序风险改动面适用场景
扩队列随队列数线性扩容窗口跨队列乱序只改 Topic 配置键离散、允许窗口期
内存队列二次分发约 × 批内不同键数无新增消费端改代码队列数改不动、键离散
拆 IO 出顺序链路约 ×10无新增;重活重试自担架构改造业务处理慢、有热点键
5.x 消息组 + Pop与队列数同阶窗口比 4.x 小升级迁移计划升级 5.x 的新系统

四个方案不互斥,常见组合:先拆 IO(收益最大且不动分片),再扩队列,消费端叠内存队列兜峰值。

消费失败与重试:顺序的代价#

维度顺序消费(Orderly)并发消费(Concurrently)
重试位置客户端本地:消息放回 ProcessQueue 重新消费发回 Broker 重试主题 %RETRY%+消费组
重试间隔挂起当前队列,suspendCurrentQueueTimeMillis 默认 1000 ms延迟级别递增(10 s 起,最长 2 h)
默认最大重试次数maxReconsumeTimes 未设置时为 Integer.MAX_VALUE(无限重试)16 次,超过进入死信队列 %DLQ%+消费组
对后续消息的影响阻塞:同队列后续消息全部等待不阻塞

顺序消费必须阻塞:第 N 条没成功,第 N+1 条不能动。两个决策必须显式做:设置 maxReconsumeTimes(如 8),默认的无限重试会让一条毒消息永久卡死整个队列,超限进死信后队列才能推进,死信的人工/补偿闭环不建,乱序就从短暂变成永久;suspendCurrentQueueTimeMillis 默认 1000 ms,瞬时故障下合理,持续故障下队列吞吐跌到 1 TPS,靠告警及时发现。

失败要先分类,再决定重不重试#

重试的价值取决于失败是否瞬时。瞬时失败(网络超时、数据库抖动)抛给 MQ 走本地重试;确定性失败(业务校验不通过、非法状态转换、消息体损坏)重试结果必然还是失败,直接落死信表并告警后返回 SUCCESS 跳过(让这类消息走完重试只是白停摆队列)。跳过即乱序,这个决定依赖两个配套:死信表有「人工或定时任务介入」的闭环;后续消息的合法性由状态机校验把守,一条消息进死信后其依赖消息会被级联拦下。这是预期行为:宁可级联进死信,不可错误执行。

并发消费的重试还会带来另一种重复:批量消费 2、3 两条,3 失败重试期间 2 可能被再次处理。无论哪种模式,重试和重复投递是常态,幂等不是优化项。

一致性设计:at-least-once 之上的业务幂等#

幂等键:业务唯一键,不是 msgId#

RocketMQ 的投递语义是 at-least-once:失败或超时就重投。exactly-once 需要 Broker 端去重,实现代价极高,官方不做。所有一致性设计收敛为一个问题:同一条消息被消费 1 次和 N 次,业务结果是否相同。

msgId 不能做幂等键:生产端重试、Broker 端写入都可能让不同 msgId 指向同一业务消息。幂等键取自业务语义:状态变更用 orderId + 目标状态,事件用事件源生成的 eventId。它同时覆盖两类重复:同一条消息被投两次,以及发送端补偿任务重发产生的「不同消息、同一操作」。

去重表与业务操作同事务#

去重记录与业务操作必须在同一事务:先记去重后执行业务,中间失败则这条消息永远不会再被处理;先执行业务后记去重,中间宕机则重投后重复执行。

@Transactional
public ConsumeOrderlyStatus handle(OrderStatusMsg m) {
try {
// bizKey = orderId + targetState,唯一键冲突即重复投递
consumeRecordMapper.insert(m.getBizKey(), CONSUMED);
orderMapper.updateState(m.getOrderId(), m.getTargetState(), m.getVersion());
return ConsumeOrderlyStatus.SUCCESS;
} catch (DuplicateKeyException e) {
return ConsumeOrderlyStatus.SUCCESS; // 幂等:重复消息视为成功
}
}

三个坑:不要引入「消费中」中间态,业务执行期间重复消息到达会把它误判,处理慢的消息反复重试后进死信,要么加超时判定要么干脆用上图的原子写;去重表必须跟着业务键分库路由,与业务操作同库同事务,全局去重库破坏同事务前提;重试粒度是整条消息,业务 A 已执行、业务 B 失败重投后 A 会被再次执行,子操作(尤其不可事务化的外部调用)要么并入同一事务,要么各自幂等。

状态机校验:乱序发生时的止损#

幂等挡重复,挡不住乱序:位点重放、Rebalance 窗口、扩容迁移产生的乱序消息对幂等表是没消费过的新消息。第二道防线是消息携带目标状态与版本号,用带前置条件的原子更新拒绝非法转换。不要「先查状态、内存判断、再更新」三步走,查询与更新之间有并发窗口,先查后改挡不住并发重复:

UPDATE order_state
SET state = 3, version = version + 1
WHERE order_id = #{orderId} AND state = 2; -- 仅当前状态是前置状态时才生效

影响行数为 0 即乱序、重复或上游状态已推进,按确定性失败落死信。这道防线把前面所有机制失效的后果收敛为「该消息与其后续依赖消息进死信」,错误状态永远写不进业务库。

版本缺口缓冲:让乱序自愈#

拒绝式的代价是瞬态乱序也要人工捞死信。缓冲式换一种思路:发现前置版本缺失(版本号不连续)时,消息落暂存表后返回 SUCCESS 放行队列;定时补偿任务扫描暂存表,前置版本补齐后重新按序处理。乱序被限制在单个键内,同队列其他单号不受影响。

维度拒绝式(状态机校验 + 死信)缓冲式(缺口暂存 + 补偿重放)
瞬态乱序(扩容、Rebalance 窗口)进死信,需人工捞回前置版本补齐后自动重放
永久缺口(消息真丢了)快速暴露,人工兜底暂存表静默膨胀,需超时转死信
实现复杂度低:一条 CAS UPDATE高:暂存表、版本检查、补偿扫描、处理中状态防并发

默认拒绝式,缓冲式留给已知存在瞬态乱序窗口的场景(如扩容迁移期),并必须配暂存超时与暂存量告警两个上限,否则消息真丢时问题会被暂存表藏住。

位点提交与对账#

位点由客户端周期性提交 Broker,返回 SUCCESS 后消息移出 ProcessQueue、位点推进:时序上「处理完成」先于「位点提交」,所以返回 SUCCESS 前必须确保业务已持久化(方案二同步等待的原因)。位点按队列连续推进,宕机恢复从位点重放,已落库部分靠幂等表挡住,整条链路成立的前提仍是幂等写入与业务操作同事务。

以上机制把不一致概率压到很低但不是零(锁过期窗口、跳过的毒消息、回查超限丢弃的半消息),定时对账收尾:比对源与目的的不一致记录,以源为准修复(修复动作带幂等键),并监控不一致数的趋势,突变通常意味着上游某个机制失效了。

可观测性:顺序消费特有的监控盲区#

顺序消费的典型故障(毒消息卡队列、锁丢失、热点键堆积)只影响单个队列:32 个队列里 1 个停摆,组级总积压只上升约 3%,组级告警不会响。监控必须下钻到队列维度,做三件事:队列级积压,分钟级通过 DefaultMQAdminExt.examineConsumeStats 拿到按队列拆分的 offsetTable,逐队列算 brokerOffset - consumerOffset,超阈值单独告警;端到端延迟,消费完成时间减 msg.getStoreTimestamp() 看 P99,处理环节排队时它先于积压恶化;重试水位,msg.getReconsumeTimes() 接近 maxReconsumeTimes 即告警,等进死信再发现,队列阻塞时间已白付。

监控回答「坏了没有」,防线是否真的生效要靠故障注入验证:测试环境注入网络抖动、kill -9 消费者实例触发 Rebalance、发送端制造重复投递,统计乱序率与重复率。机制链上任何一环的回归都只表现为极端场景的乱序,功能测试测不出来。

实战演示:订单状态变更的消费端#

生产端:TransactionMQProducer + 事务监听器(上文时序图),发送走上文 selector 代码,失败落待发送表补偿。消费端是全链路纪律的浓缩:

public class OrderStatusConsumer {
private static final int BUCKET_COUNT = 64;
// 固定槽位,每个槽位单线程串行执行
private final List<ExecutorService> buckets = new ArrayList<>(BUCKET_COUNT);
public OrderStatusConsumer() throws MQClientException {
for (int i = 0; i < BUCKET_COUNT; i++) {
buckets.add(Executors.newSingleThreadExecutor());
}
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_status_consumer_group");
consumer.setNamesrvAddr("mq-namesrv:9876");
consumer.subscribe("ORDER_STATUS_TOPIC", "*");
consumer.setConsumeThreadMin(20);
consumer.setConsumeThreadMax(20);
consumer.setConsumeMessageBatchMaxSize(16); // 批内不同 key 并行
consumer.setMaxReconsumeTimes(8); // 毒消息有限重试,防队列永久卡死
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
List<Future<?>> futures = new ArrayList<>(msgs.size());
for (MessageExt raw : msgs) {
OrderStatusMsg m = JSON.parseObject(raw.getBody(), OrderStatusMsg.class);
futures.add(buckets.get(bucketOf(m.getOrderId())).submit(() -> handle(m)));
}
try {
for (Future<?> f : futures) {
f.get(30, TimeUnit.SECONDS); // 同步等待整批:SUCCESS 即提交位点
}
return ConsumeOrderlyStatus.SUCCESS;
} catch (Exception e) {
// 瞬时失败:整批放回,队列挂起后重试
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
});
consumer.start();
}
private int bucketOf(long orderId) {
return (int) ((Long.hashCode(orderId) & Integer.MAX_VALUE) % BUCKET_COUNT);
}
@Transactional
protected void handle(OrderStatusMsg m) {
try {
consumeRecordMapper.insert(m.getBizKey(), CONSUMED); // 去重与业务同事务
orderMapper.updateState(m.getOrderId(), m.getTargetState(), m.getVersion());
} catch (DuplicateKeyException e) {
// 幂等:重复投递视为成功
}
}
}

生产化前还需要补:确定性失败的死信分流(见「失败要先分类」)、死信告警与人工闭环、对账任务。

踩坑点#

  1. Math.abs 取模为负:Integer.MIN_VALUE 取 abs 仍为负,selector 返回下标 -1,发送时数组越界。用 hash & Integer.MAX_VALUE。
  2. 顺序消费默认无限重试:默认 maxReconsumeTimes = Integer.MAX_VALUE,一条毒消息永久卡死队列。必须显式设置并建死信闭环。
  3. 异步分发后立刻返回 SUCCESS:位点已提交,宕机时未落库的消息永久丢失。要么同步等待,要么接受丢失靠对账,不能默认。
  4. 把消息轨迹当幂等:消息轨迹只是记录,没有任何去重效果,两者是完全不同的东西。
  5. 重放要求消息体向后兼容:位点重放和死信重处理意味着几个月前的消息被当前代码消费,消息体要带 schema 版本号。

总结#

顺序消费的并行度模型是整个方案的骨架:队列内 FIFO + 三把锁把并行单元锁死在队列粒度,「多线程分片」的第一性问题是队列数的规划,而不是消费线程数的调参。吞吐扩展按优先级:先拆 IO 缩短顺序链路,再扩队列(处理扩容窗口),消费端内存队列二次分发兜峰值,5.x 消息组与 Pop 消费留给有升级窗口的系统。一致性上,传输层只承诺 at-least-once:不丢靠事务消息、同步复制与「持久化后再返回 SUCCESS」,不乱靠分片路由与状态机校验,不重只能靠业务幂等;幂等表、状态机、对账三件套缺一不可。评估任何「保证顺序」的方案,先看它怎么处理失败:重试、宕机、扩缩容三个场景的答案,比正常路径的描述更能说明方案是否成立。

参考资料#

提示

如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区交流。

RocketMQ 顺序消息与多线程分片:生产、存储、消费的一致性设计
https://tblog.mmzhiku.xyz/posts/projects-rocketmq-ordered-message-sharding/
作者
MmzMing
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0
OpenSpec+Superpowers 实现流水线开发
AI 辅助前端开发 Prompt 模板集:目录结构、组件复用与代码审查

评论区

暂无歌词

暂未播放

0:00
0:00
目录
logo

隐私政策

更新日期: 2026 年 7 月 15 日
生效日期: 2026 年 7 月 15 日

适用范围#

本政策适用于 MmzMing 的博客(以下简称“本站”)。本站是个人博客,用于发布和分享内容;不提供账户注册、支付、定位或广告投放服务。访问本站、发表文章评论或在留言板留言前,请阅读本政策。

信息收集与使用#

本站只在提供内容、评论和留言功能,以及维护站点安全所需的范围内处理信息。

  • 访问与统计信息:访问页面时,统计服务可能处理访问时间、页面地址、来源页、浏览器和设备相关信息,用于了解内容访问情况、排查故障和改进站点。
  • 评论信息:使用文章评论功能时,Waline 可能处理您主动提交的昵称、邮箱、站点链接和评论内容;还可能处理 IP 地址等必要信息,用于防止垃圾评论、滥用和维护服务安全。评论内容、昵称和站点链接(如填写)可能公开展示在文章下方;邮箱不会公开展示。
  • 留言信息:留言板使用 Waline /guestbook/ 频道。Waline 可能处理您主动提交的昵称、可选邮箱、站点链接、留言内容和图片,以及浏览器、操作系统、IP 地址等必要的反滥用信息。默认情况下,留言图片以内嵌数据随留言提交;如站点维护者配置了远程图片上传接口,图片会先发送至该接口并在留言中保存返回的图片地址。留言内容、昵称、图片和站点链接(如填写)可能公开展示;邮箱和 IP 地址不会在留言板公开展示。

请不要在评论或留言中提交身份证件、银行卡、住址、密码或其他不必要的敏感个人信息。

第三方服务#

为实现本站功能,以下第三方会在各自服务范围内处理相关数据:

  • Umami:用于匿名化的网站访问统计和出站链接点击统计,帮助我了解本站的使用情况。
  • Waline:用于文章评论、留言板及访问量统计。服务会按照其自身规则处理您在评论或留言时提交的信息及必要的反滥用信息。
  • Cloudflare:可为本站提供静态资源分发和域名基础设施。
  • unpkg:用于加载 Waline 的前端脚本、样式和表情资源;请求这些资源时,您的浏览器会与该服务建立连接。

第三方服务可能有独立的隐私政策和数据保存规则。请在使用相关功能前查阅其规则;本站无法控制其独立的数据处理活动。

本站主要使用浏览器本地存储(Local Storage 或 Session Storage)保存使用偏好,例如主题颜色、明暗模式、文章列表视图和音乐播放设置。留言板会在本地保存匿名资料、未发送草稿和登录状态,以便恢复输入与会话;管理员登录状态仅保存在当前会话。

本站不主动设置用于广告定向的第一方 Cookie。评论、统计或资源服务可能按照其自身规则使用 Cookie 或类似技术。您可以通过浏览器设置查看、删除或限制 Cookie 和本地存储;清除后,部分偏好或互动状态可能会恢复为默认值,评论功能也可能受到影响。

信息公开、保存与安全#

评论和留言属于公开互动内容,提交后可能被搜索引擎收录、被他人引用或在缓存中短暂保留。请谨慎决定发布内容。除非您提出删除请求、内容违反规则或法律法规另有要求,公开内容会持续保留以维持讨论上下文。

本站会采取合理措施保护数据安全,包括使用 HTTPS、输入校验、内容转义和访问频率限制。但互联网传输和第三方服务均无法保证绝对安全,请理解并自行承担公开发布信息的相应风险。

你的权利#

你可以通过 784774835@qq.com 联系我,申请查询、更正或删除由本站直接保存的评论、留言或相关公开内容。为保护他人权益,请在请求中提供足以定位内容的信息,并说明你与该内容的关系;必要时可能需要进行合理核验。

对于由 Waline、Umami、Cloudflare 或 unpkg 独立处理的数据,你也可以直接向对应服务提供方行使相关权利。删除公开评论或留言后,第三方缓存、搜索引擎索引或他人转载的副本可能无法立即同步删除。

未成年人条款#

未满 14 周岁的未成年人应在监护人同意和指导下使用本站的评论、留言等互动功能。监护人如发现未成年人未经同意提交了个人信息,可通过上述联系方式与我联系,我会在合理范围内协助处理。

政策更新与联系#

我可能因本站功能或适用规则变化更新本政策,更新后的版本将在本站公布并标明日期。继续使用相关功能即表示你已阅读并理解更新后的政策。

如对本政策或数据处理有疑问,请联系 784774835@qq.com。

用户协议

更新日期: 2026 年 5 月 19 日
生效日期: 2026 年 5 月 19 日

适用范围#

本协议适用于你访问 MmzMing 的博客,以及使用文章评论、留言板等互动功能的行为。继续浏览本站或提交评论、留言,即表示你已阅读、理解并同意遵守本协议及本站的隐私政策。

评论及留言规则#

请在交流中保持友善、理性和尊重。你不得利用本站发布、传播或实施以下行为:

  • 发布任何违反中华人民共和国法律法规的内容。
  • 发布任何侵犯他人合法权益的内容,包括但不限于隐私、名誉、肖像、著作权、商标权和其他知识产权。
  • 恶意攻击、辱骂、骚扰、威胁、歧视其他用户或任何第三方。
  • 发布垃圾广告、恶意推广、刷屏、灌水,或与讨论主题明显无关的重复内容。
  • 利用本站进行网络诈骗、钓鱼、传播恶意软件,或发布可能危害网络和信息安全的内容。
  • 绕越或试图绕越本站的审核、限流、封禁等管理措施。
  • 冒充他人、伪造身份,或收集、公开他人的个人信息。

内容与访问管理#

你应对自己发布的评论和留言负责,并保证拥有发布该等内容所需的合法权利。论坛管理员有权在不另行通知的情况下删除违规内容、限制或封禁违规账号,或限制其继续使用本站互动功能。

如发现涉嫌违法犯罪、严重侵权或危及本站安全的内容,本站可保留相关记录,并在法律法规要求或必要时向有关部门提供协助。对管理措施有疑问时,可通过文末联系方式说明情况;本站会结合实际情况处理,但不承诺恢复已删除内容或访问权限。

知识产权与内容授权#

本站原创文章、页面设计和其他受保护内容的权利归作者或权利人所有。未经授权,请勿复制、转载、镜像或用于商业用途;法律法规允许的合理使用除外。

你发布评论或留言时,授予本站为展示、存储、备份、审核、删除和维护互动功能所必需的非独占、免费的使用许可。该许可不改变你对原创内容依法享有的权利。

免责声明#

本站内容仅用于个人记录、学习交流和一般信息参考,不构成任何专业意见、承诺或担保。你应结合自身情况独立判断,并对据此采取的行动负责。

评论、留言和外部链接中的内容由其发布者或运营者负责,不代表本站立场。本站会在合理范围内处理明显违规内容,但不保证所有内容均及时发现,也不对第三方网站的可用性、内容、安全性或隐私实践承担责任。

因网络故障、不可抗力、第三方服务异常、维护升级或超出合理控制范围的原因导致本站暂时无法访问、内容延迟或数据丢失的,本站会尽力恢复,但不承担由此产生的间接损失。

未成年人条款#

未满 14 周岁的未成年人应在监护人同意和指导下使用评论、留言等互动功能。监护人应协助未成年人理解本协议,并对其使用行为进行必要的引导。

其他条款#

我可以根据本站功能、管理需要或法律法规变化更新本协议,更新后的版本将在本站公布并标明日期。继续使用本站即视为接受更新后的协议。

本协议的订立、执行和解释适用中华人民共和国法律。因本协议或使用本站产生争议时,双方应先友好协商;协商不成的,依法向有管辖权的人民法院解决。

如对本协议或内容管理有疑问,请联系 784774835@qq.com。

复制成功,转载请标注本文地址