Kafka 如何实现顺序消费?
结论:Kafka 的顺序只有"分区内有序",所以实现顺序消费的本质是把"需要有序的消息"收敛到同一个分区,再保证这个分区从生产到消费全程串行。四个条件缺一不可:相同的 key、生产端不因重试乱序、一个分区只被一个消费者处理、消费端按分区串行执行业务。
一、顺序在哪一环被破坏
Key=order-1 ──分区器──> P0 ──追加写──> 日志有序 ✅
│
┌─────────────────┼──────────────────┐
│ ① 重试乱序 │ ② 多线程消费 │ ③ 位点提交
▼ ▼ ▼
batch2 先到、 同一分区 8 个线程 先提交后处理,
batch1 后到 → 乱序 并发处理 → 完成乱序 失败即丢(不是乱序但更严重)
生产端的乱序来自"重试":第一批发送失败重试时,第二批可能已经成功写入,于是后发的先到。Kafka 用幂等生产者(Producer Idempotence)+ 序列号解决,开启后 Broker 会拒绝乱序的批次。
二、四个条件的配置
| 环节 | 要求 | 配置/做法 |
|---|---|---|
| 分区选择 | 同一业务键进同一分区 | 发送时指定 key,且 不要 用自定义随机分区器 |
| 生产重试 | 重试不乱序 | enable.idempotence=true(此时 in-flight ≤ 5 安全) |
| 消费分配 | 一个分区只给一个消费者 | 消费者数 ≤ 分区数,多余消费者会空转 |
| 消费执行 | 分区内串行 | 单线程处理,或按 key 哈希到固定工作线程 |
| 位点提交 | 处理成功再提交 | enable.auto.commit=false + 手动提交 |
三、生产端写法
Properties p = new Properties();
p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "10.0.0.31:9092");
p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
p.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); // 幂等,重试不乱序
p.put(ProducerConfig.ACKS_CONFIG, "all");
p.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
KafkaProducer<String, String> producer = new KafkaProducer<>(p);
// key 用订单号:同一订单的所有状态变更都会进同一个分区
producer.send(new ProducerRecord<>("order-events", orderId, payload));
如果业务不能开幂等(老版本或特殊配置),唯一安全的做法是把 max.in.flight.requests.per.connection 设为 1,代价是吞吐下降。
四、消费端保序的两种写法
// 写法一:单线程处理(最稳,吞吐最低)
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(200));
for (ConsumerRecord<String, String> r : records) {
handle(r); // 串行
}
consumer.commitSync(); // 一批处理完再提交
}
// 写法二:按 key 分桶到固定线程,桶内串行、桶间并行
int buckets = 16;
ExecutorService[] workers = new ExecutorService[buckets];
for (int i = 0; i < buckets; i++) {
workers[i] = Executors.newSingleThreadExecutor();
}
for (ConsumerRecord<String, String> r : records) {
int idx = Math.floorMod(r.key().hashCode(), buckets); // 同 key 必进同一线程
workers[idx].submit(() -> handle(r));
}
写法二能提升吞吐,但提交位点是"整批"语义:某个桶还没处理完就提交,进程崩溃会丢;要精确,就得按桶维护已完成位点,取最小值提交,复杂度显著上升。
五、常见追问
- 增加分区后顺序还成立吗? 不成立。key 到分区的映射是
hash(key) % 分区数,分区数一变,同一个 key 可能落到新分区,而旧消息还在老分区。顺序敏感的业务要么预留分区,要么用"双写过渡 + 消费端按版本号排序"。 - 全局有序怎么做? 分区数设为 1,吞吐直接降到单分区上限,除非消息量极小,否则不可取。
- 消费端多线程一定破坏顺序吗? 只有"同一分区内并发处理"才破坏;按 key 分桶后,同一 key 仍然串行。
- 重平衡会导致顺序错乱吗? 不会改变已存储的顺序,但会重复投递,业务要能容忍重复;真正危险的是"处理到一半分区被收走",所以要在撤销分区前提交已完成位点。
小结
- Kafka 的顺序是分区级顺序,实现顺序消费 = 收敛分区 + 全程串行。
- 生产端用 key + 幂等生产者,消费端单线程或按 key 分桶。
- 位点提交时机决定"丢"还是"重复",顺序场景通常选"重复 + 幂等"。
- 分区扩容、随机分区器、多线程消费是三个最常见的破序来源。