
我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Kafka 顺序消息为什么乱了分区数变化与重试的 3 类原因一、先说清楚 Kafka 的顺序保证边界二、原因 1分区数变了三、原因 2生产端重试导致的乱序四、原因 3消费端并发导致的乱序五、还有一个坑异步落库六、带走物Kafka 顺序消息为什么乱了分区数变化与重试的 3 类原因一、先说清楚 Kafka 的顺序保证边界一句话Kafka 只保证分区内有序不保证 topic 全局有序。这不是缺陷是设计取舍——全局有序意味着只能有一个分区、一个消费者吞吐直接归零。所以顺序乱了这件事第一步要确认的是你依赖的顺序是不是跨分区的# 看 topic 有几个分区kafka-topics.sh --bootstrap-server localhost:9092--describe--topicorder-event消息按order_id分区你要求同一个订单的事件有序 →Kafka 能满足只要 key 用对你要求所有订单的事件全局按时间有序 →Kafka 不满足这是需求设计错了。如果是后者解法不是改 Kafka而是在消费端做业务侧排序比如按事件时间戳做窗口排序或者干脆换成单分区 topic牺牲吞吐。二、原因 1分区数变了这是最隐蔽的一类。Kafka 的默认分区器是hash(key) % partitionCount。注意分母是分区数。分区数 3 时 order_id100 → hash % 3 1 → 分区 1 分区数 4 时 order_id100 → hash % 4 0 → 分区 0同一个 key 在扩分区前后会落到不同的分区。这就意味着扩容瞬间正在途中的消息旧分区和新消息新分区会分成两拨如果消费者此时正在消费旧分区的一批新分区的那批可能被另一个消费者更早消费结果就是同一个 key 的两条消息后发的先被处理。结论扩容分区会短暂破坏按 key 的顺序性。如果你的业务严格依赖顺序扩分区要在低峰期 消费无积压时做并且接受短暂的顺序抖动。另外还有一类更简单的// 错误没指定 Key走轮询分区producer.Input()-sarama.ProducerMessage{Topic:order-event,Value:sarama.StringEncoder(data),// 没有 Key → 轮询同一个订单的消息散到不同分区}// 正确用业务主键做 Keyproducer.Input()-sarama.ProducerMessage{Topic:order-event,Key:sarama.StringEncoder(orderID),// 同一个订单永远进同一个分区Value:sarama.StringEncoder(data),}顺序乱了有相当一部分是这个原因——压根没设 Key消息从一开始就是散的。三、原因 2生产端重试导致的乱序这是第二大类而且和可靠性配置直接冲突。场景生产者发了两条消息 A、B同一个分区A 发送失败触发重试B 发送成功。结果B 先落盘A 后落盘顺序反了。它和这个参数有关max.in.flight.requests.per.connection5 # 默认 5这个参数允许最多 5 个请求同时在途、不用等前一个的确认。在途请求多了重试就可能插队。旧版本的解法是把它设成 1串行发送代价是吞吐大幅下降max.in.flight.requests.per.connection1 # 牺牲吞吐换顺序更好的解法是开启幂等生产者Kafka 0.11enable.idempotencetrue max.in.flight.requests.per.connection5 # 幂等模式下最多 5且不会乱序原理幂等生产者给每条消息带上序列号每个分区一个递增 seqbroker 侧按序列号重新排序并去重。所以即使 A 重试后晚到broker 也会把它放回正确位置同时把重复的那条丢掉。这是唯一一个既不牺牲吞吐、又能保证顺序 不重复的方案。顺序敏感的场景应该默认开启。config:sarama.NewConfig()config.Producer.Idempotenttrueconfig.Producer.RequiredAckssarama.WaitForAll config.Producer.Retry.Max3注意开启幂等后acks会被强制成allretries会被强制成大于 0——这是 Kafka 的要求不是 bug。四、原因 3消费端并发导致的乱序这一类和 Kafka 无关纯粹是消费者代码的问题分区内是严格有序的但你把它并发处理了。// 错误把同一个分区的消息丢进 goroutine 池formsg:rangeclaim.Messages(){gohandle(msg)// 并发 → 处理完成顺序不确定}即使 Kafka 保证了分区内顺序你这么一并发顺序就没了。正确的做法是分区内串行、分区间并行func(h*handler)ConsumeClaim(sess sarama.ConsumerGroupSession,claim sarama.ConsumerGroupClaim)error{// 这个方法对每个分区各跑一个 goroutine分区间是并行的formsg:rangeclaim.Messages(){// 而这里对单个分区是串行的顺序得到保证iferr:handle(msg);err!nil{returnerr}sess.MarkMessage(msg,)}returnnil}ConsumeClaim的语义就是每个分区一个 goroutine所以默认写法天然就是分区内串行 分区间并行。你要做的是别在里面再开 goroutine。如果单分区处理太慢正确的扩展方式是单分区慢 → 加分区数让更多分区并行→ 加消费者实例 而 不是 → 在分区内开 goroutine五、还有一个坑异步落库最后一类特别隐蔽消费端按顺序处理了但下游写入乱了。// 顺序处理但异步写库formsg:rangeclaim.Messages(){godb.Save(parse(msg))// 写库并发 → 到达顺序不定}如果业务依赖后一条覆盖前一条比如状态机创建 → 支付 → 完成并发写会让旧状态覆盖新状态。解法要么串行写要么在写入时带上版本号/时间戳做乐观锁UPDATEorderSETstatus?,versionversion1WHEREid?ANDversion?六、带走物排查顺序乱了按这四步走有没有设 Key没设就是轮询分区顺序从一开始就不存在。用业务主键做 Key。分区数最近变过吗变过的话扩容瞬间会有顺序抖动无法完全避免只能挑低峰期做。开了幂等生产者吗没开的话重试会乱序。开enable.idempotencetrue不要靠把max.in.flight设成 1 来换顺序。消费端有没有在分区内开 goroutine有的话去掉ConsumeClaim本身已经是分区内串行 分区间并行。一句话结论Kafka 的顺序保证是分区内 幂等生产者开启这两个前提同时成立才有效的而消费端要做的恰恰是什么都别做——不要自己再并发。