连载中 8/20

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

2026-08-02 · 2838 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞