Kafka 顺序消息

某团队在实现订单流程时,需要保证消息的处理顺序:创建订单 → 扣库存 → 扣余额。

但由于 Kafka 消息被分散到了多个分区,消费者使用多线程并发处理,导致顺序错乱。

最终解决方案:

  1. 同一订单的所有消息路由到同一 Partition
  2. 消费者使用单线程处理同一 Partition 的消息

【架构权衡】 Kafka 的顺序消息保证是有条件的:单 Partition 内有序,多 Partition 间无序。理解这个约束,才能正确设计消息顺序保证方案。


一、核心问题 🔴

1.1 Kafka 顺序保证

Kafka 的顺序保证:

单 Partition 内:
├─ 消息按写入顺序保存
├─ 消费者按顺序消费
└─ 强顺序保证

多 Partition 间:
├─ 不同 Partition 之间无顺序保证
└─ 取决于分区策略

全局顺序:
└─ 使用单 Partition(牺牲性能)

1.2 保证顺序的方案

方案一:使用 key 路由到同一 Partition

Producer:
kafkaTemplate.send("order-topic", orderId, message);
// orderId 作为 key,同一订单的所有消息到同一 Partition

配置:
spring:
  kafka:
    producer:
      properties:
        partitioner.class: org.apache.kafka.clients.producer.internals.DefaultPartitioner
// 默认按 key hash 分区

方案二:使用自定义分区策略

public class OrderPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        // 按 orderId 的前缀分区,保证同一订单在同一 Partition
        String orderId = (String) key;
        return Math.abs(orderId.hashCode()) % 6;
    }
}

1.3 消费端顺序处理

// 方案1:单 Consumer + 单 Partition
@KafkaListener(
    topics = "order-topic",
    groupId = "order-consumer",
    concurrency = "1"  // 单线程消费
)
public void consume(String message) {
    // 单线程处理,保证顺序
    processMessage(message);
}

// 方案2:分区感知消费
@KafkaListener(
    topics = "order-topic",
    containerFactory = "kafkaListenerContainerFactory"
)
public void consume(ConsumerRecord<String, String> record) {
    // 按分区处理
    processByPartition(record.partition(), record.value());
}

// 方案3:分区有序 + 并发处理(分阶段)
// 阶段1:同一 Partition 内按顺序处理
// 阶段2:不同阶段之间异步处理

二、生产避坑

2.1 消费者并发问题

// ❌ 问题:并发消费导致乱序
@KafkaListener(topics = "order-topic", concurrency = "3")
// 3 个线程并发消费,顺序可能错乱

// ✅ 解决1:按 key 分区 + 单线程消费
@KafkaListener(topics = "order-topic", concurrency = "1")

// ✅ 解决2:按 key 分区 + 按 key 路由消费线程
// 同一 key 的消息路由到同一 Partition
// 同一 Partition 只有一个 Consumer 线程处理

2.2 消息重试问题

// ❌ 问题:重试导致消息乱序
try {
    processMessage(message);
} catch (Exception e) {
    // 重试消息会插入到后面
    kafkaTemplate.send("order-topic", key, message);
}

// ✅ 解决:使用阻塞式重试,或不重试直接进入死信队列

三、落地 Checklist

  • 分区规划:根据顺序要求规划分区数
  • Key 设计:设计消息 key 保证同一业务 ID 到同一 Partition
  • 消费配置:配置合适的并发数
  • 顺序验证:测试消息顺序是否正确
  • 监控部署:监控分区 Lag、消息顺序