Kafka 实战
适合人群:后端开发工程师、架构师、大数据工程师
本文从 Kafka 核心概念讲起,结合 Java 代码实战,覆盖生产者/消费者开发、消息可靠性、Spring Boot 集成、Kafka Streams、性能调优等进阶内容,带你从零掌握 Kafka 这把分布式消息队列利器。
目录
- [1. Kafka 简介](#1. Kafka 简介)
- [2. 核心概念与架构](#2. 核心概念与架构)
- [3. 环境搭建](#3. 环境搭建)
- [4. Java 客户端依赖与配置](#4. Java 客户端依赖与配置)
- [5. 生产者实战](#5. 生产者实战)
- [6. 消费者实战](#6. 消费者实战)
- [7. 分区策略与消息路由](#7. 分区策略与消息路由)
- [8. 序列化与自定义序列化器](#8. 序列化与自定义序列化器)
- [9. 消费者组与 Rebalance](#9. 消费者组与 Rebalance)
- [10. 消息可靠性保障](#10. 消息可靠性保障)
- [11. Spring Boot 集成 Kafka](#11. Spring Boot 集成 Kafka)
- [12. Kafka Streams 实战](#12. Kafka Streams 实战)
- [13. 性能调优](#13. 性能调优)
- [14. 监控与运维](#14. 监控与运维)
- [15. 最佳实践与常见问题](#15. 最佳实践与常见问题)
1. Kafka 简介
1.1 什么是 Kafka
Apache Kafka 最初由 LinkedIn 开发,后捐赠给 Apache 基金会,现为顶级开源项目。它是一个分布式流处理平台 ,本质上是分布式发布-订阅消息系统。
1.2 Kafka 的核心特点
- 高吞吐量:单集群每秒可处理百万级消息,普通硬件即可达到 GB/s 级吞吐
- 低延迟:毫秒级消息延迟
- 可扩展性:支持水平扩展,可轻松扩展到数百个 Broker
- 持久化:消息持久化到磁盘,支持多副本,数据不丢失
- 高可用:支持副本机制和故障自动转移
- 多生产者/多消费者:多个生产者写入,多个消费者独立读取
- 顺序保证:单分区内消息有序
1.3 Kafka 的应用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 消息队列 | 异步解耦、削峰填谷 | 订单系统 → 消息队列 → 库存/积分/通知系统 |
| 日志收集 | 统一收集分布式系统日志 | 各服务日志 → Kafka → ES/HDFS |
| 流处理 | 实时数据处理 | 实时计算 UV/PV、实时风控 |
| 事件溯源 | 记录状态变更事件 | CQRS 架构中的事件存储 |
| 数据管道 | 系统间数据同步 | MySQL CDC → Kafka → 数据仓库 |
| 指标监控 | 系统指标采集上报 | 服务器指标 → Kafka → Grafana |
1.4 Kafka vs 其他消息队列
| 特性 | Kafka | RabbitMQ | RocketMQ | Pulsar |
|---|---|---|---|---|
| 吞吐量 | 极高(百万/s) | 中等(万级/s) | 高(十万/s) | 极高 |
| 延迟 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 |
| 持久化 | 磁盘 + 副本 | 可选 | 磁盘 + 副本 | 分离存储 |
| 顺序保证 | 分区内有序 | 队列内有序 | 分区内有序 | 分区内有序 |
| 事务 | 支持 | 支持 | 支持 | 支持 |
| 消息回溯 | 支持(offset) | 不支持 | 支持 | 支持 |
| 适用场景 | 日志/流处理/大数据 | 业务消息/低延迟 | 金融/电商业务 | 云原生/多租户 |
2. 核心概念与架构
2.1 整体架构
┌─────────────────────────────────────────┐
│ ZooKeeper / KRaft │
│ (集群协调、元数据管理) │
└──────────┬──────────────────┬───────────┘
│ │
┌──────────┐ ┌──────┴──────┐ ┌───────┴─────┐ ┌──────────┐
│ Producer │───写入消息──────│ Broker 1 │ │ Broker 2 │ │ Broker 3 │
│ (生产者) │ │ ┌────────┐ │ │ ┌────────┐ │ │┌────────┐│
└──────────┘ │ │Topic-A │ │ │ │Topic-A │ │ ││Topic-A ││
│ │P0(Leader)│ │ │ │P1(Leader)│ │ ││P2(Leader)│
┌──────────┐ │ │P2(Follower)│ │ │P0(Follower)│ ││P1(Follower)│
│ Consumer │ │ └────────┘ │ │ └────────┘ │ │└────────┘│
│ Group 1 │───读取消息──────│ │ │ │ │ │
└──────────┘ └─────────────┘ └────────────┘ └──────────┘
┌──────────┐
│ Consumer │
│ Group 2 │───独立消费──────→ (每个Group独立消费同一Topic的全量消息)
└──────────┘
2.2 核心概念详解
2.2.1 Broker(代理)
Kafka 集群中的一台服务器就是一个 Broker。多个 Broker 组成集群,每个 Broker 负责存储部分分区数据。
2.2.2 Topic(主题)
消息的逻辑分类,类似于数据库中的表。生产者将消息发送到 Topic,消费者从 Topic 订阅消息。
Topic: order-events
├── Partition 0: [msg0, msg1, msg4, msg7, ...]
├── Partition 1: [msg2, msg3, msg5, msg8, ...]
└── Partition 2: [msg6, msg9, msg10, ...]
2.2.3 Partition(分区)
Topic 物理上的分片,是 Kafka 并发处理的基本单位。
-
每个 Partition 是一个有序的、不可变的追加日志
-
每个 Partition 有一个唯一 Leader 处理读写
-
Follower 从 Leader 同步数据,提供冗余
-
分区数决定了最大并行度
Partition 内部结构:
┌─────────────────────────────────────────────────────────┐
│ Partition (有序日志) │
│ │
│ Offset: 0 1 2 3 4 5 │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│ Message: │msg0 │msg1 │msg2 │msg3 │msg4 │msg5 │ ... │
│ └─────┴─────┴─────┴─────┴─────┴─────┘ │
│ ↑ │
│ Consumer Offset (消费位移) │
│ │
│ Log Segment: [msg0~msg2] [msg3~msg5] [msg6~...] │
│ (按大小/时间切分,旧Segment可被删除) │
└─────────────────────────────────────────────────────────┘
2.2.4 Offset(位移)
消息在 Partition 中的唯一编号,从 0 开始单调递增。消费者通过 Offset 记录消费位置。
2.2.5 Replica(副本)
每个 Partition 可以有多个副本,提供数据冗余和高可用。
| 概念 | 说明 |
|---|---|
| Leader Replica | 处理所有读写请求的主副本 |
| Follower Replica | 从 Leader 同步数据的从副本 |
| ISR (In-Sync Replicas) | 与 Leader 保持同步的副本集合 |
| OSR (Out-of-Sync Replicas) | 落后于 Leader 的副本集合 |
| AR (All Replicas) | ISR + OSR = 所有副本 |
2.2.6 Producer(生产者)
向 Kafka Topic 发送消息的客户端。
2.2.7 Consumer(消费者)
从 Kafka Topic 读取消息的客户端。
2.2.8 Consumer Group(消费者组)
消费者组是 Kafka 实现负载均衡 和广播的核心机制:
-
同一组内:一个分区只能被一个消费者消费(负载均衡)
-
不同组间:各自独立消费全量消息(广播)
Topic: order-events (3个分区)
消费者组 A:
Consumer A1 ←── Partition 0
Consumer A2 ←── Partition 1
Consumer A3 ←── Partition 2消费者组 B:
Consumer B1 ←── Partition 0, 1, 2 (一个消费者消费所有分区)消费者组 C:
Consumer C1 ←── Partition 0
Consumer C2 ←── Partition 1, 2
Consumer C3 ←── (空闲,分区不够分)
💡 关键规则:一个消费者组中的消费者数量不应超过分区数,否则多余的消费者会空闲。
2.2.9 ZooKeeper / KRaft
Kafka 依赖协调服务来管理集群元数据:
- ZooKeeper 模式(传统):Kafka 2.x 及之前依赖外部 ZooKeeper
- KRaft 模式(新架构):Kafka 3.x 引入,内置共识协议,摆脱 ZooKeeper 依赖
3. 环境搭建
3.1 Docker Compose 快速搭建
最快的方式是使用 Docker Compose 搭建本地 Kafka 环境。
docker-compose.yml:
yaml
version: '3.8'
services:
# KRaft 模式(无需 ZooKeeper)
kafka:
image: bitnami/kafka:3.7
container_name: kafka
ports:
- "9092:9092" # 外部访问
- "9093:9093" # Controller 通信
environment:
# KRaft 模式配置
KAFKA_CFG_NODE_ID: 1
KAFKA_CFG_PROCESS_ROLES: controller,broker
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CFG_INTER_BROKER_LISTENER_NAME: PLAINTEXT
# 自动创建 Topic
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true"
# 日志保留
KAFKA_CFG_LOG_RETENTION_HOURS: 168
volumes:
- kafka_data:/bitnami/kafka
# Kafka UI 管理界面(可选)
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: kafka-ui
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
depends_on:
- kafka
volumes:
kafka_data:
bash
# 启动
docker-compose up -d
# 查看状态
docker-compose ps
# 访问 Kafka UI: http://localhost:8080
3.2 命令行操作 Topic
bash
# 进入容器
docker exec -it kafka bash
# 创建 Topic(3个分区,2个副本)
kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--topic order-events \
--partitions 3 \
--replication-factor 1
# 查看 Topic 列表
kafka-topics.sh --list --bootstrap-server localhost:9092
# 查看 Topic 详情
kafka-topics.sh --describe --topic order-events --bootstrap-server localhost:9092
# 命令行生产者
kafka-console-producer.sh --topic order-events --bootstrap-server localhost:9092
# 命令行消费者
kafka-console-consumer.sh --topic order-events --from-beginning --bootstrap-server localhost:9092
# 消费者组消费
kafka-console-consumer.sh --topic order-events --group order-consumer-group --bootstrap-server localhost:9092
# 查看消费者组
kafka-consumer-groups.sh --list --bootstrap-server localhost:9092
# 查看消费者组位移
kafka-consumer-groups.sh --describe --group order-consumer-group --bootstrap-server localhost:9092
# 删除 Topic
kafka-topics.sh --delete --topic order-events --bootstrap-server localhost:9092
3.3 Topic 分区数选择
| 考量因素 | 建议 |
|---|---|
| 吞吐量需求 | 分区数 ≈ 目标吞吐量 / 单分区吞吐量 |
| 消费者数 | 分区数 ≥ 消费者组中的消费者数 |
| Broker 数 | 单 Broker 分区数不宜过多(建议 < 4000) |
| 实际经验 | 中小规模 6~12 分区,大规模 24~100 分区 |
⚠️ 分区数一旦确定不能减少,只能增加。增加分区会破坏 Key 的路由一致性,设计时需谨慎。
4. Java 客户端依赖与配置
4.1 Maven 依赖
xml
<dependencies>
<!-- Kafka 客户端 -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.7.0</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- SLF4J 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
</dependency>
</dependencies>
4.2 核心配置参数详解
4.2.1 生产者核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
bootstrap.servers |
- | Kafka Broker 地址列表 |
key.serializer |
- | Key 序列化器 |
value.serializer |
- | Value 序列化器 |
acks |
all | 确认级别:0/1/all |
retries |
2147483647 | 重试次数 |
retry.backoff.ms |
100 | 重试间隔 |
batch.size |
16384 | 批次大小(字节) |
linger.ms |
0 | 批次等待时间(ms) |
buffer.memory |
33554432 | 发送缓冲区大小 |
compression.type |
none | 压缩:none/gzip/snappy/lz4/zstd |
max.in.flight.requests.per.connection |
5 | 每连接未确认请求数 |
enable.idempotence |
true | 幂等性 |
transactional.id |
- | 事务ID |
4.2.2 消费者核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
bootstrap.servers |
- | Kafka Broker 地址列表 |
key.deserializer |
- | Key 反序列化器 |
value.deserializer |
- | Value 反序列化器 |
group.id |
- | 消费者组ID |
enable.auto.commit |
true | 自动提交位移 |
auto.commit.interval.ms |
5000 | 自动提交间隔 |
auto.offset.reset |
latest | 无位移时策略:earliest/latest/none |
max.poll.records |
500 | 单次拉取最大记录数 |
max.poll.interval.ms |
300000 | 两次poll最大间隔 |
session.timeout.ms |
45000 | 会话超时时间 |
heartbeat.interval.ms |
3000 | 心跳间隔 |
fetch.min.bytes |
1 | 最小拉取字节数 |
isolation.level |
read_uncommitted | 事务隔离级别 |
5. 生产者实战
5.1 基础生产者
java
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class BasicProducer {
public static void main(String[] args) {
// 1. 配置生产者
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2. 创建生产者
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
// 3. 发送消息
for (int i = 0; i < 10; i++) {
String key = "order-" + i;
String value = "订单内容 " + i;
ProducerRecord<String, String> record =
new ProducerRecord<>("order-events", key, value);
// 同步发送(阻塞等待结果)
RecordMetadata metadata = producer.send(record).get();
System.out.printf("发送成功: topic=%s, partition=%d, offset=%d, key=%s%n",
metadata.topic(), metadata.partition(), metadata.offset(), key);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5.2 异步发送(推荐)
异步发送性能更高,通过回调处理发送结果:
java
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class AsyncProducer {
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 开启幂等性(防止重复消息)
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 批处理优化
props.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 等待10ms凑批
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32768); // 32KB批次
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); // 压缩
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
for (int i = 0; i < 100; i++) {
String key = "order-" + i;
String value = String.format("{\"orderId\":%d,\"amount\":%.2f}", i, i * 9.9);
ProducerRecord<String, String> record =
new ProducerRecord<>("order-events", key, value);
// 异步发送 + 回调
producer.send(record, (metadata, exception) -> {
if (exception != null) {
System.err.println("发送失败: " + exception.getMessage());
// 这里可以做重试、告警等处理
} else {
System.out.printf("成功: partition=%d, offset=%d%n",
metadata.partition(), metadata.offset());
}
});
}
// 确保所有消息发送完成
producer.flush();
TimeUnit.SECONDS.sleep(2);
}
}
}
5.3 生产者拦截器
拦截器可以在消息发送前后做处理,例如添加统一 Header:
java
import org.apache.kafka.clients.producer.*;
import java.util.Map;
public class TimestampInterceptor implements ProducerInterceptor<String, String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
// 发送前:添加时间戳 Header
long timestamp = System.currentTimeMillis();
return new ProducerRecord<>(
record.topic(), record.partition(),
record.timestamp(), record.key(), record.value(),
record.headers().add("send-timestamp", String.valueOf(timestamp).getBytes())
);
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// 收到确认后:记录发送耗时等
if (exception != null) {
System.err.println("发送异常: " + exception.getMessage());
}
}
@Override
public void close() {}
@Override
public void configure(Map<String, ?> configs) {}
}
配置使用拦截器:
java
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TimestampInterceptor.class.getName());
5.4 发送方式对比
| 方式 | 特点 | 适用场景 |
|---|---|---|
同步发送 (send().get()) |
阻塞等待、低吞吐 | 强一致性要求、少量消息 |
异步发送 (send(callback)) |
非阻塞、高吞吐 | 大部分生产场景(推荐) |
忽略结果 (send()) |
最高吞吐、不保证 | 日志采集等容忍丢失场景 |
6. 消费者实战
6.1 基础消费者
java
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class BasicConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
// 从最早的消息开始消费
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 关闭自动提交,手动控制
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
// 订阅 Topic
consumer.subscribe(Collections.singletonList("order-events"));
// 轮询消费
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息: partition=%d, offset=%d, key=%s, value=%s%n",
record.partition(), record.offset(), record.key(), record.value());
}
// 手动同步提交位移
if (!records.isEmpty()) {
consumer.commitSync();
}
}
}
}
}
6.2 手动异步提交位移
java
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class AsyncCommitConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(Collections.singletonList("order-events"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
try {
processMessage(record);
} catch (Exception e) {
System.err.println("处理失败: " + e.getMessage());
// 根据业务决定是否继续
}
}
// 异步提交位移(不阻塞,性能好)
if (!records.isEmpty()) {
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
System.err.println("位移提交失败: " + exception.getMessage());
}
});
}
}
}
}
private static void processMessage(ConsumerRecord<String, String> record) {
// 模拟业务处理
System.out.printf("处理消息: key=%s, value=%s%n", record.key(), record.value());
}
}
6.3 精确控制位移提交(按分区提交)
java
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
public class PreciseOffsetConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(Collections.singletonList("order-events"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
// 按分区处理,精确控制每个分区的位移
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
processMessage(record);
// 记录每条消息处理后的位移
offsets.put(partition, new OffsetAndMetadata(record.offset() + 1));
}
}
// 按分区精确提交位移
if (!offsets.isEmpty()) {
consumer.commitSync(offsets);
}
}
}
}
private static void processMessage(ConsumerRecord<String, String> record) {
System.out.printf("处理: partition=%d, offset=%d%n", record.partition(), record.offset());
}
}
6.4 位移提交策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自动提交 | 简单方便 | 可能重复消费/丢失 | 容错性高的场景 |
| 同步提交 | 可靠,阻塞等待 | 性能差 | 强一致性要求 |
| 异步提交 | 性能好 | 可能提交失败 | 大部分场景 |
| 按分区提交 | 精确控制 | 代码复杂 | 精确一次性消费 |
7. 分区策略与消息路由
7.1 Kafka 默认分区策略
java
// 默认分区器逻辑(DefaultPartitioner)
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
if (keyBytes == null) {
// 无 Key:轮询(Round-Robin)或 Sticky(粘性)
return stickyPartitionCache.partition(topic, cluster);
} else {
// 有 Key:按 Key 哈希取模
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
| 场景 | 策略 | 说明 |
|---|---|---|
| 有 Key | Hash 分区 | murmur2(key) % 分区数,相同Key到同一分区 |
| 无 Key (3.x之前) | Round-Robin | 轮询分配到各分区 |
| 无 Key (3.x+) | Sticky 粘性 | 批次内粘在同一分区,减少延迟 |
7.2 自定义分区器
按业务规则路由消息到特定分区:
java
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
/**
* 自定义分区器:VIP 用户消息发送到指定分区
*/
public class VipPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
// 最后一个分区专门给 VIP 用户
if (key != null && key.toString().startsWith("VIP-")) {
return numPartitions - 1; // 最后一个分区
}
// VIP 内容:根据会员等级路由
String valueStr = value.toString();
if (valueStr.contains("\"level\":\"gold\"")) {
return numPartitions - 2; // 倒数第二个分区
}
// 普通用户:Hash 分区
if (keyBytes != null) {
return Math.abs(org.apache.kafka.common.utils.Utils.murmur2(keyBytes)) % (numPartitions - 2);
}
// 无 Key:随机
return (int) (Math.random() * (numPartitions - 2));
}
@Override
public void close() {}
@Override
public void configure(Map<String, ?> configs) {}
}
使用自定义分区器:
java
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, VipPartitioner.class.getName());
7.3 指定分区发送
java
// 直接指定分区为 0(忽略 Key 的哈希路由)
ProducerRecord<String, String> record = new ProducerRecord<>(
"order-events", // topic
0, // partition(指定分区)
"order-key", // key
"order-value" // value
);
producer.send(record);
8. 序列化与自定义序列化器
8.1 内置序列化器
| 序列化器 | 说明 |
|---|---|
StringSerializer |
字符串 |
IntegerSerializer |
整数 |
LongSerializer |
长整数 |
ByteArraySerializer |
字节数组 |
ByteBufferSerializer |
ByteBuffer |
8.2 自定义序列化器
定义业务对象:
java
/**
* 订单实体
*/
public class Order {
private long orderId;
private String userId;
private double amount;
private String status;
private long timestamp;
// 构造方法
public Order(long orderId, String userId, double amount, String status) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
this.status = status;
this.timestamp = System.currentTimeMillis();
}
// Getters and Setters
public long getOrderId() { return orderId; }
public void setOrderId(long orderId) { this.orderId = orderId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
自定义序列化器:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;
/**
* Order 自定义序列化器(使用 Jackson 序列化为 JSON)
*/
public class OrderSerializer implements Serializer<Order> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public byte[] serialize(String topic, Order data) {
if (data == null) {
return null;
}
try {
return objectMapper.writeValueAsBytes(data);
} catch (Exception e) {
throw new SerializationException("Order 序列化失败: " + e.getMessage(), e);
}
}
@Override
public void close() {
// 清理资源
}
}
自定义反序列化器:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Deserializer;
/**
* Order 自定义反序列化器
*/
public class OrderDeserializer implements Deserializer<Order> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Order deserialize(String topic, byte[] data) {
if (data == null) {
return null;
}
try {
return objectMapper.readValue(data, Order.class);
} catch (Exception e) {
throw new SerializationException("Order 反序列化失败: " + e.getMessage(), e);
}
}
@Override
public void close() {}
}
8.3 使用自定义序列化器
java
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class OrderProducer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 使用自定义序列化器
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, OrderSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
try (KafkaProducer<String, Order> producer = new KafkaProducer<>(props)) {
for (int i = 0; i < 10; i++) {
Order order = new Order(i, "user-" + i, i * 99.9, "CREATED");
ProducerRecord<String, Order> record =
new ProducerRecord<>("orders", "user-" + i, order);
producer.send(record, (metadata, e) -> {
if (e != null) {
e.printStackTrace();
} else {
System.out.printf("订单发送成功: partition=%d, offset=%d%n",
metadata.partition(), metadata.offset());
}
});
}
}
}
}
💡 生产建议 :实际项目中推荐直接使用
StringSerializer+ JSON 字符串,或使用 Avro/Protobuf 等高效序列化框架配合 Schema Registry。自定义序列化器增加维护成本,仅在特殊需求时使用。
9. 消费者组与 Rebalance
9.1 Rebalance 触发条件
Rebalance(重平衡)是指消费者组中分区的重新分配,触发条件:
| 触发条件 | 说明 |
|---|---|
| 消费者加入组 | 新消费者上线 |
| 消费者离开组 | 消费者下线/崩溃 |
| 消费者崩溃 | 心跳超时(session.timeout.ms) |
| Topic 分区数变化 | 增加分区 |
| 订阅 Topic 变化 | 订阅的 Topic 列表变化 |
9.2 Rebalance 问题与影响
Rebalance 期间消费者无法消费 ,会导致消费暂停。频繁 Rebalance 会严重影响性能:
正常消费: ████ ████ ████ ████ ████
Rebalance: ████ ░░░░░░░░░░░░ ████ ████
↑
消费暂停(Stop The World)
9.3 消费者 Rebalance 监听器
java
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
public class RebalanceListenerConsumer {
// 记录每个分区当前处理到的位移(用于Rebalance恢复)
private static final Map<TopicPartition, Long> currentOffsets = new HashMap<>();
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 自定义 ConsumerRebalanceListener
ConsumerRebalanceListener listener = new ConsumerRebalanceListener() {
/**
* 分区被撤销时调用(Rebalance 开始前)
* 在这里提交已处理但未提交的位移
*/
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("分区被撤销: " + partitions);
// 提交当前处理进度,防止重复消费
if (!currentOffsets.isEmpty()) {
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
currentOffsets.forEach((tp, offset) ->
offsets.put(tp, new OffsetAndMetadata(offset + 1)));
// 使用全局 consumer 引用提交
// consumer.commitSync(offsets);
}
}
/**
* 分区被分配时调用(Rebalance 完成后)
* 可以在这里做初始化工作
*/
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("分区被分配: " + partitions);
currentOffsets.clear();
}
};
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(Collections.singletonList("order-events"), listener);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
processMessage(record);
// 记录处理进度
currentOffsets.put(
new TopicPartition(record.topic(), record.partition()),
record.offset()
);
}
if (!records.isEmpty()) {
consumer.commitAsync();
}
}
}
}
private static void processMessage(ConsumerRecord<String, String> record) {
System.out.printf("处理: partition=%d, offset=%d, value=%s%n",
record.partition(), record.offset(), record.value());
}
}
9.4 避免 Rebalance 的最佳实践
java
Properties props = new Properties();
// 1. 合理设置 session.timeout.ms(避免误判崩溃)
// 默认 45 秒,如果处理慢可适当调大
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 60000);
// 2. 合理设置 heartbeat.interval.ms(通常为 session.timeout 的 1/3)
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 20000);
// 3. 合理设置 max.poll.interval.ms(避免处理超时触发Rebalance)
// 如果单次处理时间长,必须调大此值
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 600000); // 10分钟
// 4. 减少 max.poll.records(减少单次处理量)
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);
// 5. 使用 Sticky 分配策略(减少Rebalance时的分区迁移)
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
"org.apache.kafka.clients.consumer.CooperativeStickyAssignor");
10. 消息可靠性保障
10.1 可靠性三层次
Kafka 提供三种消息可靠性保障层次:
最多一次 (At Most Once) ← 消息可能丢失,不会重复
│
至少一次 (At Least Once) ← 消息不会丢失,可能重复
│
精确一次 (Exactly Once) ← 消息不丢失不重复(最强)
10.2 生产者端配置
10.2.1 acks 确认机制
java
// acks=0: 生产者不等待确认(可能丢失,吞吐最高)
props.put(ProducerConfig.ACKS_CONFIG, "0");
// acks=1: Leader 写入即确认(Leader宕机可能丢失)
props.put(ProducerConfig.ACKS_CONFIG, "1");
// acks=all (或 -1): 所有 ISR 副本确认(最安全,推荐)
props.put(ProducerConfig.ACKS_CONFIG, "all");
acks=all 的写入流程:
Producer ──写入──→ Leader (Broker 1)
│
├──同步──→ Follower (Broker 2) ✓
└──同步──→ Follower (Broker 3) ✓
│
全部确认
↓
Producer ←──ACK───── Leader
10.2.2 幂等性生产者
java
// 开启幂等性(防止网络重试导致重复)
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 幂等性自动保证以下配置:
// - acks = all
// - retries > 0
// - max.in.flight.requests.per.connection <= 5
💡 幂等性通过 PID(Producer ID)+ Sequence Number 实现,保证单分区内不重复。但不能跨分区/跨会话。
10.2.3 事务性生产者(精确一次)
java
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class TransactionalProducer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 开启事务
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "order-tx-producer-1");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
// 初始化事务
producer.initTransactions();
try {
// 开启事务
producer.beginTransaction();
// 事务内发送多条消息
producer.send(new ProducerRecord<>("orders", "order-1", "订单1"));
producer.send(new ProducerRecord<>("orders", "order-2", "订单2"));
producer.send(new ProducerRecord<>("notifications", "notify-1", "通知1"));
// 提交事务
producer.commitTransaction();
System.out.println("事务提交成功");
} catch (Exception e) {
// 异常时中止事务
producer.abortTransaction();
System.err.println("事务已回滚: " + e.getMessage());
}
}
}
}
消费者端需要配置事务隔离级别:
java
// 只读取已提交的事务消息
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
10.3 消费者端配置
java
Properties props = new Properties();
// 关闭自动提交,手动提交(防止消息丢失)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 业务处理完成后才提交位移
// 这样即使处理失败,下次还能重新消费
10.4 消费-处理-提交模式(Exactly-Once 消费)
java
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
/**
* 精确一次消费模式:Kafka 消费 + 业务处理 + 位移提交 原子化
* 结合事务实现 Exactly-Once 语义
*/
public class ExactlyOnceConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 只读取已提交消息
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(Collections.singletonList("orders"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
try {
// 1. 处理业务逻辑
processOrder(record.value());
// 2. 处理成功后提交位移
consumer.commitSync(Collections.singletonMap(
new org.apache.kafka.common.TopicPartition(
record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1)
));
} catch (Exception e) {
// 3. 处理失败,不提交位移,下次重新消费
System.err.println("处理失败,将重试: " + e.getMessage());
}
}
}
}
}
private static void processOrder(String orderJson) {
// 实际业务处理逻辑
System.out.println("处理订单: " + orderJson);
}
}
10.5 可靠性配置总结
| 组件 | 配置项 | 推荐值 | 作用 |
|---|---|---|---|
| Topic | min.insync.replicas |
2 | 最少同步副本数 |
| Topic | replication.factor |
3 | 副本总数 |
| Producer | acks |
all | 所有副本确认 |
| Producer | enable.idempotence |
true | 幂等防重 |
| Producer | retries |
2147483647 | 无限重试 |
| Producer | max.in.flight.requests.per.connection |
5 | 控制乱序 |
| Consumer | enable.auto.commit |
false | 手动提交 |
| Consumer | isolation.level |
read_committed | 事务隔离 |
11. Spring Boot 集成 Kafka
11.1 添加依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
11.2 配置文件
application.yml:
yaml
spring:
kafka:
bootstrap-servers: localhost:9092
# 生产者配置
producer:
retries: 3
batch-size: 16384
buffer-memory: 33554432
acks: all
properties:
linger.ms: 10
enable.idempotence: true
compression.type: lz4
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
# 消费者配置
consumer:
group-id: order-consumer-group
auto-offset-reset: earliest
enable-auto-commit: false
max-poll-records: 500
properties:
max.poll.interval.ms: 600000
session.timeout.ms: 60000
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "com.example.kafka.model"
# 监听器配置
listener:
ack-mode: manual_immediate
concurrency: 3
11.3 消息模型
java
package com.example.kafka.model;
public class OrderEvent {
private Long orderId;
private String userId;
private Double amount;
private String status;
private Long timestamp;
// 构造方法
public OrderEvent() {}
public OrderEvent(Long orderId, String userId, Double amount, String status) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
this.status = status;
this.timestamp = System.currentTimeMillis();
}
// Getters and Setters
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
@Override
public String toString() {
return "OrderEvent{orderId=" + orderId + ", userId='" + userId +
"', amount=" + amount + ", status='" + status + "'}";
}
}
11.4 生产者服务
java
package com.example.kafka.producer;
import com.example.kafka.model.OrderEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class OrderEventProducer {
private static final Logger log = LoggerFactory.getLogger(OrderEventProducer.class);
private static final String TOPIC = "order-events";
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
/**
* 异步发送订单事件
*/
public void sendOrderEvent(OrderEvent event) {
String key = event.getUserId();
CompletableFuture<SendResult<String, OrderEvent>> future =
kafkaTemplate.send(TOPIC, key, event);
future.whenComplete((result, ex) -> {
if (ex != null) {
log.error("订单事件发送失败: orderId={}", event.getOrderId(), ex);
} else {
log.info("订单事件发送成功: orderId={}, partition={}, offset={}",
event.getOrderId(),
result.getRecordMetadata().partition(),
result.getRecordMetadata().offset());
}
});
}
/**
* 同步发送(阻塞等待)
*/
public void sendOrderEventSync(OrderEvent event) throws Exception {
String key = event.getUserId();
kafkaTemplate.send(TOPIC, key, event).get();
log.info("订单事件同步发送成功: orderId={}", event.getOrderId());
}
/**
* 发送到指定分区
*/
public void sendToPartition(OrderEvent event, int partition) {
kafkaTemplate.send(TOPIC, partition, event.getUserId(), event);
log.info("订单事件发送到分区 {}: orderId={}", partition, event.getOrderId());
}
}
11.5 消费者服务
java
package com.example.kafka.consumer;
import com.example.kafka.model.OrderEvent;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
@Service
public class OrderEventConsumer {
private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class);
/**
* 消费订单事件(手动确认)
*/
@KafkaListener(
topics = "order-events",
groupId = "order-consumer-group",
concurrency = "3"
)
public void consume(OrderEvent event, Acknowledgment ack) {
try {
log.info("收到订单事件: {}", event);
// 业务处理
processOrder(event);
// 手动确认位移
ack.acknowledge();
} catch (Exception e) {
log.error("订单处理失败: {}", event, e);
// 不确认位移,下次重新消费
// 也可以选择确认(跳过坏消息)避免阻塞
}
}
/**
* 获取完整 ConsumerRecord(包含分区、offset等信息)
*/
@KafkaListener(topics = "order-events", groupId = "audit-group")
public void consumeWithMetadata(ConsumerRecord<String, OrderEvent> record,
Acknowledgment ack) {
log.info("收到消息: topic={}, partition={}, offset={}, key={}, value={}",
record.topic(), record.partition(), record.offset(),
record.key(), record.value());
ack.acknowledge();
}
private void processOrder(OrderEvent event) {
// 模拟业务处理
switch (event.getStatus()) {
case "CREATED":
log.info("创建订单: {}", event.getOrderId());
break;
case "PAID":
log.info("订单已支付: {}", event.getOrderId());
break;
case "SHIPPED":
log.info("订单已发货: {}", event.getOrderId());
break;
}
}
}
11.6 REST 接口
java
package com.example.kafka.controller;
import com.example.kafka.model.OrderEvent;
import com.example.kafka.producer.OrderEventProducer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderEventProducer producer;
public OrderController(OrderEventProducer producer) {
this.producer = producer;
}
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody OrderEvent event) {
event.setStatus("CREATED");
producer.sendOrderEvent(event);
return ResponseEntity.ok("订单事件已发送: " + event.getOrderId());
}
}
11.7 ack-mode 配置说明
| ack-mode | 说明 |
|---|---|
record |
每条记录处理完后确认(最安全,性能低) |
batch |
每批 poll 处理完后确认(默认) |
time |
按时间间隔确认 |
count |
按数量确认 |
manual |
手动调用 acknowledgment.acknowledge()(下次 poll 时提交) |
manual_immediate |
手动调用后立即提交(推荐) |
12. Kafka Streams 实战
12.1 Kafka Streams 简介
Kafka Streams 是 Kafka 内置的流处理库,无需额外集群,直接嵌入 Java 应用中。
核心概念:
- Stream:无界、持续的记录流
- Table:有状态的、可更新的数据集合(类似数据库表)
- KTable:每个 Key 保留最新值
- GlobalKTable:全局副本的 KTable
- Windowed:基于时间窗口的聚合
12.2 添加依赖
xml
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>3.7.0</version>
</dependency>
12.3 词频统计实战
java
package com.example.kafka.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.*;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class WordCountStream {
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "word-count-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000);
// 构建流处理拓扑
StreamsBuilder builder = new StreamsBuilder();
// 1. 从 input-topic 读取文本流
KStream<String, String> textLines = builder.stream("text-input-topic");
// 2. 处理流
KTable<String, Long> wordCounts = textLines
// 将每行文本拆分为单词
.flatMapValues(line -> Arrays.asList(line.toLowerCase().split("\\W+")))
// 为每个单词生成 (word, 1) 的键值对
.groupBy((key, word) -> word)
// 按单词计数
.count(Materialized.as("word-counts-store"));
// 3. 将结果写入 output-topic
wordCounts.toStream().to("word-count-output-topic",
Produced.with(Serdes.String(), Serdes.Long()));
// 构建并启动
KafkaStreams streams = new KafkaStreams(builder.build(), props);
// 优雅关闭
CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
streams.close();
latch.countDown();
}));
try {
streams.start();
latch.await();
} catch (Exception e) {
System.exit(1);
}
System.exit(0);
}
}
12.4 窗口聚合实战
统计每 5 分钟内各用户的订单金额:
java
package com.example.kafka.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.*;
import java.time.Duration;
import java.util.Properties;
public class OrderAmountWindowStream {
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "order-amount-window-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
StreamsBuilder builder = new StreamsBuilder();
// 输入: userId -> "amount" (JSON字符串)
KStream<String, String> orders = builder.stream("orders");
// 5分钟滚动窗口,按用户聚合订单金额
KTable<Windowed<String>, Double> windowedAmounts = orders
// 将金额字符串转为 Double
.mapValues(value -> Double.parseDouble(value))
// 按用户分组
.groupByKey(Grouped.with(Serdes.String(), Serdes.Double()))
// 5分钟滚动窗口
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5)))
// 聚合:求总金额
.aggregate(
() -> 0.0, // 初始值
(key, value, aggregate) -> aggregate + value, // 聚合函数
Materialized.with(Serdes.String(), Serdes.Double())
);
// 输出结果
windowedAmounts.toStream().foreach((windowedKey, total) -> {
System.out.printf("用户=%s, 窗口=%s ~ %s, 总金额=%.2f%n",
windowedKey.key(),
windowedKey.window().startTime(),
windowedKey.window().endTime(),
total);
});
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();
}
}
12.5 流式 Join
将订单流与用户信息流 Join:
java
StreamsBuilder builder = new StreamsBuilder();
// 订单流: orderId -> orderJson
KStream<String, String> orders = builder.stream("orders");
// 用户信息表: userId -> userInfoJson
KTable<String, String> users = builder.table("users");
// 按订单中的 userId Join 用户信息
KStream<String, String> enrichedOrders = orders
.selectKey((orderId, order) -> extractUserId(order)) // 重新按userId分组
.join(users, (order, user) -> {
// 将订单和用户信息合并
return mergeOrderWithUser(order, user);
});
enrichedOrders.to("enriched-orders");
12.6 窗口类型对比
| 窗口类型 | 说明 | 场景 |
|---|---|---|
| Tumbling Window(滚动) | 固定大小、不重叠 | 每5分钟统计一次 |
| Hopping Window(跳跃) | 固定大小、可重叠 | 每1分钟统计过去5分钟数据 |
| Session Window(会话) | 基于活动间隔 | 用户会话分析 |
13. 性能调优
13.1 生产者调优
java
Properties props = new Properties();
// === 吞吐量优化 ===
// 批次大小(默认16KB,调大到64KB~128KB)
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 65536);
// 批次等待时间(默认0,设为5~20ms让消息凑批)
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
// 发送缓冲区(默认32MB,大数据量可调大)
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 67108864); // 64MB
// 压缩(lz4 综合性能最佳,zstd 压缩率最高)
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
// === 延迟优化 ===
// 减小 linger.ms(立即发送,不等待凑批)
props.put(ProducerConfig.LINGER_MS_CONFIG, 0);
// 减小 batch.size
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
13.2 消费者调优
java
Properties props = new Properties();
// === 吞吐量优化 ===
// 单次拉取最大记录数(默认500,调大到1000~2000)
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1000);
// 最小拉取字节数(默认1B,调大减少小批次)
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024 * 10); // 10KB
// 最大等待时间(配合 fetch.min.bytes,攒够数据或超时返回)
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);
// 最大拉取字节数(默认50MB)
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, 52428800);
// === 并发优化 ===
// 增加分区数和消费者数
// 增加消费者 concurrency(Spring Boot 中 listener.concurrency)
13.3 Broker 端调优
properties
# server.properties
# 网络线程数(处理请求,默认3)
num.network.threads=8
# IO 线程数(磁盘读写,默认8)
num.io.threads=16
# Socket 缓冲区
socket.send.buffer.bytes=1048576
socket.receive.buffer.bytes=1048576
socket.request.max.bytes=104857600
# 日志刷盘策略(权衡性能与可靠性)
# 方式一:按消息条数刷盘
log.flush.interval.messages=10000
# 方式二:按时间刷盘
log.flush.interval.ms=1000
# 日志段大小(默认1GB,大日志段减少文件数)
log.segment.bytes=1073741824
# 日志保留时间(默认7天)
log.retention.hours=168
# 压缩清理(针对 compact 策略的 topic)
log.cleaner.threads=4
13.4 JVM 调优
bash
# Kafka Broker JVM 推荐(16GB 内存机器)
export KAFKA_HEAP_OPTS="-Xms8g -Xmx8g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
export KAFKA_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true"
13.5 调优决策矩阵
| 目标 | 生产者 | 消费者 | Broker |
|---|---|---|---|
| 高吞吐 | ↑batch.size, ↑linger.ms, lz4压缩 | ↑max.poll.records, ↑fetch.min.bytes | ↑num.io.threads |
| 低延迟 | ↓linger.ms=0, ↓batch.size | ↑fetch.min.bytes | ↑num.network.threads |
| 高可靠 | acks=all, ↑retries, 幂等 | 手动提交, read_committed | ↑replication.factor, ↑min.insync.replicas |
14. 监控与运维
14.1 JMX 指标监控
Kafka 通过 JMX 暴露丰富的监控指标:
bash
# 启动时开启 JMX
export JMX_PORT=9999
kafka-server-start.sh server.properties
# 使用 jconsole / jvisualvm 连接
# 或使用命令行工具
kafka-run-class.sh kafka.tools.JmxTool \
--object-name kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec \
--jmx-url service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi \
--attributes OneMinuteRate \
--reporting-interval 2000
14.2 核心监控指标
14.2.1 Broker 指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
BytesInPerSec |
每秒入站字节 | - |
BytesOutPerSec |
每秒出站字节 | - |
MessagesInPerSec |
每秒消息数 | - |
UnderReplicatedPartitions |
未同步分区数 | > 0 |
OfflinePartitionsCount |
离线分区数 | > 0(严重) |
ActiveControllerCount |
活跃Controller数 | 必须 = 1 |
RequestHandlerAvgIdlePercent |
请求处理空闲率 | < 0.3 |
DiskUsage |
磁盘使用率 | > 85% |
14.2.2 生产者指标
| 指标 | 说明 |
|---|---|
record-send-rate |
每秒发送记录数 |
record-error-rate |
每秒错误记录数 |
request-latency-avg |
平均请求延迟 |
batch-size-avg |
平均批次大小 |
compression-rate-avg |
平均压缩率 |
14.2.3 消费者指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
records-lag-max |
最大消费延迟 | 需根据业务设定 |
records-consumed-rate |
每秒消费记录数 | - |
fetch-latency-avg |
平均拉取延迟 | - |
commit-latency-avg |
平均提交延迟 | - |
14.3 监控方案
Kafka (JMX) ──→ JMX Exporter ──→ Prometheus ──→ Grafana
↑
告警: Alertmanager
Prometheus + Grafana 部署(docker-compose 补充):
yaml
services:
jmx-exporter:
image: bitnami/jmx-exporter:latest
ports:
- "5556:5556"
environment:
JMX_PORT: 9999
TARGET_HOST: kafka
14.4 常用运维命令
bash
# === Topic 管理 ===
# 增加 Topic 分区数(只能增加)
kafka-topics.sh --alter --bootstrap-server localhost:9092 \
--topic order-events --partitions 6
# 修改 Topic 配置
kafka-configs.sh --alter --bootstrap-server localhost:9092 \
--entity-type topics --entity-name order-events \
--add-config retention.ms=86400000 # 保留1天
# === 消费者组管理 ===
# 查看所有消费者组
kafka-consumer-groups.sh --list --bootstrap-server localhost:9092
# 查看消费者组详情(Lag 消费延迟)
kafka-consumer-groups.sh --describe --group order-consumer-group \
--bootstrap-server localhost:9092
# 重置消费者位移到最早
kafka-consumer-groups.sh --reset-offsets --group order-consumer-group \
--topic order-events --to-earliest --execute \
--bootstrap-server localhost:9092
# 重置位移到指定时间
kafka-consumer-groups.sh --reset-offsets --group order-consumer-group \
--topic order-events --to-datetime 2025-06-19T00:00:00.000 --execute \
--bootstrap-server localhost:9092
# 重置位移到指定 offset
kafka-consumer-groups.sh --reset-offsets --group order-consumer-group \
--topic order-events:0:100 --to-offset 100 --execute \
--bootstrap-server localhost:9092
# === 查询消息 ===
# 按时间查询 offset
kafka-get-offsets.sh --bootstrap-server localhost:9092 \
--topic order-events --time -1 # -1=最新, -2=最早
# 查看指定分区的消息(需配合工具)
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic order-events --partition 0 --offset 100 --max-messages 10
14.5 消费延迟(Lag)处理
# 消费者组 Lag 示例输出
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
order-events 0 10000 10500 500
order-events 1 9800 9800 0
order-events 2 9900 10300 400
Lag 持续增长的处理方案:
- 增加消费者数量(确保 < 分区数)
- 增大
max.poll.records - 多线程处理(注意位移提交的线程安全)
- 增加分区数(需同步增加消费者)
- 优化业务处理逻辑(异步化、批量处理)
15. 最佳实践与常见问题
15.1 Topic 设计最佳实践
- 命名规范 :
<环境>.<域>.<事件类型>,如prod.order.events - 分区数:提前规划,宁多勿少(但不超过 Broker 承受能力)
- 副本数:生产环境至少 3 副本
- min.insync.replicas:设为 2(配合 acks=all)
- 保留策略 :按业务需求设置
retention.ms或retention.bytes
15.2 生产者最佳实践
java
// 1. 使用异步发送
producer.send(record, callback);
// 2. 合理设置重试,防止消息丢失
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120000); // 2分钟超时
// 3. 开启幂等性(默认已开启)
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 4. 有 Key 保证同业务实体消息有序
new ProducerRecord<>("orders", userId, orderJson);
// 5. 使用 try-with-resources 确保关闭
try (KafkaProducer<K, V> producer = new KafkaProducer<>(props)) {
// ...
}
// 6. 关闭前 flush
producer.flush();
producer.close(Duration.ofSeconds(10));
15.3 消费者最佳实践
java
// 1. 关闭自动提交,手动控制位移
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 2. 合理设置 max.poll.interval.ms(处理时间上限)
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 300000);
// 3. 处理失败的消息(死信队列模式)
try {
processMessage(record);
ack.acknowledge();
} catch (Exception e) {
// 发送到死信队列
deadLetterProducer.send(new ProducerRecord<>("dlq-order-events",
record.key(), record.value()));
// 确认原消息(跳过)
ack.acknowledge();
}
// 4. 使用多线程消费(注意位移安全)
// 推荐方案:每个线程一个消费者实例
15.4 多线程消费者实现
java
package com.example.kafka.consumer;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
/**
* 多线程消费者:每个线程独立消费
* 推荐做法:一个消费者对应一个处理线程
*/
public class MultiThreadConsumer {
public static void main(String[] args) {
int numConsumers = 3;
ExecutorService executor = Executors.newFixedThreadPool(numConsumers);
for (int i = 0; i < numConsumers; i++) {
executor.submit(new ConsumerWorker("order-consumer-group", i));
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
executor.shutdown();
try {
executor.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
}
static class ConsumerWorker implements Runnable {
private final String groupId;
private final int id;
ConsumerWorker(String groupId, int id) {
this.groupId = groupId;
this.id = id;
}
@Override
public void run() {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(Collections.singletonList("order-events"));
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("[Consumer-%d] partition=%d, offset=%d, value=%s%n",
id, record.partition(), record.offset(), record.value());
}
if (!records.isEmpty()) {
consumer.commitSync();
}
}
}
}
}
}
15.5 常见问题排查
Q1: 消费者 Lag 持续增大
原因:消费速度 < 生产速度
解决:
- 增加消费者实例(需确保分区数足够)
- 增大
max.poll.records - 优化业务处理逻辑(批量、异步)
- 多线程处理 + 分区级位移管理
Q2: 消息丢失
排查方向:
生产者端 消费者端
│ │
├─ acks=0/1? → 改为 all ├─ 自动提交位移?
├─ 未重试? → 设置 retries │ → 处理未完成就提交了
├─ 缓冲区满丢消息? ├─ poll 后异步处理?
│ → 增加 buffer.memory │ → 异步处理失败丢失
└─ Producer 关闭未 flush? └─ → 改为同步处理或回调提交
→ 添加 producer.flush()
Q3: 消息重复
原因:生产者重试 / 消费者 Rebalance 重复消费
解决:
- 生产者开启幂等性
enable.idempotence=true - 消费者实现幂等处理(数据库唯一约束 / Redis 去重)
- 使用事务实现 Exactly-Once
java
// 消费者幂等处理示例
public void processMessage(ConsumerRecord<String, String> record) {
String messageId = extractMessageId(record.value());
// 使用 Redis 去重
if (redis.setIfAbsent("msg:processed:" + messageId, "1", 24, TimeUnit.HOURS)) {
// 首次处理
doBusinessLogic(record.value());
} else {
// 已处理过,跳过
log.info("消息已处理过,跳过: {}", messageId);
}
}
Q4: Rebalance 频繁
原因:消费者频繁上下线 / 心跳超时 / 处理超时
解决:
- 调大
session.timeout.ms和max.poll.interval.ms - 减小
max.poll.records,降低单次处理量 - 使用 CooperativeStickyAssignor 减少分区迁移
- 确保消费者处理逻辑稳定,不长时间阻塞
Q5: 消息顺序性保证
场景:同一订单的状态变更必须有序(创建→支付→发货)
解决方案:
java
// 生产者:使用 orderId 作为 Key,保证同一订单到同一分区
producer.send(new ProducerRecord<>("order-events", orderId, orderJson));
// 同一分区内消息严格有序
// 注意:开启幂等性时 max.in.flight.requests.per.connection <= 5 仍保证有序
// 如果未开启幂等性,重试可能导致乱序,需设为 1
⚠️ Kafka 只保证单分区内有序,跨分区不保证。如果需要全局有序,只能用单分区(牺牲并发)。
Q6: 如何实现延迟队列
Kafka 原生不支持延迟队列,可通过以下方式实现:
方案:多级延迟 Topic
Producer ──→ delay-30s-topic (retention=30s)
│ (30秒后过期)
↓
delay-60s-topic (retention=60s)
│
↓
delay-5min-topic (retention=5min)
│
↓
target-topic
java
// 消费者消费延迟 Topic,通过 seek 到指定时间点的消息
consumer.subscribe(Collections.singletonList("delay-30s-topic"));
// 利用 Kafka 的时间索引特性
// 消息写入后 30 秒才被消费者读取(通过轮询 + 时间判断)
15.6 学习路径建议
入门阶段
├── 理解消息队列基本概念
├── Docker 搭建本地 Kafka 环境
├── 命令行操作 Topic、生产/消费消息
└── Java 客户端基础生产者/消费者
↓
进阶阶段
├── 理解分区、副本、消费者组机制
├── 掌握序列化、分区策略
├── 掌握位移管理与精确提交
├── 掌握消息可靠性(acks、幂等、事务)
└── Spring Boot 集成
↓
高级阶段
├── Kafka Streams 流处理
├── 性能调优(生产者/消费者/Broker)
├── 监控运维(JMX、Prometheus、Grafana)
├── 故障排查与问题定位
└── Kafka 集群规划与容量评估
附录
A. Kafka 配置速查
生产者推荐配置
properties
bootstrap.servers=localhost:9092
acks=all
retries=2147483647
enable.idempotence=true
max.in.flight.requests.per.connection=5
batch.size=65536
linger.ms=10
buffer.memory=67108864
compression.type=lz4
delivery.timeout.ms=120000
request.timeout.ms=30000
消费者推荐配置
properties
bootstrap.servers=localhost:9092
group.id=my-consumer-group
enable.auto.commit=false
auto.offset.reset=earliest
max.poll.records=500
max.poll.interval.ms=300000
session.timeout.ms=45000
heartbeat.interval.ms=15000
fetch.min.bytes=10240
isolation.level=read_committed
Topic 推荐配置
properties
# 3副本,最少2副本同步
replication.factor=3
min.insync.replicas=2
# 保留7天
retention.ms=604800000
# 单个日志段1GB
segment.bytes=1073741824
B. 常用 Kafka 命令速查
bash
# Topic 管理
kafka-topics.sh --create --bootstrap-server localhost:9092 --topic <topic> --partitions <n> --replication-factor <n>
kafka-topics.sh --list --bootstrap-server localhost:9092
kafka-topics.sh --describe --topic <topic> --bootstrap-server localhost:9092
kafka-topics.sh --alter --topic <topic> --partitions <n> --bootstrap-server localhost:9092
kafka-topics.sh --delete --topic <topic> --bootstrap-server localhost:9092
# 生产/消费
kafka-console-producer.sh --topic <topic> --bootstrap-server localhost:9092
kafka-console-consumer.sh --topic <topic> --from-beginning --bootstrap-server localhost:9092
kafka-console-consumer.sh --topic <topic> --group <group> --bootstrap-server localhost:9092
# 消费者组
kafka-consumer-groups.sh --list --bootstrap-server localhost:9092
kafka-consumer-groups.sh --describe --group <group> --bootstrap-server localhost:9092
kafka-consumer-groups.sh --reset-offsets --group <group> --topic <topic> --to-earliest --execute --bootstrap-server localhost:9092
# 配置管理
kafka-configs.sh --describe --entity-type topics --entity-name <topic> --bootstrap-server localhost:9092
kafka-configs.sh --alter --entity-type topics --entity-name <topic> --add-config retention.ms=86400000 --bootstrap-server localhost:9092
# Offset 查询
kafka-get-offsets.sh --topic <topic> --time -1 --bootstrap-server localhost:9092
C. 推荐学习资源
结语 :Kafka 作为分布式消息队列的事实标准,在大数据和微服务架构中扮演着关键角色。掌握 Kafka 不仅是学会使用 API,更重要的是理解其背后的分区、副本、消费者组等核心机制。希望本文的 Java 实战示例能帮助你在实际项目中游刃有余地使用 Kafka。记住:可靠性 > 性能 > 功能,在保证消息可靠的前提下追求性能,这才是工程实践的正确姿势。