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 使用场景。


🔄 源码

源码下载

相关推荐
哲此一生9841 天前
SpringBoot3集成Mybatis(开启第一个集成Mybatis的后端接口)
java·spring boot·mybatis
九转苍翎1 天前
Java外功精要(3)——Spring配置文件和mybatis
spring boot·mybatis
摇滚侠1 天前
Spring Boot 3零基础教程,Spring Boot 日志级别,笔记19
java·spring boot·笔记
zl9798991 天前
SpringBoot-配置文件yaml
java·spring boot·spring
thginWalker1 天前
使用Spring Boot构建系统监控层
spring boot
沉默终止1 天前
Kafka Queue: 如何严格控制消息数量
kafka
凤山老林1 天前
SpringBoot 启动时执行某些操作的 8 种方式
java·开发语言·spring boot·后端
PHP源码1 天前
SpringBoot房屋租赁系统
spring boot·springboot房屋租赁·java房屋租赁系统·layui房屋租赁·springboot租房系统·java租房系统