连载中 8/20

消息顺序:全局有序与分区内有序

2026-08-02 · 2833 阅读 · 0 评论 · 0 赞

乱序是怎么发生的

老王遇到过一次诡异的事:订单 10086 明明是「已发货」,积分系统却按「已下单」算了一笔新手积分。追日志发现,两条消息一前一后发出,却被分到了两个队列,两个消费者并行处理,快的跑赢了慢的。并行是吞吐的来源,也是乱序的来源——鱼与熊掌的关系。

乱序的三个惯犯:

  • 多队列并行:同一业务的消息落在不同队列,消费完成时间不可控;
  • 消费重试:第一条处理失败进重试,第二条处理成功先落了地;
  • Rebalance 位点回退:队列被分给新消费者后从旧位点重放,后面的消息可能先于前面的消息被再次处理。

全局有序:听起来完美,用起来劝退

让所有消息严格按发送顺序被消费,方法只有一个:整个 Topic 一个队列,队列里单线程消费。顺序是有了,吞吐也归一了——每秒几千条的口子,大促直接堵死。全局有序在业务系统里基本是空中楼阁,除非万不得已,别碰

分区内有序:99% 场景的正解

回看业务需求:订单 10086 的「下单→支付→发货」必须有序,但订单 10086 和订单 10087 之间谁先谁后,用户根本不关心。业务要的不是全局有序,而是「同一业务 key 内有序」——这就是分区内有序。

实现只要两步。第一步,生产端按 key 路由,让同一个订单的消息永远进同一条队列:

sendResult = producer.send(msg, (mqs, message, arg) -> {
    Long orderId = (Long) arg;                      // 业务 key
    int index = Math.abs(orderId.hashCode()) % mqs.size();
    return mqs.get(index);                          // 同单永远同队
}, order.getId());

Kafka 天生如此:按 key 哈希进同一 partition,partition 内天然有序,不用写选择器。第二步,消费端对这条队列串行消费:RocketMQ 用 MessageListenerOrderly,队列加锁加消费锁,保证单线程处理;Kafka 单个 partition 只分配给组内一个消费者,天然串行。

顺序的代价:队头阻塞

串行是拿吞吐换的。更要命的是队头阻塞:队列里第一条消息处理失败,顺序消费不能跳过它(跳过就乱序了),只能原地重试——RocketMQ 的顺序消费会把这条队列挂起本地重试,重试期间整个队列停摆,后面的消息全堵着。一条毒消息卡一条队列,这就是顺序消费最大的雷。所以顺序消费的重试策略、告警和人工介入通道,要提前设计。

兜底方案:让乱序失去杀伤力

工程上还有一个思路:与其消灭乱序,不如让业务对乱序免疫。还记得第 7 篇的状态机吗?带前置状态校验的更新天然免疫乱序:

-- 「已下单」消息迟到时,状态已是 38,UPDATE 影响 0 行,直接丢弃
UPDATE orders SET status = 30 WHERE id = 10086 AND status = 20;

再进一步,消息里带上版本号或时间戳,消费端只接受比自己新的——这和数据库乐观锁是同一个思想。能改业务逻辑免疫乱序的,优先改逻辑;改不了的,再上分区内有序。

要不要顺序的判断表

场景需要顺序吗方案
同一订单的状态流转通知需要订单号 key 路由 + 分区内串行,或状态机免疫
不同订单之间不需要并行消费,吞吐优先
数据库 binlog 同步同一行需要表名+主键做 key 路由(Canal 的标准做法)
埋点日志上报基本不需要乱序无害,全力吞吐

小结

乱序是并行的影子,治法分三层:能免疫的用状态机和版本号,必须有序的按 key 路由加分区内串行,全局有序能不用就不用。用顺序消费前先问一句:我承受得起队头阻塞吗?

可靠性的四大金刚——不丢、不重、有序——到这一篇集齐了。下一站开始进阶特性:延迟消息。订单 30 分钟不支付自动取消,除了定时扫表还有没有优雅解法?

503

10 年全栈工程师 · 503咖啡馆主理人

#消息队列#顺序消息#分区内有序#队头阻塞#状态机

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞