一、Kafka 是什么,为什么要用它
在系统越来越复杂之后,你会遇到一个问题:A 系统要通知 B、C、D 系统。最原始的做法是 A 直接调用 B、C、D 的接口,但这带来了几个麻烦:
- B 挂了,A 的请求就失败了,两者强耦合
- 同时调 B、C、D,A 要等三个都响应才能结束,性能差
- 以后再来个 E 系统,要改 A 的代码
Kafka 是一个分布式消息队列,它让 A 只管"把消息扔进去",B、C、D 各自订阅,各自消费,互不干扰。
传统调用链:
A → B(同步等待)
A → C(同步等待)
A → D(同步等待)
引入 Kafka:
A → [Kafka Topic] → B(异步,随时消费)
→ C(异步,随时消费)
→ D(异步,随时消费)
Kafka 的三大核心价值:
- 解耦:生产者和消费者互不知道对方的存在
- 削峰:突发流量先缓冲在 Kafka,消费者按自己的节奏处理
- 异步:生产者投完消息立刻返回,不等消费者处理结果
二、核心概念,先理清再写代码
2.1 五个必须懂的概念
Topic(主题)
消息的分类标签,相当于数据库的表名。生产者往 Topic 写,消费者从 Topic 读。一个系统可以有很多个 Topic,例如 order-created、payment-success、user-register。
Partition(分区)
一个 Topic 可以切成多个 Partition,分散存储在不同机器上,这是 Kafka 高吞吐的关键。每个 Partition 内部消息严格有序,但 Partition 之间无序。
Topic: order-created
├── Partition 0: [消息1, 消息4, 消息7...]
├── Partition 1: [消息2, 消息5, 消息8...]
└── Partition 2: [消息3, 消息6, 消息9...]
Offset(偏移量)
每条消息在 Partition 内的唯一编号,从 0 开始递增。消费者通过 Offset 记录自己"消费到哪里了",这也是 Kafka 可以重放消息的原因。
Producer(生产者)
往 Kafka 写消息的一方。
Consumer & Consumer Group(消费者和消费者组)
从 Kafka 读消息的一方。多个消费者可以组成一个 Consumer Group,同一 Group 内的消费者分摊消费 Partition,实现负载均衡;不同 Group 之间互不影响,都能收到完整的消息,实现广播。
Topic: order-created(3个Partition)
消费者组A(订单服务):
消费者A1 → 负责 Partition 0
消费者A2 → 负责 Partition 1 和 Partition 2
消费者组B(通知服务):
消费者B1 → 负责全部3个 Partition(独享)
2.2 Broker 和 ZooKeeper/KRaft
Broker 就是 Kafka 的服务器节点,多个 Broker 组成 Kafka 集群。
Kafka 早期依赖 ZooKeeper 做集群元数据管理(记录哪些 Broker 活着、Topic 分区在哪里)。2.8 版本之后引入 KRaft 模式,去掉了 ZooKeeper 依赖,部署更简单。本文示例基于带 ZooKeeper 的经典模式(生产环境更常见),原理一样。
三、本地环境快速搭建
3.1 下载 Kafka
去 kafka.apache.org 下载,推荐 3.x 版本。解压到一个没有中文和空格的路径,例如 D:/kafka。
3.2 启动 ZooKeeper(Windows)
# 在 kafka 目录下
.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties
3.2 启动 ZooKeeper(Linux/Mac)
./bin/zookeeper-server-start.sh ./config/zookeeper.properties
3.3 启动 Kafka Broker
# Windows
.\bin\windows\kafka-server-start.bat .\config\server.properties
# Linux/Mac
./bin/kafka-server-start.sh ./config/server.properties
3.4 创建测试 Topic
# Windows
.\bin\windows\kafka-topics.bat --create --topic order-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
# Linux/Mac
./bin/kafka-topics.sh --create --topic order-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
参数说明:
--partitions 3:创建 3 个分区--replication-factor 1:副本数为 1(单机只能为 1,集群可以设置 2 或 3)
3.5 常用命令速查
# 查看所有 Topic
kafka-topics.sh --list --bootstrap-server localhost:9092
# 查看 Topic 详情(分区、副本情况)
kafka-topics.sh --describe --topic order-topic --bootstrap-server localhost:9092
# 命令行生产消息(测试用)
kafka-console-producer.sh --topic order-topic --bootstrap-server localhost:9092
# 命令行消费消息(从头开始消费)
kafka-console-consumer.sh --topic order-topic --bootstrap-server localhost:9092 --from-beginning
四、Java 原生 API
在引入 Spring Boot 之前,先用原生 API 搞清楚 Kafka 的本质。
4.1 Maven 依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.5.1</version>
</dependency>
4.2 原生 Producer(生产者)
最简单的版本:发一条消息
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class SimpleProducer {
public static void main(String[] args) throws Exception {
// 1. 配置生产者参数
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092"); // Kafka 地址
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // key 序列化
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // value 序列化
// 2. 创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// 3. 创建消息(Topic, Key, Value)
// Key 用于决定消息发往哪个 Partition:相同 Key 一定进同一个 Partition
ProducerRecord<String, String> record =
new ProducerRecord<>("order-topic", "order-001", "{'orderId':1,'amount':99.9}");
// 4. 发送(异步)
producer.send(record);
// 5. 关闭
producer.close();
System.out.println("消息发送完成");
}
}
带回调的版本:知道消息有没有发成功
producer.send(record, (metadata, exception) -> {
if (exception == null) {
// 发送成功
System.out.printf("消息发送成功 → Topic: %s, Partition: %d, Offset: %d%n",
metadata.topic(),
metadata.partition(),
metadata.offset());
} else {
// 发送失败
System.err.println("消息发送失败: " + exception.getMessage());
// 实际业务中:记录失败日志,加入重试队列,触发告警等
}
});
同步发送:等待确认再继续
// get() 会阻塞直到收到 Broker 的确认
RecordMetadata metadata = producer.send(record).get();
System.out.printf("同步发送成功,Offset: %d%n", metadata.offset());
同步 vs 异步:异步吞吐高但不能立即知道结果;同步性能低但可以立即处理失败。业务中根据场景选择,大多数情况用异步+回调。
关键生产者参数
// 消息可靠性配置
props.put("acks", "all");
// "0" = 不等确认,最快但可能丢消息
// "1" = Leader 写入即确认,均衡
// "all" = 所有副本写入才确认,最安全,推荐生产使用
// 失败重试次数
props.put("retries", 3);
// 批量发送:消息积累到 16KB 就发出去(提高吞吐)
props.put("batch.size", 16384);
// 等待时间:即使没满 batch.size,超过 5ms 也强制发送(降低延迟)
props.put("linger.ms", 5);
// 消息最大字节数
props.put("max.request.size", 1048576); // 1MB
指定 Partition 发送
// 方式一:在 ProducerRecord 构造时直接指定 Partition 编号
ProducerRecord<String, String> record =
new ProducerRecord<>("order-topic", 0, "order-001", "消息内容"); // 强制发往 Partition 0
// 方式二:通过 Key 路由(相同 Key 一定去同一个 Partition,适合保序场景)
// 例如同一个用户的所有订单消息,用 userId 作为 Key,保证顺序消费
ProducerRecord<String, String> record =
new ProducerRecord<>("order-topic", "user-123", "消息内容");
// 方式三:自定义分区器
public class OrderPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
// 根据业务逻辑决定发往哪个 Partition
int partitionCount = cluster.partitionCountForTopic(topic);
if (key != null) {
return Math.abs(key.hashCode()) % partitionCount;
}
return 0;
}
// ... 其他方法
}
// 使用自定义分区器
props.put("partitioner.class", "com.example.OrderPartitioner");
4.3 原生 Consumer(消费者)
基础消费:手动控制循环
import org.apache.kafka.clients.consumer.*;
import java.time.Duration;
import java.util.*;
public class SimpleConsumer {
public static void main(String[] args) {
// 1. 配置消费者参数
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-consumer-group"); // 消费者组 ID,非常重要
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 自动提交 Offset(先理解,后面会讲手动提交更安全)
props.put("auto.commit.enable", "true");
props.put("auto.commit.interval.ms", "1000"); // 每 1 秒自动提交一次
// 2. 创建消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 3. 订阅 Topic(可以订阅多个)
consumer.subscribe(Arrays.asList("order-topic"));
// 4. 循环拉取消息(Kafka 是拉模式,消费者主动去拉)
try {
while (true) {
// poll 拉取消息,最多等待 100ms
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息 → Topic: %s, Partition: %d, Offset: %d, Key: %s, Value: %s%n",
record.topic(),
record.partition(),
record.offset(),
record.key(),
record.value());
// 业务处理:保存到数据库、调用服务等
processOrder(record.value());
}
}
} finally {
consumer.close();
}
}
private static void processOrder(String orderJson) {
// 实际业务逻辑
System.out.println("处理订单: " + orderJson);
}
}
手动提交 Offset(生产环境推荐)
自动提交有个风险:消息拉下来还没处理完,Offset 就提交了,这时候应用崩溃,消息就丢了。手动提交能保证"处理完再提交":
props.put("enable.auto.commit", "false"); // 关闭自动提交
// 方式一:同步提交(处理完一批提交一次,简单安全)
for (ConsumerRecord<String, String> record : records) {
processOrder(record.value());
}
consumer.commitSync(); // 批量处理完后统一提交
// 方式二:异步提交(性能更好,不阻塞)
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
System.err.println("Offset 提交失败: " + exception.getMessage());
// 记录日志,下次重新消费
}
});
// 方式三:精确提交到某个 Offset(最精细的控制)
Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
processOrder(record.value());
offsetsToCommit.put(
new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1) // 注意:提交的是下一条消息的 Offset
);
}
consumer.commitSync(offsetsToCommit);
auto.offset.reset 参数:新消费者组第一次消费从哪开始
// "earliest":从 Topic 最早的消息开始消费(--from-beginning 的效果)
props.put("auto.offset.reset", "earliest");
// "latest":只消费订阅之后新来的消息(默认值)
props.put("auto.offset.reset", "latest");
五、Spring Boot 整合 Kafka
原生 API 帮你理解了本质,但在实际项目里,Spring Boot 封装让一切更优雅。
5.1 依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
5.2 application.yml 配置
spring:
kafka:
# Kafka 服务器地址(集群用逗号分隔)
bootstrap-servers: localhost:9092
# 生产者配置
producer:
# 序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# 可靠性:all = 等所有副本确认
acks: all
# 失败重试次数
retries: 3
# 批量发送大小
batch-size: 16384
# 批量等待时间
properties:
linger.ms: 5
# 消费者配置
consumer:
# 消费者组 ID
group-id: my-service-group
# 反序列化
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 新消费者组从最早的消息开始消费
auto-offset-reset: earliest
# 关闭自动提交(手动控制更安全)
enable-auto-commit: false
# 监听器配置
listener:
# MANUAL_IMMEDIATE = 手动立即提交 Offset
ack-mode: MANUAL_IMMEDIATE
# 并发消费线程数(建议等于 Partition 数量)
concurrency: 3
5.3 创建 Topic(用配置类,推荐)
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;
@Configuration
public class KafkaTopicConfig {
/**
* 创建订单主题:3 个分区,2 个副本
* Spring Boot 启动时会自动检查,不存在才创建
*/
@Bean
public NewTopic orderTopic() {
return TopicBuilder
.name("order-created")
.partitions(3)
.replicas(1) // 本地开发用 1,生产环境用 2 或 3
.build();
}
@Bean
public NewTopic paymentTopic() {
return TopicBuilder
.name("payment-success")
.partitions(3)
.replicas(1)
.build();
}
}
5.4 生产者:KafkaTemplate
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFutureCallback;
@Service
public class OrderEventProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/**
* 最简单的发送
*/
public void sendSimple(String orderId, String message) {
kafkaTemplate.send("order-created", orderId, message);
}
/**
* 带回调的异步发送(生产环境推荐)
*/
public void sendWithCallback(String orderId, String orderJson) {
kafkaTemplate.send("order-created", orderId, orderJson)
.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> result) {
RecordMetadata meta = result.getRecordMetadata();
log.info("订单消息发送成功 → orderId={}, partition={}, offset={}",
orderId, meta.partition(), meta.offset());
}
@Override
public void onFailure(Throwable ex) {
log.error("订单消息发送失败 → orderId={}, 原因={}", orderId, ex.getMessage());
// 业务处理:写入补偿表、触发告警、加入重试队列等
}
});
}
/**
* 发送对象(需要配置 JSON 序列化,后面会讲)
*/
public void sendOrderEvent(OrderCreatedEvent event) {
kafkaTemplate.send("order-created", String.valueOf(event.getOrderId()), event);
}
}
5.5 消费者:@KafkaListener
基础消费
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
@Component
public class OrderEventConsumer {
/**
* 最基础的消费
*/
@KafkaListener(topics = "order-created", groupId = "order-service-group")
public void handleOrderCreated(String message) {
System.out.println("收到订单消息: " + message);
// 处理业务逻辑
}
/**
* 获取消息完整元数据 + 手动提交 Offset(生产环境推荐)
*/
@KafkaListener(topics = "order-created", groupId = "order-service-group")
public void handleWithAck(
@Payload String message, // 消息内容
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic, // Topic 名称
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition, // Partition 编号
@Header(KafkaHeaders.OFFSET) long offset, // Offset
Acknowledgment ack) { // 手动提交工具(需配置 ack-mode: MANUAL_IMMEDIATE)
log.info("收到消息 → topic={}, partition={}, offset={}, message={}",
topic, partition, offset, message);
try {
processOrder(message);
ack.acknowledge(); // 处理成功才提交 Offset
} catch (Exception e) {
log.error("消息处理失败,不提交 Offset,等待重试: {}", e.getMessage());
// 不调用 ack.acknowledge(),消息会被重新消费
}
}
/**
* 批量消费(提高吞吐量)
*/
@KafkaListener(topics = "order-created", groupId = "order-batch-group",
containerFactory = "batchKafkaListenerContainerFactory")
public void handleBatch(List<String> messages, Acknowledgment ack) {
log.info("批量收到 {} 条消息", messages.size());
for (String message : messages) {
processOrder(message);
}
ack.acknowledge(); // 批量处理完后统一提交
}
}
监听多个 Topic / 指定 Partition
// 监听多个 Topic
@KafkaListener(topics = {"order-created", "order-updated"}, groupId = "order-group")
public void handleMultipleTopics(String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
log.info("来自 Topic: {}, 消息: {}", topic, message);
}
// 监听指定 Partition(精确控制,不常用)
@KafkaListener(topicPartitions = {
@TopicPartition(topic = "order-created", partitions = {"0", "1"})
}, groupId = "order-group")
public void handleSpecificPartition(String message) {
// 只消费 Partition 0 和 1 的消息
}
六、发送 Java 对象(JSON 序列化)
实际业务中发的是对象,不是裸字符串。
6.1 定义消息对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderCreatedEvent {
private Long orderId;
private Integer userId;
private BigDecimal amount;
private String status;
private LocalDateTime createdAt;
}
6.2 配置 JSON 序列化(两种方式)
方式一:yml 配置(简洁)
spring:
kafka:
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "cn.tedu.charging.*" # 允许反序列化的包,安全白名单
spring.json.value.default.type: cn.tedu.charging.order.dto.OrderCreatedEvent
方式二:Java 配置类(更灵活,推荐)
@Configuration
public class KafkaSerializerConfig {
@Bean
public ProducerFactory<String, Object> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ConsumerFactory<String, OrderCreatedEvent> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service-group");
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
config.put(JsonDeserializer.TRUSTED_PACKAGES, "cn.tedu.charging.*");
return new DefaultKafkaConsumerFactory<>(config,
new StringDeserializer(),
new JsonDeserializer<>(OrderCreatedEvent.class));
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
6.3 发送和接收对象
// 发送
@Service
public class OrderService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
public void createOrder(Order order) {
// 业务逻辑保存订单...
// 发送领域事件
OrderCreatedEvent event = new OrderCreatedEvent(
order.getId(), order.getUserId(),
order.getAmount(), "CREATED", LocalDateTime.now()
);
kafkaTemplate.send("order-created", String.valueOf(order.getId()), event);
log.info("订单创建事件已发送,orderId={}", order.getId());
}
}
// 消费
@KafkaListener(topics = "order-created", groupId = "notification-group")
public void handleOrderCreated(OrderCreatedEvent event, Acknowledgment ack) {
log.info("收到订单创建事件: orderId={}, userId={}, amount={}",
event.getOrderId(), event.getUserId(), event.getAmount());
// 发送通知
notificationService.sendOrderCreatedNotification(event);
ack.acknowledge();
}
七、实战场景:完整的充电计费事件流
以充电桩场景为例,把 Kafka 嵌入真实业务:
充电桩上报数据
→ MQTT Consumer 接收
→ 调用计费服务计算费用
→ 发送 Kafka 消息(charging-process-topic)
→ 通知服务消费 → 推送 WebSocket 给用户
→ 账单服务消费 → 更新账单记录
→ 日志服务消费 → 写入充电日志
7.1 定义消息对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChargingProcessEvent {
private String orderNo;
private Integer userId;
private BigDecimal chargingCapacity; // 当前累计充电量
private BigDecimal totalFee; // 当前累计费用
private BigDecimal powerFee; // 电费单价
private BigDecimal serviceFee; // 服务费单价
private String costRuleName; // 计费规则名称(尖峰平谷)
private Long chargingTime; // 充电时长(ms)
private Long timestamp; // 事件时间戳
}
7.2 生产者:计费服务计算完成后发事件
@Service
@Slf4j
public class ChargingEventPublisher {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
public void publishChargingProcess(ChargingProcessVO vo) {
ChargingProcessEvent event = buildEvent(vo);
kafkaTemplate.send("charging-process", vo.getOrderNo(), event)
.addCallback(
result -> log.info("充电进度事件发布成功,orderNo={}", vo.getOrderNo()),
ex -> log.error("充电进度事件发布失败,orderNo={}", vo.getOrderNo(), ex)
);
}
private ChargingProcessEvent buildEvent(ChargingProcessVO vo) {
ChargingProcessEvent event = new ChargingProcessEvent();
BeanUtils.copyProperties(vo, event);
event.setTimestamp(System.currentTimeMillis());
return event;
}
}
7.3 消费者一:通知服务推送 WebSocket
@Component
@Slf4j
public class ChargingNotificationConsumer {
@Autowired
private WebSocketServer webSocketServer;
@KafkaListener(
topics = "charging-process",
groupId = "notification-service-group",
containerFactory = "chargingEventContainerFactory"
)
public void pushToUser(ChargingProcessEvent event, Acknowledgment ack) {
log.info("收到充电进度事件,准备推送 WebSocket,userId={}", event.getUserId());
try {
String message = JsonUtil.toJson(event);
webSocketServer.sendMessage(event.getUserId(), message);
ack.acknowledge();
log.info("WebSocket 推送成功,userId={}", event.getUserId());
} catch (Exception e) {
log.error("WebSocket 推送失败,userId={}", event.getUserId(), e);
// 不 ack,等待重试
}
}
}
7.4 消费者二:账单服务更新账单(同一 Topic,不同 Group)
@Component
@Slf4j
public class ChargingBillConsumer {
@Autowired
private BillService billService;
// groupId 不同,所以这个消费者也能收到完整的消息(广播效果)
@KafkaListener(
topics = "charging-process",
groupId = "bill-service-group",
containerFactory = "chargingEventContainerFactory"
)
public void updateBill(ChargingProcessEvent event, Acknowledgment ack) {
log.info("收到充电进度事件,更新账单,orderNo={}", event.getOrderNo());
try {
billService.updateChargingBill(event.getOrderNo(), event.getTotalFee());
ack.acknowledge();
} catch (Exception e) {
log.error("账单更新失败,orderNo={}", event.getOrderNo(), e);
}
}
}
八、死信队列:消息消费失败怎么办
消费失败不能无限重试,需要有个兜底方案。
@Configuration
public class KafkaDeadLetterConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object>
kafkaListenerContainerFactory(ConsumerFactory<String, Object> consumerFactory,
KafkaTemplate<String, Object> kafkaTemplate) {
ConcurrentKafkaListenerContainerFactory<String, Object> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
// 配置错误处理器:重试 3 次失败后发送到死信队列
factory.setErrorHandler(new SeekToCurrentErrorHandler(
new DeadLetterPublishingRecoverer(kafkaTemplate), // 发到死信 Topic
new FixedBackOff(1000L, 3) // 每隔 1 秒重试,最多重试 3 次
));
return factory;
}
}
死信 Topic 命名规则:原 Topic 名称 + .DLT,例如 charging-process.DLT
// 监听死信队列,人工介入或告警
@KafkaListener(topics = "charging-process.DLT", groupId = "dlt-handler-group")
public void handleDeadLetter(ConsumerRecord<String, Object> record) {
log.error("【死信消息】Topic={}, Key={}, Value={}",
record.topic(), record.key(), record.value());
// 告警、写入数据库人工处理、邮件通知等
alertService.sendDeadLetterAlert(record);
}
九、常见问题与踩坑
9.1 消息重复消费
Kafka 保证"至少一次"(at-least-once)投递,网络抖动时可能重复。解决方案:消费端做幂等。
@KafkaListener(topics = "order-created")
public void handleOrder(String orderJson, Acknowledgment ack) {
OrderCreatedEvent event = JsonUtil.toObject(orderJson, OrderCreatedEvent.class);
// 用 orderNo 做幂等键,检查是否已经处理过
if (orderProcessedCache.contains(event.getOrderNo())) {
log.warn("重复消息,已跳过处理,orderNo={}", event.getOrderNo());
ack.acknowledge(); // 仍要 ack,避免无限重试
return;
}
processOrder(event);
orderProcessedCache.add(event.getOrderNo()); // 实际用 Redis SET 做分布式幂等
ack.acknowledge();
}
9.2 消息顺序问题
同一 Topic 多个 Partition 之间无序。如果需要保序(比如同一用户的操作按顺序处理):
-
生产端:相同业务 Key(如 userId)发到相同 Partition
-
消费端:该 Partition 只由一个消费者处理
// 生产端:userId 相同的消息一定去同一个 Partition
kafkaTemplate.send("user-events", String.valueOf(event.getUserId()), event);
9.3 消费者线程数与 Partition 数的关系
消费者数 < Partition 数:部分消费者处理多个 Partition,可以(压力不均)
消费者数 = Partition 数:最理想,每人负责一个 Partition
消费者数 > Partition 数:多出来的消费者空闲,浪费资源
所以 yml 里的 listener.concurrency 建议等于 Partition 数量:
spring:
kafka:
listener:
concurrency: 3 # Topic 有 3 个 Partition,就配 3 个线程
9.4 消费者 Rebalance
当消费者组内有成员加入或退出时,Kafka 会触发 Rebalance,重新分配 Partition,期间会暂停消费。优化方式:
spring:
kafka:
consumer:
properties:
# 心跳间隔(消费者每隔多久告诉 Broker 自己还活着)
heartbeat.interval.ms: 3000
# 会话超时时间(超过这个时间没心跳,认为消费者挂了,触发 Rebalance)
session.timeout.ms: 30000
# 每次 poll 之间的最大间隔,超过这个时间也会触发 Rebalance
max.poll.interval.ms: 300000
十、总结对照表
| 功能 | 原生 API | Spring Boot |
|---|---|---|
| 生产消息 | KafkaProducer.send() |
KafkaTemplate.send() |
| 消费消息 | KafkaConsumer.poll() + while 循环 |
@KafkaListener 注解 |
| 手动提交 | consumer.commitSync() |
Acknowledgment.acknowledge() |
| 序列化 | 手动配置 Serializer | yml 配置或 @Bean 配置类 |
| 创建 Topic | 命令行或 AdminClient | @Bean NewTopic 自动创建 |
| 死信队列 | 手动捕异常转发 | DeadLetterPublishingRecoverer |
| 重试 | 手动实现 | SeekToCurrentErrorHandler + FixedBackOff |
选型建议:
- 学习和理解底层原理 → 先玩原生 API
- 实际项目开发 → 直接用 Spring Boot 整合,开发效率高出一个量级
- 需要精细控制分区、序列化、消费策略 → 用 Java Config 方式配置 Spring Kafka,不要只依赖 yml
记住最重要的一条:Kafka 能帮你解耦和削峰,但它不是银弹。消息的可靠性、幂等性、顺序性都需要你在业务代码里配合处理,Kafka 只负责"把消息安全地从 A 送到 B",剩下的还是你的活。