Kafka 入门实战:用 Java 理解消息队列的 Producer/Consumer(下单到库存/通知)

Kafka 基本用法入门:Java 后端初学者如何理解消息队列、生产者和消费者

1. Kafka 是什么

Kafka 可以把它理解成"高性能的分布式日志系统 + 消息队列"。它把消息顺序地写入磁盘日志(可持久化),通过分区并行提升吞吐,用多副本保证故障时不丢数据。对于 Java 后端,Kafka 最常见的价值:

  • 解耦:下单服务不需要直接调用库存/通知服务,只要把消息写入 Kafka。
  • 异步与削峰:高峰期先写入队列,消费者慢慢处理,系统更稳。
  • 可回放:需要重算或补偿时,可从历史偏移量重新消费。
  • 可靠:acks、重试、幂等配合使用,尽量做到"至少一次"交付并降低重复影响。

2. 用下单场景理解 Producer、Consumer、Topic

设想一个电商最小闭环:用户提交订单,订单服务落库成功后发送一条"订单已创建"的事件;库存服务与通知服务分别订阅并消费。

  • Producer(生产者):订单服务。它把一条事件消息写到 Kafka。
  • Consumer(消费者):库存服务、通知服务。它们从 Kafka 拉取并处理消息。
  • Topic(主题):消息的"分类名"。例如 order.created。订单服务只负责往这个 Topic 写;谁订阅,它不关心。

事件内容建议:

  • key:orderId(用于分区与局部有序)。
  • value:JSON,例如 {orderId, skuId, qty, createdAt}。
  • headers:可放 traceId、来源应用等,便于排错。

交付语义与幂等:

  • 默认"至少一次"。同一条消息可能被投递多次,消费者应设计幂等(如用去重表、业务唯一键、CAS 更新)。
  • 生产端建议开启 acks=all、重试,与幂等生产者(enable.idempotence=true)。

3. Partition 和 Consumer Group 怎么理解

  • Partition(分区):同一个 Topic 可被切成多个分区,像多条并行的日志。并行度与吞吐靠分区数扩展。Kafka 只保证"同一分区内顺序"。所以要对同一 orderId 固定分区(key 一致),才能保证订单相关消息的局部有序。
  • Consumer Group(消费组):同一组内的多个实例"分摊"一个 Topic 的分区,每条消息只会被组内一个实例处理;不同组互不干扰。这样库存服务(group=inventory)和通知服务(group=notify)可以各自独立消费同一条订单创建事件。
  • Rebalance:当组内实例数变化时,会触发分区重新分配,短暂抖动是正常现象。生产要关注消费延迟监控与幂等处理。
  • Offset:消费者的"进度指针"。可手动提交(处理成功再提交),避免未处理就前进导致丢单。

结合下单场景:

  • 订单服务 -> 发送到 topic=order.created,key=orderId。
  • 库存服务(group=inventory) -> 扣减库存、落账。
  • 通知服务(group=notify) -> 推送短信/站内信。
  • 若需要退款逆向流程,可再发 order.refund 等新事件,实现解耦的业务编排。

4. Java 项目中 Kafka 的基本使用思路

落地步骤可以照这个 Checklist:

  1. 明确 Topic 与事件模型:定义 order.created 的字段与数据约定,key=orderId。
  2. 生产端配置要点:acks=all、retries>0、enable.idempotence=true,合理批量与压缩(lz4/zstd)。
  3. 消费端配置要点:group.id 分服务划分;关闭自动提交或改为"处理成功后手动提交";并设置重试与死信策略。
  4. 幂等与一致性:用订单号/业务唯一键做去重;外部副作用(扣库存、发通知)要能够安全重试。
  5. 可观测性:埋点 traceId,监控 Lag、失败率与重平衡频次。

下面给出最小可用的 Spring Boot 代码骨架(演示核心思路,省略配置与异常处理):

java 复制代码
@Service
public class OrderProducer {
  @Autowired private KafkaTemplate<String, String> kafka;
  public void sendCreated(String orderId, int skuId, int qty) {
    String json = String.format("{\"orderId\":\"%s\",\"skuId\":%d,\"qty\":%d}", orderId, skuId, qty);
    kafka.send("order.created", orderId, json); // key=orderId 保障局部有序
  }
}

@Component
public class InventoryConsumer {
  @KafkaListener(topics = "order.created", groupId = "inventory-service")
  public void onMessage(ConsumerRecord<String, String> rec) {
    String key = rec.key();
    String body = rec.value();
    // TODO: 解析 body,基于 orderId 做幂等扣减;成功后提交 offset
    // 失败可抛出异常触发框架重试/进入 DLT(需在实际工程配置)
  }
}

工程落地补充建议:

  • Topic 规划:一个事件一类 Topic,避免"大杂烩"。为回放方便,不轻易删除历史数据。
  • 错误处理:短暂失败重试;持续失败写入死信 Topic 供人工排查。
  • 事务与一致性:若下单落库与发送消息需原子,可用"事务消息"或"本地事务表 + 异步补偿"。
  • 压测与容量:根据峰值 QPS 反推分区数与消费者实例数;观察端到端延迟是否满足 SLA。
  • 安全与合规:脱敏敏感字段;设置生产与消费权限,避免误读误写。