从零入门 Kafka:Java 原生 API 到 Spring Boot 实战全解析

一、Kafka 是什么,为什么要用它

在系统越来越复杂之后,你会遇到一个问题:A 系统要通知 B、C、D 系统。最原始的做法是 A 直接调用 B、C、D 的接口,但这带来了几个麻烦:

  • B 挂了,A 的请求就失败了,两者强耦合
  • 同时调 B、C、D,A 要等三个都响应才能结束,性能差
  • 以后再来个 E 系统,要改 A 的代码

Kafka 是一个分布式消息队列,它让 A 只管"把消息扔进去",B、C、D 各自订阅,各自消费,互不干扰。

复制代码
传统调用链:
A → B(同步等待)
A → C(同步等待)
A → D(同步等待)

引入 Kafka:
A → [Kafka Topic] → B(异步,随时消费)
                  → C(异步,随时消费)
                  → D(异步,随时消费)

Kafka 的三大核心价值:

  • 解耦:生产者和消费者互不知道对方的存在
  • 削峰:突发流量先缓冲在 Kafka,消费者按自己的节奏处理
  • 异步:生产者投完消息立刻返回,不等消费者处理结果

二、核心概念,先理清再写代码

2.1 五个必须懂的概念

Topic(主题)

消息的分类标签,相当于数据库的表名。生产者往 Topic 写,消费者从 Topic 读。一个系统可以有很多个 Topic,例如 order-createdpayment-successuser-register

Partition(分区)

一个 Topic 可以切成多个 Partition,分散存储在不同机器上,这是 Kafka 高吞吐的关键。每个 Partition 内部消息严格有序,但 Partition 之间无序。

复制代码
Topic: order-created
├── Partition 0: [消息1, 消息4, 消息7...]
├── Partition 1: [消息2, 消息5, 消息8...]
└── Partition 2: [消息3, 消息6, 消息9...]

Offset(偏移量)

每条消息在 Partition 内的唯一编号,从 0 开始递增。消费者通过 Offset 记录自己"消费到哪里了",这也是 Kafka 可以重放消息的原因。

Producer(生产者)

往 Kafka 写消息的一方。

Consumer & Consumer Group(消费者和消费者组)

从 Kafka 读消息的一方。多个消费者可以组成一个 Consumer Group,同一 Group 内的消费者分摊消费 Partition,实现负载均衡;不同 Group 之间互不影响,都能收到完整的消息,实现广播。

复制代码
Topic: order-created(3个Partition)

消费者组A(订单服务):
  消费者A1 → 负责 Partition 0
  消费者A2 → 负责 Partition 1 和 Partition 2

消费者组B(通知服务):
  消费者B1 → 负责全部3个 Partition(独享)

2.2 Broker 和 ZooKeeper/KRaft

Broker 就是 Kafka 的服务器节点,多个 Broker 组成 Kafka 集群。

Kafka 早期依赖 ZooKeeper 做集群元数据管理(记录哪些 Broker 活着、Topic 分区在哪里)。2.8 版本之后引入 KRaft 模式,去掉了 ZooKeeper 依赖,部署更简单。本文示例基于带 ZooKeeper 的经典模式(生产环境更常见),原理一样。


三、本地环境快速搭建

3.1 下载 Kafka

kafka.apache.org 下载,推荐 3.x 版本。解压到一个没有中文和空格的路径,例如 D:/kafka

3.2 启动 ZooKeeper(Windows)

复制代码
# 在 kafka 目录下
.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties

3.2 启动 ZooKeeper(Linux/Mac)

复制代码
./bin/zookeeper-server-start.sh ./config/zookeeper.properties

3.3 启动 Kafka Broker

复制代码
# Windows
.\bin\windows\kafka-server-start.bat .\config\server.properties

# Linux/Mac
./bin/kafka-server-start.sh ./config/server.properties

3.4 创建测试 Topic

复制代码
# Windows
.\bin\windows\kafka-topics.bat --create --topic order-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1

# Linux/Mac
./bin/kafka-topics.sh --create --topic order-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1

参数说明:

  • --partitions 3:创建 3 个分区
  • --replication-factor 1:副本数为 1(单机只能为 1,集群可以设置 2 或 3)

3.5 常用命令速查

复制代码
# 查看所有 Topic
kafka-topics.sh --list --bootstrap-server localhost:9092

# 查看 Topic 详情(分区、副本情况)
kafka-topics.sh --describe --topic order-topic --bootstrap-server localhost:9092

# 命令行生产消息(测试用)
kafka-console-producer.sh --topic order-topic --bootstrap-server localhost:9092

# 命令行消费消息(从头开始消费)
kafka-console-consumer.sh --topic order-topic --bootstrap-server localhost:9092 --from-beginning

四、Java 原生 API

在引入 Spring Boot 之前,先用原生 API 搞清楚 Kafka 的本质。

4.1 Maven 依赖

复制代码
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.5.1</version>
</dependency>

4.2 原生 Producer(生产者)

最简单的版本:发一条消息
复制代码
import org.apache.kafka.clients.producer.*;
import java.util.Properties;

public class SimpleProducer {
    public static void main(String[] args) throws Exception {

        // 1. 配置生产者参数
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");        // Kafka 地址
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");   // key 序列化
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // value 序列化

        // 2. 创建生产者
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        // 3. 创建消息(Topic, Key, Value)
        // Key 用于决定消息发往哪个 Partition:相同 Key 一定进同一个 Partition
        ProducerRecord<String, String> record =
                new ProducerRecord<>("order-topic", "order-001", "{'orderId':1,'amount':99.9}");

        // 4. 发送(异步)
        producer.send(record);

        // 5. 关闭
        producer.close();
        System.out.println("消息发送完成");
    }
}
带回调的版本:知道消息有没有发成功
复制代码
producer.send(record, (metadata, exception) -> {
    if (exception == null) {
        // 发送成功
        System.out.printf("消息发送成功 → Topic: %s, Partition: %d, Offset: %d%n",
                metadata.topic(),
                metadata.partition(),
                metadata.offset());
    } else {
        // 发送失败
        System.err.println("消息发送失败: " + exception.getMessage());
        // 实际业务中:记录失败日志,加入重试队列,触发告警等
    }
});
同步发送:等待确认再继续
复制代码
// get() 会阻塞直到收到 Broker 的确认
RecordMetadata metadata = producer.send(record).get();
System.out.printf("同步发送成功,Offset: %d%n", metadata.offset());

同步 vs 异步:异步吞吐高但不能立即知道结果;同步性能低但可以立即处理失败。业务中根据场景选择,大多数情况用异步+回调。

关键生产者参数
复制代码
// 消息可靠性配置
props.put("acks", "all");
// "0" = 不等确认,最快但可能丢消息
// "1" = Leader 写入即确认,均衡
// "all" = 所有副本写入才确认,最安全,推荐生产使用

// 失败重试次数
props.put("retries", 3);

// 批量发送:消息积累到 16KB 就发出去(提高吞吐)
props.put("batch.size", 16384);

// 等待时间:即使没满 batch.size,超过 5ms 也强制发送(降低延迟)
props.put("linger.ms", 5);

// 消息最大字节数
props.put("max.request.size", 1048576); // 1MB
指定 Partition 发送
复制代码
// 方式一:在 ProducerRecord 构造时直接指定 Partition 编号
ProducerRecord<String, String> record =
        new ProducerRecord<>("order-topic", 0, "order-001", "消息内容"); // 强制发往 Partition 0

// 方式二:通过 Key 路由(相同 Key 一定去同一个 Partition,适合保序场景)
// 例如同一个用户的所有订单消息,用 userId 作为 Key,保证顺序消费
ProducerRecord<String, String> record =
        new ProducerRecord<>("order-topic", "user-123", "消息内容");

// 方式三:自定义分区器
public class OrderPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        // 根据业务逻辑决定发往哪个 Partition
        int partitionCount = cluster.partitionCountForTopic(topic);
        if (key != null) {
            return Math.abs(key.hashCode()) % partitionCount;
        }
        return 0;
    }
    // ... 其他方法
}

// 使用自定义分区器
props.put("partitioner.class", "com.example.OrderPartitioner");

4.3 原生 Consumer(消费者)

基础消费:手动控制循环
复制代码
import org.apache.kafka.clients.consumer.*;
import java.time.Duration;
import java.util.*;

public class SimpleConsumer {
    public static void main(String[] args) {

        // 1. 配置消费者参数
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("group.id", "order-consumer-group");          // 消费者组 ID,非常重要
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 自动提交 Offset(先理解,后面会讲手动提交更安全)
        props.put("auto.commit.enable", "true");
        props.put("auto.commit.interval.ms", "1000");           // 每 1 秒自动提交一次

        // 2. 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 3. 订阅 Topic(可以订阅多个)
        consumer.subscribe(Arrays.asList("order-topic"));

        // 4. 循环拉取消息(Kafka 是拉模式,消费者主动去拉)
        try {
            while (true) {
                // poll 拉取消息,最多等待 100ms
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));

                for (ConsumerRecord<String, String> record : records) {
                    System.out.printf("收到消息 → Topic: %s, Partition: %d, Offset: %d, Key: %s, Value: %s%n",
                            record.topic(),
                            record.partition(),
                            record.offset(),
                            record.key(),
                            record.value());

                    // 业务处理:保存到数据库、调用服务等
                    processOrder(record.value());
                }
            }
        } finally {
            consumer.close();
        }
    }

    private static void processOrder(String orderJson) {
        // 实际业务逻辑
        System.out.println("处理订单: " + orderJson);
    }
}
手动提交 Offset(生产环境推荐)

自动提交有个风险:消息拉下来还没处理完,Offset 就提交了,这时候应用崩溃,消息就丢了。手动提交能保证"处理完再提交":

复制代码
props.put("enable.auto.commit", "false");  // 关闭自动提交

// 方式一:同步提交(处理完一批提交一次,简单安全)
for (ConsumerRecord<String, String> record : records) {
    processOrder(record.value());
}
consumer.commitSync();  // 批量处理完后统一提交

// 方式二:异步提交(性能更好,不阻塞)
consumer.commitAsync((offsets, exception) -> {
    if (exception != null) {
        System.err.println("Offset 提交失败: " + exception.getMessage());
        // 记录日志,下次重新消费
    }
});

// 方式三:精确提交到某个 Offset(最精细的控制)
Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
    processOrder(record.value());
    offsetsToCommit.put(
        new TopicPartition(record.topic(), record.partition()),
        new OffsetAndMetadata(record.offset() + 1)  // 注意:提交的是下一条消息的 Offset
    );
}
consumer.commitSync(offsetsToCommit);
auto.offset.reset 参数:新消费者组第一次消费从哪开始
复制代码
// "earliest":从 Topic 最早的消息开始消费(--from-beginning 的效果)
props.put("auto.offset.reset", "earliest");

// "latest":只消费订阅之后新来的消息(默认值)
props.put("auto.offset.reset", "latest");

五、Spring Boot 整合 Kafka

原生 API 帮你理解了本质,但在实际项目里,Spring Boot 封装让一切更优雅。

5.1 依赖

复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

5.2 application.yml 配置

复制代码
spring:
  kafka:
    # Kafka 服务器地址(集群用逗号分隔)
    bootstrap-servers: localhost:9092

    # 生产者配置
    producer:
      # 序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 可靠性:all = 等所有副本确认
      acks: all
      # 失败重试次数
      retries: 3
      # 批量发送大小
      batch-size: 16384
      # 批量等待时间
      properties:
        linger.ms: 5

    # 消费者配置
    consumer:
      # 消费者组 ID
      group-id: my-service-group
      # 反序列化
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 新消费者组从最早的消息开始消费
      auto-offset-reset: earliest
      # 关闭自动提交(手动控制更安全)
      enable-auto-commit: false

    # 监听器配置
    listener:
      # MANUAL_IMMEDIATE = 手动立即提交 Offset
      ack-mode: MANUAL_IMMEDIATE
      # 并发消费线程数(建议等于 Partition 数量)
      concurrency: 3

5.3 创建 Topic(用配置类,推荐)

复制代码
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;

@Configuration
public class KafkaTopicConfig {

    /**
     * 创建订单主题:3 个分区,2 个副本
     * Spring Boot 启动时会自动检查,不存在才创建
     */
    @Bean
    public NewTopic orderTopic() {
        return TopicBuilder
                .name("order-created")
                .partitions(3)
                .replicas(1)  // 本地开发用 1,生产环境用 2 或 3
                .build();
    }

    @Bean
    public NewTopic paymentTopic() {
        return TopicBuilder
                .name("payment-success")
                .partitions(3)
                .replicas(1)
                .build();
    }
}

5.4 生产者:KafkaTemplate

复制代码
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFutureCallback;

@Service
public class OrderEventProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 最简单的发送
     */
    public void sendSimple(String orderId, String message) {
        kafkaTemplate.send("order-created", orderId, message);
    }

    /**
     * 带回调的异步发送(生产环境推荐)
     */
    public void sendWithCallback(String orderId, String orderJson) {
        kafkaTemplate.send("order-created", orderId, orderJson)
                .addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
                    @Override
                    public void onSuccess(SendResult<String, String> result) {
                        RecordMetadata meta = result.getRecordMetadata();
                        log.info("订单消息发送成功 → orderId={}, partition={}, offset={}",
                                orderId, meta.partition(), meta.offset());
                    }

                    @Override
                    public void onFailure(Throwable ex) {
                        log.error("订单消息发送失败 → orderId={}, 原因={}", orderId, ex.getMessage());
                        // 业务处理:写入补偿表、触发告警、加入重试队列等
                    }
                });
    }

    /**
     * 发送对象(需要配置 JSON 序列化,后面会讲)
     */
    public void sendOrderEvent(OrderCreatedEvent event) {
        kafkaTemplate.send("order-created", String.valueOf(event.getOrderId()), event);
    }
}

5.5 消费者:@KafkaListener

基础消费
复制代码
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

@Component
public class OrderEventConsumer {

    /**
     * 最基础的消费
     */
    @KafkaListener(topics = "order-created", groupId = "order-service-group")
    public void handleOrderCreated(String message) {
        System.out.println("收到订单消息: " + message);
        // 处理业务逻辑
    }

    /**
     * 获取消息完整元数据 + 手动提交 Offset(生产环境推荐)
     */
    @KafkaListener(topics = "order-created", groupId = "order-service-group")
    public void handleWithAck(
            @Payload String message,                         // 消息内容
            @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,      // Topic 名称
            @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition, // Partition 编号
            @Header(KafkaHeaders.OFFSET) long offset,        // Offset
            Acknowledgment ack) {                            // 手动提交工具(需配置 ack-mode: MANUAL_IMMEDIATE)

        log.info("收到消息 → topic={}, partition={}, offset={}, message={}",
                topic, partition, offset, message);
        try {
            processOrder(message);
            ack.acknowledge();  // 处理成功才提交 Offset
        } catch (Exception e) {
            log.error("消息处理失败,不提交 Offset,等待重试: {}", e.getMessage());
            // 不调用 ack.acknowledge(),消息会被重新消费
        }
    }

    /**
     * 批量消费(提高吞吐量)
     */
    @KafkaListener(topics = "order-created", groupId = "order-batch-group",
                   containerFactory = "batchKafkaListenerContainerFactory")
    public void handleBatch(List<String> messages, Acknowledgment ack) {
        log.info("批量收到 {} 条消息", messages.size());
        for (String message : messages) {
            processOrder(message);
        }
        ack.acknowledge();  // 批量处理完后统一提交
    }
}
监听多个 Topic / 指定 Partition
复制代码
// 监听多个 Topic
@KafkaListener(topics = {"order-created", "order-updated"}, groupId = "order-group")
public void handleMultipleTopics(String message,
        @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
    log.info("来自 Topic: {}, 消息: {}", topic, message);
}

// 监听指定 Partition(精确控制,不常用)
@KafkaListener(topicPartitions = {
        @TopicPartition(topic = "order-created", partitions = {"0", "1"})
}, groupId = "order-group")
public void handleSpecificPartition(String message) {
    // 只消费 Partition 0 和 1 的消息
}

六、发送 Java 对象(JSON 序列化)

实际业务中发的是对象,不是裸字符串。

6.1 定义消息对象

复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderCreatedEvent {
    private Long orderId;
    private Integer userId;
    private BigDecimal amount;
    private String status;
    private LocalDateTime createdAt;
}

6.2 配置 JSON 序列化(两种方式)

方式一:yml 配置(简洁)

复制代码
spring:
  kafka:
    producer:
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "cn.tedu.charging.*"  # 允许反序列化的包,安全白名单
        spring.json.value.default.type: cn.tedu.charging.order.dto.OrderCreatedEvent

方式二:Java 配置类(更灵活,推荐)

复制代码
@Configuration
public class KafkaSerializerConfig {

    @Bean
    public ProducerFactory<String, Object> producerFactory() {
        Map<String, Object> config = new HashMap<>();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory<>(config);
    }

    @Bean
    public KafkaTemplate<String, Object> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }

    @Bean
    public ConsumerFactory<String, OrderCreatedEvent> consumerFactory() {
        Map<String, Object> config = new HashMap<>();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        config.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service-group");
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        config.put(JsonDeserializer.TRUSTED_PACKAGES, "cn.tedu.charging.*");
        return new DefaultKafkaConsumerFactory<>(config,
                new StringDeserializer(),
                new JsonDeserializer<>(OrderCreatedEvent.class));
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent>
            kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        return factory;
    }
}

6.3 发送和接收对象

复制代码
// 发送
@Service
public class OrderService {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void createOrder(Order order) {
        // 业务逻辑保存订单...

        // 发送领域事件
        OrderCreatedEvent event = new OrderCreatedEvent(
                order.getId(), order.getUserId(),
                order.getAmount(), "CREATED", LocalDateTime.now()
        );
        kafkaTemplate.send("order-created", String.valueOf(order.getId()), event);
        log.info("订单创建事件已发送,orderId={}", order.getId());
    }
}

// 消费
@KafkaListener(topics = "order-created", groupId = "notification-group")
public void handleOrderCreated(OrderCreatedEvent event, Acknowledgment ack) {
    log.info("收到订单创建事件: orderId={}, userId={}, amount={}",
            event.getOrderId(), event.getUserId(), event.getAmount());

    // 发送通知
    notificationService.sendOrderCreatedNotification(event);
    ack.acknowledge();
}

七、实战场景:完整的充电计费事件流

以充电桩场景为例,把 Kafka 嵌入真实业务:

复制代码
充电桩上报数据
    → MQTT Consumer 接收
    → 调用计费服务计算费用
    → 发送 Kafka 消息(charging-process-topic)
    → 通知服务消费 → 推送 WebSocket 给用户
    → 账单服务消费 → 更新账单记录
    → 日志服务消费 → 写入充电日志

7.1 定义消息对象

复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChargingProcessEvent {
    private String orderNo;
    private Integer userId;
    private BigDecimal chargingCapacity;    // 当前累计充电量
    private BigDecimal totalFee;            // 当前累计费用
    private BigDecimal powerFee;            // 电费单价
    private BigDecimal serviceFee;          // 服务费单价
    private String costRuleName;            // 计费规则名称(尖峰平谷)
    private Long chargingTime;              // 充电时长(ms)
    private Long timestamp;                 // 事件时间戳
}

7.2 生产者:计费服务计算完成后发事件

复制代码
@Service
@Slf4j
public class ChargingEventPublisher {

    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void publishChargingProcess(ChargingProcessVO vo) {
        ChargingProcessEvent event = buildEvent(vo);

        kafkaTemplate.send("charging-process", vo.getOrderNo(), event)
                .addCallback(
                        result -> log.info("充电进度事件发布成功,orderNo={}", vo.getOrderNo()),
                        ex -> log.error("充电进度事件发布失败,orderNo={}", vo.getOrderNo(), ex)
                );
    }

    private ChargingProcessEvent buildEvent(ChargingProcessVO vo) {
        ChargingProcessEvent event = new ChargingProcessEvent();
        BeanUtils.copyProperties(vo, event);
        event.setTimestamp(System.currentTimeMillis());
        return event;
    }
}

7.3 消费者一:通知服务推送 WebSocket

复制代码
@Component
@Slf4j
public class ChargingNotificationConsumer {

    @Autowired
    private WebSocketServer webSocketServer;

    @KafkaListener(
            topics = "charging-process",
            groupId = "notification-service-group",
            containerFactory = "chargingEventContainerFactory"
    )
    public void pushToUser(ChargingProcessEvent event, Acknowledgment ack) {
        log.info("收到充电进度事件,准备推送 WebSocket,userId={}", event.getUserId());
        try {
            String message = JsonUtil.toJson(event);
            webSocketServer.sendMessage(event.getUserId(), message);
            ack.acknowledge();
            log.info("WebSocket 推送成功,userId={}", event.getUserId());
        } catch (Exception e) {
            log.error("WebSocket 推送失败,userId={}", event.getUserId(), e);
            // 不 ack,等待重试
        }
    }
}

7.4 消费者二:账单服务更新账单(同一 Topic,不同 Group)

复制代码
@Component
@Slf4j
public class ChargingBillConsumer {

    @Autowired
    private BillService billService;

    // groupId 不同,所以这个消费者也能收到完整的消息(广播效果)
    @KafkaListener(
            topics = "charging-process",
            groupId = "bill-service-group",
            containerFactory = "chargingEventContainerFactory"
    )
    public void updateBill(ChargingProcessEvent event, Acknowledgment ack) {
        log.info("收到充电进度事件,更新账单,orderNo={}", event.getOrderNo());
        try {
            billService.updateChargingBill(event.getOrderNo(), event.getTotalFee());
            ack.acknowledge();
        } catch (Exception e) {
            log.error("账单更新失败,orderNo={}", event.getOrderNo(), e);
        }
    }
}

八、死信队列:消息消费失败怎么办

消费失败不能无限重试,需要有个兜底方案。

复制代码
@Configuration
public class KafkaDeadLetterConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, Object>
            kafkaListenerContainerFactory(ConsumerFactory<String, Object> consumerFactory,
                                         KafkaTemplate<String, Object> kafkaTemplate) {

        ConcurrentKafkaListenerContainerFactory<String, Object> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory);

        // 配置错误处理器:重试 3 次失败后发送到死信队列
        factory.setErrorHandler(new SeekToCurrentErrorHandler(
                new DeadLetterPublishingRecoverer(kafkaTemplate), // 发到死信 Topic
                new FixedBackOff(1000L, 3)  // 每隔 1 秒重试,最多重试 3 次
        ));

        return factory;
    }
}

死信 Topic 命名规则:原 Topic 名称 + .DLT,例如 charging-process.DLT

复制代码
// 监听死信队列,人工介入或告警
@KafkaListener(topics = "charging-process.DLT", groupId = "dlt-handler-group")
public void handleDeadLetter(ConsumerRecord<String, Object> record) {
    log.error("【死信消息】Topic={}, Key={}, Value={}",
            record.topic(), record.key(), record.value());
    // 告警、写入数据库人工处理、邮件通知等
    alertService.sendDeadLetterAlert(record);
}

九、常见问题与踩坑

9.1 消息重复消费

Kafka 保证"至少一次"(at-least-once)投递,网络抖动时可能重复。解决方案:消费端做幂等

复制代码
@KafkaListener(topics = "order-created")
public void handleOrder(String orderJson, Acknowledgment ack) {
    OrderCreatedEvent event = JsonUtil.toObject(orderJson, OrderCreatedEvent.class);

    // 用 orderNo 做幂等键,检查是否已经处理过
    if (orderProcessedCache.contains(event.getOrderNo())) {
        log.warn("重复消息,已跳过处理,orderNo={}", event.getOrderNo());
        ack.acknowledge();  // 仍要 ack,避免无限重试
        return;
    }

    processOrder(event);
    orderProcessedCache.add(event.getOrderNo());  // 实际用 Redis SET 做分布式幂等
    ack.acknowledge();
}

9.2 消息顺序问题

同一 Topic 多个 Partition 之间无序。如果需要保序(比如同一用户的操作按顺序处理):

  • 生产端:相同业务 Key(如 userId)发到相同 Partition

  • 消费端:该 Partition 只由一个消费者处理

    // 生产端:userId 相同的消息一定去同一个 Partition
    kafkaTemplate.send("user-events", String.valueOf(event.getUserId()), event);

9.3 消费者线程数与 Partition 数的关系

复制代码
消费者数 < Partition 数:部分消费者处理多个 Partition,可以(压力不均)
消费者数 = Partition 数:最理想,每人负责一个 Partition
消费者数 > Partition 数:多出来的消费者空闲,浪费资源

所以 yml 里的 listener.concurrency 建议等于 Partition 数量:

复制代码
spring:
  kafka:
    listener:
      concurrency: 3   # Topic 有 3 个 Partition,就配 3 个线程

9.4 消费者 Rebalance

当消费者组内有成员加入或退出时,Kafka 会触发 Rebalance,重新分配 Partition,期间会暂停消费。优化方式:

复制代码
spring:
  kafka:
    consumer:
      properties:
        # 心跳间隔(消费者每隔多久告诉 Broker 自己还活着)
        heartbeat.interval.ms: 3000
        # 会话超时时间(超过这个时间没心跳,认为消费者挂了,触发 Rebalance)
        session.timeout.ms: 30000
        # 每次 poll 之间的最大间隔,超过这个时间也会触发 Rebalance
        max.poll.interval.ms: 300000

十、总结对照表

功能 原生 API Spring Boot
生产消息 KafkaProducer.send() KafkaTemplate.send()
消费消息 KafkaConsumer.poll() + while 循环 @KafkaListener 注解
手动提交 consumer.commitSync() Acknowledgment.acknowledge()
序列化 手动配置 Serializer yml 配置或 @Bean 配置类
创建 Topic 命令行或 AdminClient @Bean NewTopic 自动创建
死信队列 手动捕异常转发 DeadLetterPublishingRecoverer
重试 手动实现 SeekToCurrentErrorHandler + FixedBackOff

选型建议

  • 学习和理解底层原理 → 先玩原生 API
  • 实际项目开发 → 直接用 Spring Boot 整合,开发效率高出一个量级
  • 需要精细控制分区、序列化、消费策略 → 用 Java Config 方式配置 Spring Kafka,不要只依赖 yml

记住最重要的一条:Kafka 能帮你解耦和削峰,但它不是银弹。消息的可靠性、幂等性、顺序性都需要你在业务代码里配合处理,Kafka 只负责"把消息安全地从 A 送到 B",剩下的还是你的活。

相关推荐
唐青枫1 小时前
C#.NET YARP + OpenTelemetry:网关链路追踪实战
c#·.net
Xin_ye1008613 小时前
C# 零基础到精通教程 - 第七章:面向对象编程(入门)——类与对象
开发语言·c#
rockey62713 小时前
AScript异步执行与await关键字
c#·.net·script·eval·expression·异步执行·动态脚本
程序leo源15 小时前
Qt窗口详解
开发语言·数据库·c++·qt·青少年编程·c#
月巴月巴白勺合鸟月半19 小时前
质本洁来还洁去,强于污淖陷文本
c#
Xin_ye1008620 小时前
C# 零基础到精通教程 - 第八章:面向对象编程(进阶)——继承与多态
开发语言·c#
asdzx671 天前
使用 C# 打印 Excel 文档(详细教程)
c#·excel
伽蓝_游戏1 天前
第四章:AssetBundle 核心机制与文件结构
unity·c#·游戏引擎·游戏程序
2501_930707781 天前
使用C#代码拆分 PowerPoint 演示文稿
开发语言·c#·powerpoint