SpringBoot教程(十六) | SpringBoot集成Kafka
【参考文章】
【1】SpringBoot集成Kafka实战应用
【2】SpringBoot——集成Kafka详解
(一)Kafka介绍
Apache Kafka是一个分布式流处理平台。它最初由LinkedIn开发,后来成为Apache软件基金会的一部分,并在开源社区中得到了广泛应用。
Kafka的核心概念包括Producer、Consumer、Broker、Topic、Partition和Offset。
- Producer:生产者,负责将数据发送到Kafka集群。
- Consumer:消费者,从Kafka集群中读取数据。
- Broker:Kafka服务器实例,Kafka集群通常由多个Broker组成。
- Topic:主题,数据按主题进行分类。
- Partition:分区,每个主题可以有多个分区,用于实现并行处理和提高吞吐量。
- Offset:偏移量,每个消息在其分区中的唯一标识。
(二)使用场景
Kafka适用于以下场景:
- 日志收集:集中收集系统日志和应用日志,通过Kafka传输到大数据处理系统。
- 消息队列:作为高吞吐量、低延迟的消息队列系统。
- 数据流处理:实时处理数据流,用于实时分析、监控和处理。
- 事件源架构:将所有的变更事件存储在Kafka中,实现事件溯源和回放。
- 流数据管道:构建数据管道,连接数据源和数据存储系统。
(三)Spring Boot 集成 Kafka
1、 构建项目
1.1 前提条件
确保你已经安装了Kafka和ZooKeeper,并且它们正在正常运行
1.2 引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
1.3 application.propertise配置(本文用到的配置项这里全列了出来)
###########【Kafka集群 配多个时中间用","逗号隔开】###########
spring.kafka.bootstrap-servers=localhost:9092,localhost:9093
###########【初始化生产者配置】###########
# 消息发送重试次数
spring.kafka.producer.retries=0
# 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
spring.kafka.producer.acks=1
# 批量发送的消息大小(定义了生产者发送的每个批次中消息的总字节大小)
spring.kafka.producer.batch-size=16384
# 提交延时(控制了生产者在发送一个批次之前等待更多消息加入该批次的时间)
spring.kafka.producer.properties.linger.ms=0
# 生产端缓冲区大小
spring.kafka.producer.buffer-memory = 33554432
# Kafka提供的序列化和反序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 自定义分区器
# spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner
###########【初始化消费者配置】###########
# 默认的消费组ID
spring.kafka.consumer.properties.group.id=defaultConsumerGroup
# 是否自动提交offset
spring.kafka.consumer.enable-auto-commit=true
# 提交offset延时(接收到消息后多久提交offset)
spring.kafka.consumer.auto.commit.interval.ms=1000
# 当kafka中没有初始offset或offset超出范围时将自动重置offset
# earliest:重置为分区中最小的offset;
# latest:重置为分区中最新的offset(消费分区中新产生的数据);
# none:只要有一个分区不存在已提交的offset,就抛出异常;
spring.kafka.consumer.auto-offset-reset=latest
# 消费会话超时时间(超过这个时间consumer没有发送心跳,就会触发rebalance操作)
spring.kafka.consumer.properties.session.timeout.ms=120000
# 消费请求超时时间
spring.kafka.consumer.properties.request.timeout.ms=180000
# Kafka提供的序列化和反序列化类
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# 消费端监听的topic不存在时,项目启动会报错(关掉)
spring.kafka.listener.missing-topics-fatal=false
# 设置批量消费
# spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
# spring.kafka.consumer.max-poll-records=50
影响 生产者进行发送 的两个重要配置 linger.ms
和 batch-size
- linger.ms 的作用
linger.ms
控制了生产者在发送一个批次之前等待更多消息加入该批次的时间。
如果设置为 0,生产者将不会等待,而是会立即发送任何可用的消息批次,无论该批次是否达到了batch-size
的大小限制。- batch-size 的作用
batch-size
定义了生产者发送的每个批次中消息的总字节大小。当生产者收集到足够多的消息(其总字节大小达到或超过batch-size
)时,它会发送这个批次,或者当达到linger.ms
的时间限制时(如果linger.ms
大于 0)。- linger.ms=0 时的效果
当linger.ms
设置为 0 时,生产者不会等待更多消息加入当前批次,而是会立即发送任何已经收集到的消息。这意味着,如果生产者只收集到少量消息(其总字节大小远小于batch-size
),这些消息也会被发送出去。然而,如果生产者能够迅速收集到足够多的消息以达到或超过batch-size
,那么这些消息仍然会被组合成一个较大的批次发送。
1.4 实践一(简单)
1、简单生产者
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
// 发送消息
@GetMapping("/kafka/normal/{message}")
public void sendMessage1(@PathVariable("message") String normalMessage) {
kafkaTemplate.send("topic1", normalMessage);
}
}
2、简单消费
@Component
public class KafkaConsumer {
// 消费监听
@KafkaListener(topics = {"topic1"})
public void onMessage1(ConsumerRecord<?, ?> record){
// 消费的哪个topic、partition的消息,打印出消息内容
System.out.println("简单消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
}
}
上面示例创建了一个生产者,发送消息到topic1,消费者监听topic1消费消息。
监听器用@KafkaListener注解,topics表示监听的topic,支持同时监听多个,用英文逗号分隔。
启动项目,postman调接口触发生产者发送消息,
可以看到监听器消费成功,
2、生产者
2.1 实践二(带回调的生产者)
1、带回调的生产者
kafkaTemplate提供了一个回调方法addCallback,我们可以在回调方法中监控消息是否发送成功 或 失败时做补偿处理,有两种写法,
写法一:
@GetMapping("/kafka/callbackOne/{message}")
public void sendMessage2(@PathVariable("message") String callbackMessage) {
kafkaTemplate.send("topic1", callbackMessage).addCallback(success -> {
// 消息发送到的topic
String topic = success.getRecordMetadata().topic();
// 消息发送到的分区
int partition = success.getRecordMetadata().partition();
// 消息在分区内的offset
long offset = success.getRecordMetadata().offset();
System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
}, failure -> {
System.out.println("发送消息失败:" + failure.getMessage());
});
}
写法二:
@GetMapping("/kafka/callbackTwo/{message}")
public void sendMessage3(@PathVariable("message") String callbackMessage) {
kafkaTemplate.send("topic1", callbackMessage).addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable ex) {
System.out.println("发送消息失败:"+ex.getMessage());
}
@Override
public void onSuccess(SendResult<String, Object> result) {
System.out.println("发送消息成功:" + result.getRecordMetadata().topic() + "-"
+ result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
}
});
}
2.2 监听器 (异步监听生产者消息是否发送成功)
Kafka提供了ProducerListener 监听器来异步监听生产者消息是否发送成功,我们可以自定义一个kafkaTemplate添加ProducerListener,
当消息发送失败我们可以拿到消息进行重试或者把失败消息记录到数据库定时重试。
@Configuration
public class KafkaConfig {
@Autowired
ProducerFactory producerFactory;
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
KafkaTemplate<String, Object> kafkaTemplate = new KafkaTemplate<String, Object>();
kafkaTemplate.setProducerListener(new ProducerListener<String, Object>() {
@Override
public void onSuccess(ProducerRecord<String, Object> producerRecord, RecordMetadata recordMetadata) {
System.out.println("发送成功 " + producerRecord.toString());
}
@Override
public void onSuccess(String topic, Integer partition, String key, Object value, RecordMetadata recordMetadata) {
System.out.println("发送成功 topic = " + topic + " ; partion = " + partition + "; key = " + key + " ; value=" + value);
}
@Override
public void onError(ProducerRecord<String, Object> producerRecord, Exception exception) {
System.out.println("发送失败" + producerRecord.toString());
System.out.println(exception.getMessage());
}
@Override
public void onError(String topic, Integer partition, String key, Object value, Exception exception) {
System.out.println("发送失败" + "topic = " + topic + " ; partion = " + partition + "; key = " + key + " ; value=" + value);
System.out.println(exception.getMessage());
}
});
return kafkaTemplate;
}
}
注意:当我们发送一条消息,既会走 ListenableFutureCallback 回调,也会走ProducerListener回调。
2.3 自定义分区器
我们知道,kafka中每个topic被划分为多个分区,那么生产者将消息发送到topic时,具体追加到哪个分区呢?这就是所谓的分区策略,Kafka 为我们提供了默认的分区策略,这些策略对于数据在Kafka集群中的分布、负载均衡以及性能优化起着关键作用。以下是Kafka中几种常见的分区策略:
轮询策略(Round-Robin Strategy):
这是Kafka Java生产者API默认提供的分区策略
。- 如果没有指定分区策略,则会默认使用轮询。
- 轮询策略按照顺序将消息发送到不同的分区,每个消息被发送到其对应分区,按照顺序轮询每个分区,以确保每个分区均匀地接收消息。
- 这种策略能够实现负载均衡,并且能够最大限度地利用集群资源。
按键分配策略(Key-Based Partitioning):
- 在Kafka中,如果消息指定了key,那么生产者会根据key的哈希值来决定消息应该发送到哪个分区。
- 这种策略通过hash(key) % numPartitions来计算分区号,其中numPartitions是主题的总分区数。
- 如果key相同,那么这些消息会被发送到同一个分区,这有助于保持消息的局部性和顺序性。
注意:从Kafka 2.4.0版本开始,如果消息中没有指定分区且没有key,Kafka会使用粘性分区策略(Sticky Partitioning)来尽可能将消息发送到与之前消息相同的分区,以减少跨分区的数据移动和复制。
Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。
其路由机制为:
- 若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区;
- 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;
- patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition;
我们来自定义一个分区策略,将消息发送到我们指定的partition,首先新建一个分区器类实现Partitioner接口,重写方法,其中partition方法的返回值就表示将消息发送到几号分区
public class CustomizePartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 自定义分区规则(这里假设全部发到0号分区)
// ......
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
在application.propertise中配置自定义分区器,配置的值就是分区器类的全路径名,
# 自定义分区器
spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner
如果在发送消息时需要创建事务,可以使用 KafkaTemplate 的 executeInTransaction 方法来声明事务,
@GetMapping("/kafka/transaction")
public void sendMessage7(){
// 声明事务:后面报错的话 消息是不会发出去
kafkaTemplate.executeInTransaction(operations -> {
operations.send("topic1","test executeInTransaction");
throw new RuntimeException("fail");
});
// 不声明事务:后面报错的话 但前面消息已经发送成功了
kafkaTemplate.send("topic1","test executeInTransaction");
throw new RuntimeException("fail");
}
注意:如果声明了事务,需要在application.yml中指定:
#事务id前缀
spring.kafka.producer.transaction-id-prefix=tx_