1、依赖及yml配置
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.8.0</version>
</dependency>
<!-- 监控(可选) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
spring:
kafka:
# 生产者配置
producer:
bootstrap-servers: kafka1:9092,kafka2:9092,kafka3:9092
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# 重试机制
retries: 3
# 批量发送
batch-size: 16384
linger-ms: 1
# 消费者配置
consumer:
bootstrap-servers: kafka1:9092,kafka2:9092,kafka3:9092
group-id: "my-consumer-group"
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 自动提交偏移量(生产环境建议手动提交)
enable-auto-commit: false
# 从最新偏移量开始消费(根据业务调整)
auto-offset-reset: earliest/latest
# 并发消费者数(根据分区数调整)
concurrency: 3
# 监听器配置
listener:
# 手动提交偏移量
ack-mode: MANUAL_IMMEDIATE
# 消费失败重试
retry:
max-attempts: 3
initial-interval: 1000
multiplier: 2.0
max-interval: 10000
auto-offset-reset
auto.offset.reset
参数决定了消费者组在以下场景的行为:
- 消费者组初次启动 (无历史偏移量)。
- 偏移量无效 (如消费者组被删除或分区数据过期)。
earliest:从头开始读,读取全量历史数据
latest:仅读最新数据
搭配enable-auto-commit: false手动提交避免消息丢失
问题 1:消息重复消费
- 原因 :自动提交偏移量可能在消息处理前提交。
- 解决方案 :关闭自动提交,手动提交偏移量。
问题 2:消息丢失
- 原因 :消费者崩溃时,未提交的偏移量可能丢失。
- 解决方案 :结合手动提交和事务(Kafka Transactions)。
2、生产者配置(事务支持)
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092");
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 显式启用幂等性
config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 固定事务 ID(示例:结合应用名和实例 ID)
String transactionId = "tx-myapp-" + UUID.randomUUID().toString(); // 或使用固定前缀 + 实例标识
config.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, transactionId);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public KafkaTransactionManager<String, String> kafkaTransactionManager() {
return new KafkaTransactionManager<>(producerFactory());
}
}
3、消费者配置(手动提交+错误处理DLQ)
@Configuration
@EnableKafka
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092");
config.put(ConsumerConfig.GROUP_ID_CONFIG, "my-consumer-group");
// 使用 ErrorHandlingDeserializer 包装实际反序列化器
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class);
config.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, StringDeserializer.class);
config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
return new DefaultKafkaConsumerFactory<>(config);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
ConsumerFactory<Object, Object> consumerFactory,
KafkaTemplate<Object, Object> kafkaTemplate) {
ConcurrentKafkaListenerContainerFactory<Object, Object> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.setConcurrency(3);
// 配置错误处理器(重试3次后转发到DLQ)
factory.setErrorHandler(new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(kafkaTemplate,
(record, exception) -> {
// 自定义 DLQ Topic 名称(例如按原始 Topic 分类)
return TopicPartition.of("dlq-" + record.topic());
}),
new FixedBackOff(1000, 3) // 重试3次,间隔1秒
));
return factory;
}
}
DeadLetterPublishingRecoverer的作用
- 功能 :
当消息消费失败且重试次数耗尽后,DeadLetterPublishingRecoverer
会通过KafkaTemplate
将失败消息发送到指定的 DLQ Topic。 - DLQ Topic 命名规则 :
你的配置中使用dlq-<original-topic>
,例如原始 Topic 是user-activity
,则 DLQ 是dlq-user-activity
。
DefaultErrorHandler的行为
- 重试策略 :
FixedBackOff(1000, 3)
表示最多重试 3 次,间隔 1 秒。 - 触发条件 :
当消费者抛出异常且重试次数用尽时,DeadLetterPublishingRecoverer
会被调用。
DLQ生效条件
- 确保 DLQ Topic 存在
- Kafka 不会自动创建 DLQ Topic,需手动创建或启用自动创建
- 禁用自动提交偏移量
- 显式地抛出异常
4、生产者服务(事务消息)
事务与异步发送的交互原理
- 事务消息的缓冲机制
当使用@Transactional
时,KafkaTemplate 的send()
操作会将消息暂存到事务缓冲区,而不是立即发送到 Kafka Broker。
实际的消息发送会延迟到事务提交时(通常是在方法成功返回时)才会批量发送。 - 异步回调的触发时机
即使使用了ListenableFuture
的回调机制:onSuccess
/onFailure
回调会在 消息被写入事务缓冲区后立即触发- 而不是在消息实际发送到 Kafka Broker 之后
这意味着回调中无法准确反映消息最终是否成功提交到 Kafka。
1、同步发送+事务
@Transactional("kafkaTxManager")
public void transactionalSend() {
// 同步等待发送完成(实际仍会延迟到事务提交)
SendResult<K, V> result = kafkaTemplate.send(topic, message).get();
log.info("消息进入事务缓冲区, offset暂未分配");
} // 事务提交时才会真正发送
2、使用Transactional Event Listeners
(Spring Kafka 2.5+)
@Transactional("kafkaTxManager")
public void sendWithEvent() {
kafkaTemplate.send(topic, message);
// 事务提交后触发事件
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 这里可以安全处理发送成功逻辑
}
});
}
3、禁用事务的自动提交
手动控制事务
@Autowired private KafkaTemplate<String, String> kafkaTemplate;
public void manualTransaction() {
kafkaTemplate.executeInTransaction(t -> {
ListenableFuture<SendResult<String, String>> future = t.send(topic, message);
future.addCallback(
result -> log.info("Sent message=[{}] with offset=[{}]", message, result.getRecordMetadata().offset()),
ex -> log.error("Failed to send message=[{}]", message, ex)); // 此时回调在事务提交后触发
return null;
});
}
4、取消事务
public void send(String message) {
CompletableFuture<SendResult<String, String>> future = kafkaTemplate.send("test", message);
future.whenComplete((result, ex) -> {
if (ex == null) {
System.out.println("发送消息成功:"
+ result.getRecordMetadata().topic() + "-"
+ result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
} else {
System.out.println("发送消息失败:" + ex.getMessage());
}
});
}
5、消费者服务(手动提交)
@KafkaListener(topics = "my_topic", groupId = "my-consumer-group")
public void listen(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
// 处理消息
System.out.println("收到消息: " + record.value());
// 模拟业务异常
if (record.value().contains("bad-data")) {
throw new RuntimeException("模拟处理失败");
}
// 手动提交偏移量(确保是最后一行)
ack.acknowledge();
} catch (Exception e) {
// 记录异常并抛出(触发重试或DLQ)
log.error("消息处理失败: {}", record.value(), e);
throw new RuntimeException("消息处理失败", e);
}
}