从零入门 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-created、payment-success、user-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",剩下的还是你的活。

相关推荐
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
神仙别闹3 天前
基于C#+MySQL实现(WinForm)个人聊天室软件
c#
kybs19913 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
Polevne3 天前
C# SFTP客户端实现文件传输
c#·ssh
samble3 天前
从零搭建灌装监控系统(四):深色主题与样式系统,ResourceDictionary 统一管理
c#·wpf·mvvm·样式·工业控制
cjp5603 天前
024 . WPF MVVM类封装优化
c#
淡海水4 天前
13-04-面试-源码级深度追问链
数据结构·unity·面试·c#·游戏引擎·源码·il2cpp
asdzx674 天前
如何使用 C# 提取 PPT 文本与图片
c#·ppt·提取文本
2501_930707784 天前
使用C#代码使用条件格式为 Excel 隔行设置颜色
c#·excel
何以解忧唯有撸码4 天前
【开源】c#造个轮子-日程安排工具
c#·自定义控件