Spring Boot 通过 Spring for Apache Kafka 项目提供了非常优雅的方式来对接 Kafka 的分区存储机制。你可以从简单到复杂,根据业务需求选择不同的控制级别。
核心概念与配置
首先,在 pom.xml
中引入依赖:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
在 application.yml
中进行基本配置:
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: my-consumer-group # 消费者组ID,非常重要!
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
auto-offset-reset: earliest # 从最早的消息开始消费(可选)
一、生产者端:控制消息写入哪个分区
生产者决定消息发送到哪个分区。Spring Kafka 提供了多种策略。
1. 默认行为 (不指定 Key)
如果不指定消息的 Key,生产者会使用轮询(Round-Robin) 策略,将消息均匀地分发到 Topic 的所有分区上,以实现负载均衡。
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyProducerService {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendMessageDefault(String topic, String message) {
// 没有指定 key,消息将被轮询发送到各个分区
kafkaTemplate.send(topic, message);
}
}
2. 指定 Key (最常用的分区控制策略)
这是保证顺序性的关键。Kafka 会对 Key 进行哈希运算,相同的 Key 总是被路由到同一个分区。
public void sendMessageWithKey(String topic, String key, String message) {
// 指定 key,相同 key 的消息会进入同一个分区,从而保证其顺序性
kafkaTemplate.send(topic, key, message);
}
- 示例:
send("user-events", "user123", "UserLoggedIn")
和send("user-events", "user123", "UserPurchased")
这两个拥有相同 Key (user123
) 的消息,一定会被发送到user-events
Topic 的同一个分区里。
3. 自定义分区策略 (高级用法)
如果你有更复杂的分区逻辑(比如根据消息头的某个字段),可以实现 Partitioner
接口。
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;
import java.util.Map;
public class CustomPartitioner 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();
if (keyBytes == null) {
throw new InvalidRecordException("All messages must have a custom header 'partitionKey'");
}
// 这里实现你的自定义逻辑,例如根据 key 的特定规则计算分区
// 假设我们想将包含"important"的key都分配到最后一个分区
String keyStr = (String) key;
if (keyStr.contains("important")) {
return numPartitions - 1; // 分配到最后一个分区
}
// 默认使用 Kafka 的默认哈希策略
return Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1);
}
@Override
public void close() {}
@Override
public void configure(Map<String, ?> configs) {}
}
然后在配置中启用它:
spring:
kafka:
producer:
properties:
partitioner.class: com.yourpackage.CustomPartitioner
4. 精确指定分区 (极少使用)
你可以直接指定目标分区,但这通常不推荐,因为它破坏了 Kafka 的负载均衡特性。
public void sendMessageToSpecificPartition(String topic, int partition, String key, String message) {
// 直接发送到指定的分区(注意:分区号从0开始)
kafkaTemplate.send(topic, partition, key, message);
}
二、消费者端:监听特定分区
消费者通常不需要关心具体分区,因为分区会自动在消费者组内进行分配。但 Spring Kafka 也提供了监听特定分区的功能,主要用于特殊场景(如调试、特定分区处理)。
1. 自动分配分区 (默认和推荐方式)
你只需要使用 @KafkaListener
注解即可。Spring Kafka 会自动处理消费者组协调、分区分配和再平衡。
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class MyConsumer {
// 最简单的方式,监听 topic
@KafkaListener(topics = "my-topic")
public void listen(String message) {
System.out.println("Received message: " + message);
}
// 可以监听多个 topic,并获取消息头等信息
@KafkaListener(topics = {"topic1", "topic2"})
public void listenWithHeaders(
String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
System.out.println(String.format("Received message [%s] from topic [%s] partition [%d]", message, topic, partition));
}
}
2. 手动指定分区 (高级控制)
你可以显式地让某个监听器只消费特定 Topic 的特定分区。注意:这种情况下,你需要手动管理消费者组ID,或者使用空组ID(groupId = ""
)来避免自动分配。
@KafkaListener(
// 注意:这里不指定 groupId 或使用空组,以避免冲突
topicPartitions = @TopicPartition(
topic = "my-topic",
partitions = { "0", "1" }, // 只监听 0 和 1 号分区
partitionOffsets = {} // 也可以指定从某个偏移量开始监听
)
)
public void listenToPartitions(String message) {
// 这个方法只会收到 my-topic 分区0和分区1的消息
System.out.println("Received from manually assigned partition: " + message);
}
3. 获取分区信息
你可以在监听方法中注入分区信息,用于记录日志或实现分区相关的业务逻辑。
@KafkaListener(topics = "my-topic")
public void listen(
String payload,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.OFFSET) long offset) {
log.info("Received message [{}] from topic [{}], partition [{}], offset [{}]",
payload, topic, partition, offset);
// 可以根据分区号执行不同的逻辑
if (partition == 0) {
// 处理分区0的消息
} else {
// 处理其他分区的消息
}
}
三、最佳实践总结
- 首选策略(生产者):使用消息 Key 来控制分区。这是实现消息顺序性和逻辑分组最自然、最有效的方式。
- 首选策略(消费者):使用默认的
@KafkaListener
和消费者组机制。让 Kafka 自动处理分区的分配和再平衡,这是最可靠和可扩展的方式。 - 谨慎使用:
- 自定义分区器:只在默认的哈希策略不满足复杂业务需求时使用。
- 手动指定消费者分区:只在非常特殊的场景下使用(如创建一个独立的管理工具来消费某个分区的数据进行审计),不要在主流业务逻辑中使用,因为它破坏了消费者组的自动容错和扩展机制。
- 监控:始终在日志中记录消息的 Topic、分区和偏移量信息,这对于调试消息顺序问题和延迟问题至关重要。
通过 Spring Boot 的 Spring Kafka 模块,你可以轻松地利用 Kafka 分区的所有强大功能,同时享受 Spring 框架带来的开发便利性。