如何保证消息的有序性
- 通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。订单号相同的消息会被先后发送到同一个队列中,
- 在获取到路由信息以后,会根据算法来选择一个队列,同一个 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 分发到固定工作线程 |
兜底:业务序号判序
链路各环节都可能引入乱序时,最终防线在业务层:消息带单调业务序号/版本戳(如订单状态机版本号),消费端发现”序号小于已处理最大序号”即丢弃或挂起等待——状态机+版本号让乱序消息自动失效(见幂等与状态机篇的同源思想)。
方案组合速查
| 需求 | 组合 |
|---|---|
| 分区序(主流) | key 哈希路由 + 分区内单线程/二次分发 + 重试不跨序 |
| 全局序 | 单分区 + 单消费者(接受吞吐上限) |
| 高吞吐+容忍局部乱序 | 无序 + 业务版本号判序兜底 |
| 扩容/重试敏感场景 | 业务序号兜底必配 |
有序性的完整答案 = 分级(要哪种序)+ 三段链路各自设防(生产串行/路由哈希/消费保序)+ 业务版本号兜底;记住”轮询不保序、重试是乱序大头、消费端多线程是隐形杀手”三个高频错误点,有序性设计就不再靠运气。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus










