1. 通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。订单号相同的消息会被先后发送到同一个队列中,
  2. 在获取到路由信息以后,会根据算法来选择一个队列,同一个 OrderId 获取到的肯定是同一个队列。

两条描述的是”同 key 路由到同队列”这一有序性基石(原文第 1 条的”轮询”表述与第 2 条的哈希路由略有矛盾:保序场景必须用 key 哈希而非轮询)。本文把有序性问题拆成三段链路(生产-存储-消费)各自的乱序来源与对策,并给出”需要多有序”的分级模型——多数业务其实不需要全局序。

先分级:你要哪种有序

  • 全局有序:所有消息全序——单分区/单队列,牺牲并行度,极少需要
  • 分区有序(业务键有序):同一订单/用户的事件有序——90% 需求的真实答案(同 key 同队列)
  • 无序:只要最终都处理(日志采集类)——最大吞吐

分级决定成本:全局序=单点瓶颈;分区序=key 哈希+消费端单线程/保序处理;无序=全并行。先问业务”谁和谁之间需要序”,再选方案。

乱序来源一:生产端

  • 多线程并发发送同 key 消息:异步发送的完成顺序不确定 → 同 key 消息需单线程发送或发送前按 key 串行化(或接受 broker 侧按到达序=发送序不保证,需同步发送)
  • 重试导致乱序:消息 1 发送超时重试,消息 2 已成功 → 重发的 1 后到。对策:重试不改变路由(同 key 同分区)+ 消费端按业务序号/时间戳判序兜底;或开启”重试期间阻塞后续同 key 发送”(客户端串行化)

乱序来源二:存储/路由端

  • 路由策略:轮询/随机路由破坏同 key 同队列 → 必须 key 哈希路由(原文第 2 条;修正第 1 条的轮询表述)
  • 分区扩容:Kafka 扩分区后同 key 哈希结果改变 → 扩容瞬间同 key 消息跨新旧分区乱序。对策:扩容前消化积压+业务容忍窗口,或自定义路由含版本号
  • 主从切换:副本未同步完成切换可能丢/乱(少见,靠 ISR 配置缓解)

乱序来源三:消费端(最常被忽视)

  • 多线程并发消费同分区:拉取有序但处理完成无序 → 分区内单线程消费(Kafka 消费者线程=分区绑定)或内存队列二次分发(按 key 哈希到固定工作线程,保序且并行)
  • 重试乱序:消息 1 消费失败进重试队列,消息 2 成功 → 1 的重试晚于 2。对策:失败不跳出分区(分区内阻塞重试,牺牲吞吐)或消费端按版本号丢弃过期消息
  • rebalance 瞬间:分区移交期间两消费者短暂并发同分区 → 消费幂等+版本判序兜底

消费端保序的两种工程模式

模式一:分区内单线程(简单、吞吐=分区数):消费者线程与分区 1:1,处理逻辑同步执行;吞吐不够就加分区。

模式二:key 哈希二次分发(保序+并行):

// 消费线程拉取后按 key 分发到固定工作线程
int slot = Math.floorMod(key.hashCode(), workerCount);
workers[slot].submit(() -> process(msg)); // 同 key 永远同 worker,内部串行

兜底:业务序号判序

链路各环节都可能引入乱序时,最终防线在业务层:消息带单调业务序号/版本戳(如订单状态机版本号),消费端发现”序号小于已处理最大序号”即丢弃或挂起等待——状态机+版本号让乱序消息自动失效(见幂等与状态机篇的同源思想)。

方案组合速查

需求 组合
分区序(主流) key 哈希路由 + 分区内单线程/二次分发 + 重试不跨序
全局序 单分区 + 单消费者(接受吞吐上限)
高吞吐+容忍局部乱序 无序 + 业务版本号判序兜底
扩容/重试敏感场景 业务序号兜底必配

有序性的完整答案 = 分级(要哪种序)+ 三段链路各自设防(生产串行/路由哈希/消费保序)+ 业务版本号兜底;记住”轮询不保序、重试是乱序大头、消费端多线程是隐形杀手”三个高频错误点,有序性设计就不再靠运气。