订阅更新

Kafka 如何保证消息顺序?

面试问 032 约 5 分钟读完 3 次阅读

本文目录
  1. 面试怎么回答
  2. 第一步:先定义要保证哪一种顺序
  3. 第二步:Producer 重试不能打乱批次
  4. 第三步:Consumer 拉取有序,不代表处理完成有序
  5. 第四步:下游还要识别旧消息和重复消息
  6. 一个典型场景

面试怎么回答

Kafka 只保证单个 Topic-Partition 内的日志顺序。我会先确定全局有序还是按 Key 局部有序;局部有序时,用订单或用户 ID 作为 Key,让相关消息进入同一 Partition。Producer 端核对幂等、重试和 max.in.flight,避免失败重排;Consumer 端保证同 Key 串行处理并正确提交 Offset;下游再用事件版本和幂等拒绝迟到旧消息,形成端到端业务顺序。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

很多人会回答:

“Kafka 一个 Partition 内有序,把消息发到同一个 Partition 就行。”

这句话只说明了 Broker 日志的顺序边界。

真实链路里还有 Producer 重试、Consumer 多线程、Rebalance 和下游异步写入。任何一段处理不当,都可能让业务最终看到乱序。

所以更完整的结论是:

Kafka 只保证 Topic-Partition 内的日志顺序;业务顺序还要同时约束分区键、Producer 发送、Consumer 处理和下游落库。


第一步:先定义要保证哪一种顺序

顺序不是一个笼统概念。

常见需求有两种:

  • 全局有序:整个 Topic 的所有消息只有一条严格顺序
  • 局部有序:同一个订单、用户或设备的消息保持顺序

Kafka 的顺序保证范围是单个 Partition。

如果要求全局有序,最直接的方式是只使用一个 Partition,但吞吐和消费并行能力也会被限制在单分区上。

生产里更多是局部有序:使用 order_id、user_id 这类稳定业务键作为 Message Key,让同一业务对象的消息进入同一个 Partition。

如果“订单创建”按 order_id 分区,“订单支付”却按 user_id 分区,即使两个 Topic 各自分区内有序,也无法保证同一订单的业务顺序。


第二步:Producer 重试不能打乱批次

假设同一连接上先后发送 Batch A 和 Batch B。A 发送失败需要重试,但 B 已经成功,如果没有正确的幂等和在途请求约束,B 可能先写入,造成重排。

生产上应该显式确认 Producer 的幂等配置没有被冲突参数关闭。

启用 Idempotent Producer 时,Kafka 会使用 Producer ID 和序列号识别重试批次;在允许的 max.in.flight.requests.per.connection 范围内,可以在保留并发能力的同时维持顺序。

如果关闭幂等,又允许多个未确认请求并发发送,发生失败重试时就存在乱序风险。把 max.in.flight.requests.per.connection 降为 1 可以缩小风险,但也会牺牲吞吐。

所以不能只记一个参数,要一起看:

enable.idempotence
acks
retries
max.in.flight.requests.per.connection

第三步:Consumer 拉取有序,不代表处理完成有序

Consumer 从一个 Partition 中按 Offset 顺序拉取消息,但应用代码可能把消息提交到线程池并行处理。

例如:

Offset 100:订单创建,处理耗时 500 ms
Offset 101:订单支付,处理耗时 50 ms

虽然拉取顺序是 100、101,但支付可能先落库。

如果业务要求同 Key 有序,可以采用:

  • 单 Partition 单线程顺序处理
  • 按业务 Key 分发到固定执行队列,同 Key 串行、不同 Key 并行
  • 等前序任务完成后再推进对应 Offset 的提交边界

还要注意 Rebalance。Partition 转移给新 Consumer 时,要先停止旧任务、完成或安全丢弃在途处理,再提交正确 Offset,避免新旧 Consumer 同时影响同一业务对象。


第四步:下游还要识别旧消息和重复消息

即使 Kafka 内部顺序正确,跨 Topic 合并、多数据源写入、网络重试和异步 Sink 仍可能改变最终到达顺序。

关键业务状态最好携带业务版本号、事件序号或业务发生时间,例如:

order_id = 1001
status = PAID
version = 3

下游更新时只接受高于当前版本的事件,并用业务主键和事件 ID 做幂等。这样旧消息晚到时,不会把 PAID 回写成 CREATED。

这不是替代 Kafka 的分区内有序,而是给端到端链路增加最后一道业务防线。

Kafka 端到端有序链路


一个典型场景

比如订单创建和支付都进入同一个 Partition,Kafka 中 Offset 顺序完全正确,但 Consumer 为了提高吞吐,把每条消息直接丢进公共线程池。

支付逻辑更快,先把状态更新为 PAID;创建逻辑后完成,又把状态覆盖回 CREATED。

排查只看 Kafka Offset,会误以为消息没有乱序。真正破坏顺序的是 Consumer 的完成顺序和下游无版本覆盖。

修复时可以按 order_id 分配固定执行队列,让同订单串行处理;下游同时增加版本比较与幂等,避免重试或迟到事件回写旧状态。

要保证的不是“消息被按顺序拉出来”,而是业务状态按正确顺序生效。


刷题进度保存在当前浏览器