Spring Boot + Kafka 全面实战案例

🌟 Spring Boot + Kafka 全面实战案例(覆盖市面 99% 场景)

本文使用 Spring Boot 3 + Kafka 3.x,演示 Kafka 在微服务中的完整应用场景,包括生产者、消费者、事务、幂等、批量消费、Streams、延迟消息、死信队列等。


📖 目录

  1. 项目简介
  2. 环境准备
  3. 项目结构
  4. [配置 Kafka](#配置 Kafka)
  5. [Kafka 生产者示例](#Kafka 生产者示例)
  6. [Kafka 消费者示例](#Kafka 消费者示例)
  7. 事务与幂等生产
  8. 批量消费与并发消费
  9. [Kafka Streams 实时处理](#Kafka Streams 实时处理)
  10. [延迟消息 & Dead Letter Queue](#延迟消息 & Dead Letter Queue)
  11. 消息序列化与自定义对象传输
  12. 测试与调试
  13. 总结
  14. 源码

📝 项目简介

  • Spring Boot 版本:3.2.x
  • Kafka 版本:3.x
  • JDK:17
  • 覆盖场景:
    • 普通生产者/消费者
    • 事务消息 & 幂等生产
    • 批量 & 并发消费
    • Streams 流处理
    • 延迟消息 & 死信队列
    • 对象序列化与 JSON 消息
    • Spring Cloud Stream 方式集成

💻 环境准备

  • Kafka 集群(单机或多节点)
  • Zookeeper(Kafka 依赖)

Docker Compose 示例

yaml 复制代码
version: '3.8'

services:
  zookeeper:
    image: bitnami/zookeeper:3.8
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ALLOW_ANONYMOUS_LOGIN: "yes"
    restart: unless-stopped

  kafka:
    image: bitnami/kafka:3.5
    container_name: kafka
    ports:
      - "9092:9092"
      - "29092:29092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    depends_on:
      - zookeeper
    restart: unless-stopped

📂 项目结构

复制代码
springboot-kafka-demo
├─ src/main/java/com/example/kafka
│  ├─ config
│  │   ├─ KafkaAdminConfig.java
│  │   ├─ KafkaConsumerConfig.java
│  │   ├─ KafkaProducerConfig.java
│  │   └─ KafkaStreamConfig.java
│  ├─ controller
│  │   └─ KafkaController.java
│  ├─ service
│  │   ├─ KafkaConsumerService.java
│  │   ├─ KafkaLogService.java
│  │   ├─ KafkaProducerService.java
│  │   └─ KafkaTopicService.java
│  ├─ dto
│  │   └─ UserQueryLog.java
│  └─ SpringbootKafkaDemoApplication.java
└─ src/main/resources
   └─ application.yml

⚙️ 配置 Kafka

yaml 复制代码
server:
  port: 8081

# kafka
spring:
  kafka:
    bootstrap-servers: 192.168.3.150:29092 # Kafka broker 地址列表,客户端连接 Kafka 集群的入口,格式 host:port,多节点用逗号分隔
    # Producer(生产者)配置
    producer:
      # 生产者发送消息时 key 的序列化器,将 Java 对象转换为字节数组
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 生产者发送消息时 value 的序列化器
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      # 消息发送确认机制: all, none, lead (default: all),"all" 表示等待所有 ISR 副本确认,保证最高可靠性
      acks: all
      # 发送失败时重试次数,默认是 0(不重试)
      retries: 3
      # 批量发送的消息大小(字节),达到该大小后批量发送
      batch-size: 16384
      # 消息压缩类型,支持 none(无)、gzip、snappy、lz4、zstd,提高网络传输效率
      compression-type: snappy
      # 生产者网络请求缓冲区大小,单位字节(default: 131072)
      buffer-memory: 131072
      # 生产者的其他属性
      properties:
        # 是否开启幂等性,防止重复消息发送,确保消息不重复写入
        enable.idempotence: true
        # 最大的未确认请求数,设置为 1 可以保证严格的顺序写入 (default: 5)
        max.in.flight.requests.per.connection: 1
    # Consumer(消费者)配置
    consumer:
      # 消费者组 ID,Kafka 通过该 ID 管理消费者组协调消费分区
      group-id: my-group-id
      # key 的反序列化器,将字节数组转换为 Java 对象
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # value 的反序列化器
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      # 自动提交偏移量的时间间隔(毫秒)
      auto-commit-interval: 1000
      # 是否开启自动提交偏移量,true 表示 Kafka 客户端自动提交消费进度(default: true)
      enable-auto-commit: false  # 禁用自动提交
      # 新消费组第一次消费时从哪里开始消费,earliest 表示从头开始消费,latest 表示最新消息开始消费
      auto-offset-reset: earliest
      # 每次 poll 拉取的最大记录数
      max-poll-records: 10
      # 消费者其他属性
      properties:
        # 会话超时时间(毫秒),超过该时间未收到心跳则认为消费者离线(default: 10000)
        session.timeout.ms: 10000
        # 心跳间隔(毫秒),定期发送心跳保持与 Kafka 协调器连接(default: 3000)
        heartbeat.interval.ms: 3000
        # 最大轮询间隔(毫秒),超过此时间未调用 poll 方法则认为消费者失效(default: 5000)
        max.poll.interval.ms: 5000
        # 指定消息值的反序列化器,将 Kafka 中的 JSON 数据转换为 Java 对象。
        spring.json.trusted.packages: '*'
    # Listener(消息监听器)配置
    listener:
      # 消费者监听器的确认模式,RECORD 表示每条消息确认,其他还有 MANUAL、TIME 等 (MANUAL, MANUAL_IMMEDIATE, RECORD, TIME)
      ack-mode: MANUAL  # 手动 ack
      # 监听器的并发线程数,提高消费吞吐量
      concurrency: 3
      # 监听消息的类型,batch 表示批量消息消费,single 表示单条消息消费
      type: batch
    # KafkaTemplate 默认配置
    template:
      # 发送消息时默认的主题名
      default-topic: default-topic
    streams:
      application-id: log-aggregation-app
      properties:
        default.key.serde: org.apache.kafka.common.serialization.Serdes$StringSerde
        default.value.serde: org.apache.kafka.common.serialization.Serdes$StringSerde

logging:
  level:
    org.springframework.web: INFO
    org.example.microservice: DEBUG

🚀 Kafka 生产者示例

java 复制代码
package com.example.kafka.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

/**
 * Kafka生产者服务
 */
@Service
@Slf4j
public class KafkaProducerService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public KafkaProducerService(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    /**
     * 发送消息到指定主题
     *
     * @param topic   目标主题
     * @param message 消息内容
     */
    public void sendMessage(String topic, String message) {
        log.info("Producing message to topic {}: {}", topic, message);
        // 发送消息,不关心结果
        kafkaTemplate.send(topic, message);
    }

    /**
     * 发送消息并异步处理结果
     *
     * @param topic   目标主题
     * @param key     消息键
     * @param message 消息内容
     */
    public void sendMessageWithCallback(String topic, String key, String message) {
        log.info("Producing message to topic {} with key {}: {}", topic, key, message);

        kafkaTemplate.executeInTransaction(kt -> {
            // 发送消息并添加回调
            CompletableFuture<SendResult<String, String>> future = kt.send(topic, key, message);

            future.thenAccept(result -> {
                log.info("Sent message successfully to topic {} with offset {}",
                        result.getRecordMetadata().topic(),
                        result.getRecordMetadata().offset());
            }).exceptionally(ex -> {
                log.error("Failed to send message to topic {}", topic, ex);
                return null;
            });
            return true;
        });

    }
}

调用示例:

java 复制代码
package com.example.kafka.controller;

import com.alibaba.fastjson2.JSONObject;
import com.example.kafka.service.KafkaLogService;
import com.example.kafka.service.KafkaProducerService;
import com.example.kafka.service.KafkaTopicService;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.admin.TopicDescription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Set;

@RestController
@RequestMapping("/kafka")
@Slf4j
public class KafkaController {

    @Autowired
    KafkaProducerService producerService;

    @Autowired
    private KafkaTopicService topicService;

    @Autowired
    private KafkaLogService logService;

    @GetMapping("/send/{message}")
    public String sendMessage(@PathVariable String message) {
        producerService.sendMessage("default-topic", message);
        return "Message sent: " + message;
    }

    @GetMapping("/send")
    public String sendMessage() {
        JSONObject json = new JSONObject();
        json.put("level", "ERROR");
        producerService.sendMessage("application-logs", json.toString());
        return "Message sent: " + json;
    }

    /**
     * 发送消息
     */
    @GetMapping("/send/{topic}/{key}/{message}")
    public String sendMessage(@PathVariable String topic, @PathVariable String key, @PathVariable String message) {
        producerService.sendMessageWithCallback(topic, key, message);
        return "topic sent: " + topic + ",key sent: " + key + ",Message sent: " + message;
    }

}

发送消息:http://localhost:8081/kafka/send/测试发送


📥 Kafka 消费者示例

java 复制代码
package com.example.kafka.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeader;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.List;


/**
 * | 概念       | 作用                               |
 * | --------- | -------------------------------- |
 * | **Topic** | 消息分类容器(生产者写入、消费者读取)              |
 * | **Group** | 消费者逻辑分组,控制消费并发与 offset 提交        |
 * | **Key**   | 控制消息落入哪个 Partition(分区),从而控制消费顺序性 |
 * 丨
 * Kafka消费者服务
 *
 * @author wkj
 */
@Service
@Slf4j
public class KafkaConsumerService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public KafkaConsumerService(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
        this.kafkaTemplate.setTransactionIdPrefix("txn-");
    }

    /**
     * 监听单个消息
     *
     * @param consumeRecord 消费的消息记录
     */
    @KafkaListener(topics = "${spring.kafka.template.default-topic}", groupId = "${spring.kafka.consumer.group-id}")
    public void listenSingleMessage(ConsumerRecord<String, String> consumeRecord) {
        log.info("监听单个消息,主题:{},key:{},value: {}",
                consumeRecord.topic(), consumeRecord.key(), consumeRecord.value());
    }

    /**
     * 监听批量消息
     *
     * @param consumeRecords 批量消息记录
     * @param ack            手动确认对象(当配置为手动确认时使用)
     */
    /*
      containerFactory = "kafkaListenerContainerFactory":使用哪个 Kafka 监听容器工厂(ConcurrentKafkaListenerContainerFactory)来创建消费者监听容器。
      该容器 在 KafkaConsumerConfig 配置中配置了批量消费,因此这里使用 kafkaListenerContainerFactory
     */
    @KafkaListener(topics = "my-batch", containerFactory = "kafkaListenerContainerFactory")
    public void listenBatchMessages(List<ConsumerRecord<String, String>> consumeRecords, Acknowledgment ack) {
        try {
            log.info("批量消息数量: {}", consumeRecords.size());
            for (ConsumerRecord<String, String> consumeRecord : consumeRecords) {
                log.info("监听单个消息,主题:{},key:{},value: {},偏移量:{},分区:{}",
                        consumeRecord.topic(), consumeRecord.key(), consumeRecord.value(), consumeRecord.offset(), consumeRecord.partition());
                // 处理逻辑 ...
            }
            // 处理成功后手动 ack
            ack.acknowledge();
        } catch (Exception e) {
            log.error("处理失败,提交 offset,失败原因:{}", e.getMessage());
            // 不调用 ack.acknowledge(),Kafka 将在下一次重新投递
        }
    }

    /**
     * 监听特定主题的消息 (类似于 监听单个消息, 只需要自己设置topics,和groupId)
     *
     * @param message 消息内容
     */
    @KafkaListener(topics = "custom-topic", groupId = "custom-group")
    public void listenCustomTopic(String message) {
        log.info("Received message from custom topic: {}", message);
    }

    /**
     * 手动提交偏移量示例
     *
     * @param consumeRecord 消费者记录
     * @param ack           手动确认对象
     */
    @KafkaListener(topics = "reliable-topic", groupId = "reliable-group")
    public void consumeReliably(ConsumerRecord<String, String> consumeRecord, Acknowledgment ack) {
        try {
            log.info("手动提交偏移量示例,主题:{},key:{},value: {}",
                    consumeRecord.topic(), consumeRecord.key(), consumeRecord.value());
            // 模拟业务处理
            // ..........
            // 手动提交偏移量
            ack.acknowledge();
            log.info("已提交偏移量");
        } catch (Exception e) {
            // 不提交偏移量,消息会被重新消费
            log.error("不提交偏移量,消息会被重新消费,失败原因:{}", e.getMessage());
        }
    }

    @KafkaListener(topics = "dead-letter-topic", groupId = "dlt-group")
    public void listenDLT(ConsumerRecord<String, String> consumeRecord) {
        log.warn("死信队列接收到失败消息:Key:{},value:{}", consumeRecord.key(), consumeRecord.value());
    }

    private static final int MAX_RETRIES = 3;
    private static final String DEAD_LETTER_TOPIC = "dead-letter-topic";

    /**
     * 消费者重试机制,适用于消费失败的消息,会根据重试次数进行重试。
     * 超过最大重试次数后,消息会被发送到死信队列。
     *
     * @param consumeRecord 消费者记录
     * @param ack           手动确认对象
     */
    @KafkaListener(topics = "retryable-topic", groupId = "retryable-group")
    public void consumeWithRetry(ConsumerRecord<String, String> consumeRecord, Acknowledgment ack) {
        // 从消息头中提取重试次数
        int retryCount = extractRetryCount(consumeRecord.headers());

        try {
            log.info("手动提交偏移量示例,主题:{},key:{},value: {},重试次数:{}",
                    consumeRecord.topic(), consumeRecord.key(), consumeRecord.value(), retryCount);
            // 业务处理
            // .......
            ack.acknowledge();
        } catch (Exception e) {
            retryCount++;
            if (retryCount <= MAX_RETRIES) {
                log.info("重试失败,失败次数:{},消息:{}", retryCount, consumeRecord.value());
                // 更新消息头中的重试次数并重新发送到原主题
                sendWithRetryHeader(consumeRecord.topic(), consumeRecord.key(), consumeRecord.value(), retryCount);
            } else {
                log.error("超过最大重试次数,消息:{}", consumeRecord.value());
                // 发送到死信队列
                kafkaTemplate.send(DEAD_LETTER_TOPIC, consumeRecord.key(), consumeRecord.value());
                ack.acknowledge(); // ❗转发到 DLQ 后,手动 ack,防止 Kafka 一直重复投递
            }
        }
    }

    /**
     * Kafka支持事务性消息
     *
     * @param consumeRecord 消费者记录
     * @param ack           手动确认对象
     */
    /*
        Kafka 能做到的是"事务性生产 + 最终一致性消费",不等于数据库级别的全局分布式事务。如果你要跨 Kafka 与数据库、Redis、HTTP 服务等实现一致性,请考虑:
        ✅ 幂等性设计(业务唯一 key)
        ✅ 手动提交 offset(控制消费确认时机)
        ✅ 本地消息表 + 补偿机制(适合 DDD/Event Sourcing)
        ✅ 事务出错转 DLQ + 监控
     */
    @KafkaListener(topics = "transactional-topic", groupId = "transactional-group")
    public void consumeTransactional(ConsumerRecord<String, String> consumeRecord, Acknowledgment ack) {
        // 开启事务
        kafkaTemplate.executeInTransaction(operations -> {
            try {
                log.info("处理事务消息,主题:{},key:{},value: {}",
                        consumeRecord.topic(), consumeRecord.key(), consumeRecord.value());
                // 业务处理
                // .......
                // 发送到其他主题
                operations.send("processed-topic1", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                operations.send("processed-topic2", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                operations.send("processed-topic3", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                // 手动提交偏移量(事务中会自动处理)
                ack.acknowledge();  // ✅ 第二步:提交 offset,表示成功处理

                return true; // 事务成功
            } catch (Exception e) {
                log.error("事务失败,失败原因:{}", e.getMessage());
                // 事务回滚
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 发送带有重试次数的消息
     *
     * @param topic      目标主题
     * @param key        消息的 key
     * @param value      消息的内容
     * @param retryCount 重试次数
     */
    private void sendWithRetryHeader(String topic, String key, String value, int retryCount) {
        // 创建带有重试次数的消息头
        // 使用 Kafka 的 RecordHeaders 添加重试次数
        RecordHeaders headers = new RecordHeaders();
        headers.add(new RecordHeader("retry-count", String.valueOf(retryCount).getBytes(StandardCharsets.UTF_8)));

        // 构造 Kafka 原生 ProducerRecord
        ProducerRecord<String, String> producerRecord =
                new ProducerRecord<>(topic, null, null, key, value, headers);
        kafkaTemplate.send(producerRecord);
    }

    /**
     * 从消息头中提取重试次数
     *
     * @param headers 消息头
     * @return 重试次数
     */
    public int extractRetryCount(Headers headers) {
        Header retryHeader = headers.lastHeader("retry-count");
        if (retryHeader != null) {
            try {
                // 假设 retry-count 是以字符串形式存的
                String value = new String(retryHeader.value(), StandardCharsets.UTF_8);
                return Integer.parseInt(value);
            } catch (Exception e) {
                // 解析失败默认返回 0
                return 0;
            }
        }
        return 0;
    }
}

🔄 事务与幂等生产

java 复制代码
package com.example.kafka.config;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaProducerConfig {

    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 可选配置
        configProps.put(ProducerConfig.ACKS_CONFIG, "all");
        configProps.put(ProducerConfig.RETRIES_CONFIG, 3);

        // 开启事务
        // 保证幂等性
//        configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        // 唯一事务 ID
//        configProps.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "txn-producer-1");
        return new DefaultKafkaProducerFactory<>(configProps);
    }

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

}
java 复制代码
/**
     * Kafka支持事务性消息
     *
     * @param consumeRecord 消费者记录
     * @param ack           手动确认对象
     */
    /*
        Kafka 能做到的是"事务性生产 + 最终一致性消费",不等于数据库级别的全局分布式事务。如果你要跨 Kafka 与数据库、Redis、HTTP 服务等实现一致性,请考虑:
        ✅ 幂等性设计(业务唯一 key)
        ✅ 手动提交 offset(控制消费确认时机)
        ✅ 本地消息表 + 补偿机制(适合 DDD/Event Sourcing)
        ✅ 事务出错转 DLQ + 监控
     */
    @KafkaListener(topics = "transactional-topic", groupId = "transactional-group")
    public void consumeTransactional(ConsumerRecord<String, String> consumeRecord, Acknowledgment ack) {
        // 开启事务
        kafkaTemplate.executeInTransaction(operations -> {
            try {
                log.info("处理事务消息,主题:{},key:{},value: {}",
                        consumeRecord.topic(), consumeRecord.key(), consumeRecord.value());
                // 业务处理
                // .......
                // 发送到其他主题
                operations.send("processed-topic1", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                operations.send("processed-topic2", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                operations.send("processed-topic3", consumeRecord.key(), consumeRecord.value()); // ✅ 第一步:发送到其他主题
                // 手动提交偏移量(事务中会自动处理)
                ack.acknowledge();  // ✅ 第二步:提交 offset,表示成功处理

                return true; // 事务成功
            } catch (Exception e) {
                log.error("事务失败,失败原因:{}", e.getMessage());
                // 事务回滚
                throw new RuntimeException(e);
            }
        });
    }

📦 批量消费与并发消费

java 复制代码
 /**
     * 监听批量消息
     *
     * @param consumeRecords 批量消息记录
     * @param ack            手动确认对象(当配置为手动确认时使用)
     */
    /*
      containerFactory = "kafkaListenerContainerFactory":使用哪个 Kafka 监听容器工厂(ConcurrentKafkaListenerContainerFactory)来创建消费者监听容器。
      该容器 在 KafkaConsumerConfig 配置中配置了批量消费,因此这里使用 kafkaListenerContainerFactory
     */
    @KafkaListener(topics = "my-batch", containerFactory = "kafkaListenerContainerFactory")
    public void listenBatchMessages(List<ConsumerRecord<String, String>> consumeRecords, Acknowledgment ack) {
        try {
            log.info("批量消息数量: {}", consumeRecords.size());
            for (ConsumerRecord<String, String> consumeRecord : consumeRecords) {
                log.info("监听单个消息,主题:{},key:{},value: {},偏移量:{},分区:{}",
                        consumeRecord.topic(), consumeRecord.key(), consumeRecord.value(), consumeRecord.offset(), consumeRecord.partition());
                // 处理逻辑 ...
            }
            // 处理成功后手动 ack
            ack.acknowledge();
        } catch (Exception e) {
            log.error("处理失败,提交 offset,失败原因:{}", e.getMessage());
            // 不调用 ack.acknowledge(),Kafka 将在下一次重新投递
        }
    }

⚡ Kafka Streams 实时处理

java 复制代码
package com.example.kafka.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Produced;
import org.apache.kafka.streams.kstream.TimeWindows;

import java.time.Duration;

/**
 *  Kafka Streams 实现 日志实时聚合
 *  每分钟统计一次不同日志级别(如 INFO、ERROR、WARN)的日志数量,并输出到 Kafka 的新主题 log-level-stats,同时打印控制台信息。
 * @author wkj
 */
//@Configuration
// EnableKafkaStreams:启用 Spring Kafka Streams 功能
//@EnableKafkaStreams
public class KafkaStreamConfig {

    /**
     * 定义一个 Kafka 流处理逻辑的 Bean,处理来自 Kafka 的日志流(KStream)。
     * StreamsBuilder 是 Kafka Streams 构建处理拓扑的类。
     */
//    @Bean
    public KStream<String, String> kStream(StreamsBuilder builder) {
        // 从名为 application-logs 的 Kafka topic 中读取数据(格式为 JSON 字符串)
        KStream<String, String> stream = builder.stream("application-logs");

        ObjectMapper objectMapper = new ObjectMapper();

        // 提取 level 字段并统计
        stream
                .map((key, value) -> {
                    try {
                        JsonNode node = objectMapper.readTree(value);
                        // 提取 "level" 字段
                        String level = node.get("level").asText();
                        // 返回键为 level,值为 1
                        return new KeyValue<>(level, "1");
                    } catch (Exception e) {
                        // 解析失败则标记为 UNKNOWN
                        return new KeyValue<>("UNKNOWN", "1");
                    }
                })
                // 分组 + 滑动窗口聚合统计
                // .groupByKey():按日志级别(INFO、ERROR 等)分组。
                .groupByKey()
                // .windowedBy(...):基于时间窗口(这里是 1 分钟)统计。
                .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(1)))
                // .count():对每个日志级别在每个 1 分钟内出现的次数进行计数。
                .count()
                .toStream()
                // 输出控制台调试信息:每分钟内每种日志级别的数量。
                .peek((key, count) -> System.out.printf("窗口[%s] 等级[%s] 数量: %d%n",
                        key.window().startTime(),
                        key.key(),
                        count))
                // 写回 Kafka:发送到 log-level-stats 主题
                .map((key, count) -> new KeyValue<>(key.key(), count.toString()))
                // 写入到 Kafka 的 log-level-stats topic 中,供下游消费或存入 Elasticsearch。
                .to("log-level-stats", Produced.with(Serdes.String(), Serdes.String()));

        // 返回源数据流(不是聚合流),通常用于后续处理或测试,不影响核心逻辑。
        return stream;
    }
}

⏰ 延迟消息 & Dead Letter Queue

java 复制代码
    @KafkaListener(topics = "dead-letter-topic", groupId = "dlt-group")
    public void listenDLT(ConsumerRecord<String, String> consumeRecord) {
        log.warn("死信队列接收到失败消息:Key:{},value:{}", consumeRecord.key(), consumeRecord.value());
    }

🛠 消息序列化与自定义对象传输

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO implements Serializable {
    private String username;
    private int age;
}

@Autowired
private KafkaTemplate<String, UserDTO> userTemplate;

userTemplate.send("user-topic", new UserDTO("Alice", 20));

🧪 测试与调试

  1. 使用 kafka-console-producer.shkafka-console-consumer.sh 验证消息发送接收
  2. 使用 Postman 或 Controller 调用生产者接口发送消息
  3. 监控 Kafka UI(如 Kafka Manager、CMAK)

✅ 总结

本文提供了 Spring Boot + Kafka 的全面实战案例,覆盖:

  • 普通生产者/消费者
  • 批量与并发消费
  • 事务 & 幂等生产
  • Kafka Streams 流处理
  • 延迟消息 & 死信队列
  • 自定义对象消息 & JSON

本教程可直接用于生产环境参考,覆盖市面上 99% Kafka 使用场景。


🔄 源码

源码下载

相关推荐
卓怡学长18 小时前
w1基于springboot高校学生评教系统
java·spring boot·tomcat·maven·intellij-idea
talen_hx29618 小时前
《kafka核心源码解读》学习笔记 Day 02
笔记·学习·kafka
lifallen18 小时前
如何保证 Kafka 的消息顺序性?
java·大数据·分布式·kafka
真实的菜18 小时前
Kafka 2.x vs 3.x,我为什么选择升级?
kafka
时光追逐者18 小时前
分享四款开源且实用的 Kafka 管理工具
分布式·kafka·开源
Rick199318 小时前
rabbitmq, rocketmq, kafka这三种消息如何分别保住可靠性,顺序性,以及应用场景?
kafka·rabbitmq·rocketmq
大佐不会说日语~18 小时前
Spring AI Alibaba 的 Function Calling 使用 @Tool 调用中,无法获取用户ID踩坑记录
java·人工智能·spring boot·spring·alibaba·function
阿丰资源19 小时前
java项目-基于SpringBoot+MySQL+Vue的前后端分离宠物商店系统(附资料)
java·spring boot·mysql
☞遠航☜21 小时前
kafka快速上手
分布式·kafka·linq
java1234_小锋1 天前
Java高频面试题:Springboot的自动配置原理?
java·spring boot·面试