持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情
实战
了解Kafka基本知识点后,下面就来尝试一下SpringBoot+kafka的整合。
首先需要本机需要安装一下Kafka,在这里选择的是2.8.1的版本,下载解压到对应的文件夹即可。
随后便先启动一下Zookeeper和Kafka
// 启动Zookeepr
E:\kafka\kafka_2.12-2.8.2>bin\windows\zookeeper-server-start.bat config\zookeeper.properties
// 启动Kafka
E:\kafka\kafka_2.12-2.8.2>bin\windows\kafka-server-start.bat config\server.properties
开发环境
| 依赖 | 版本 |
|---|---|
| spring-boot-starter-parent | 2.7.4 |
| JDK | 11 |
Kafka配置信息
spring:
kafka:
bootstrap-servers:
- 127.0.0.1:9092 // kafka服务器地址
consumer:
group-id: defaultConsumerGroup // 默认消费者名
enable-auto-commit: false // 消费者是否自动提交 默认为true
listener:
ack-mode: manual // 配置消费者监听器手动提交
kafka:
topic:
name: spring-kafka-demo1 // 自定义配置-主题名
创建Topic
@Configuration
public class KafkaInitialConfiguration {
@Value( "${kafka.topic.name}" )
private String topicName;
@Bean
public NewTopic initTopic(){
return new NewTopic( topicName,3, (short) 3 );
}
}
在这里创建了一个分区数和副本数都为3的主题。
创建简单生产者
@RestController
@RequestMapping("/kafka")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Slf4j
public class KafkaProducerController {
private final KafkaTemplate<String,String> kafkaTemplate;
@Value( "${kafka.topic.name}" )
private String topicName;
@GetMapping("/send")
public ResponseEntity sendMessage(@RequestParam String message){
kafkaTemplate.send( topicName, message );
return ResponseEntity.ok().build();
}
}
在这里创建一个简单的生产者,在这里没有指定key和partition,因此将会使用默认的分区策略:轮询选择一个分区。
创建简单消费者
@Component
public class KafkaConsumer {
private static final Logger log = LoggerFactory.getLogger(KafkaConsumer.class);
@KafkaListener(topics = "${kafka.topic.name}")
public void handleMessage(ConsumerRecord<String,String> consumerRecord, Acknowledgment acknowledgment) {
log.info( "key={} value={}",consumerRecord.key(),consumerRecord.value() );
acknowledgment.acknowledge(); // 手动提交ack
}
}
由于配置里关闭了自动提交ack,因此在handleMessage方法里就需要手动进行提交
创建回调生产者
@GetMapping("/send-callback")
public ResponseEntity sendCallBackMessage(@RequestParam String message){
kafkaTemplate.send( topicName,message ).addCallback( success -> {
String topic = success.getRecordMetadata().topic();
int partition = success.getRecordMetadata().partition();
long offset = success.getRecordMetadata().offset();
log.info( "topic={}\tpartition={}\toffset={}",topic,partition,offset );
},failure->{
log.info( "发送失败:{}",failure.getMessage() );
});
return ResponseEntity.ok().build();
}
在大部分情况下,我们都需要知道消息是否成功发送到对应的Kafka服务器去,所以我们可以使用回调来获得监控信息是否发送成功或者失败,以此可以做出相关的恢复操作。
创建指定分区生产者
@GetMapping("/send-partition")
public ResponseEntity sendMessageByPartition(@RequestParam(defaultValue = "1") Integer num, @RequestParam String message){
kafkaTemplate.send( topicName, num, "just key",message );
return ResponseEntity.ok().build();
}
随后我们创建两个指定分区的消费者,随后我们调用对应发送接口,就可以看到都进入到对应的分区啦
@KafkaListener(topicPartitions = {
@TopicPartition( topic = "${kafka.topic.name}",partitions = "1")
})
public void handleMessageByPartition1(ConsumerRecord<String,String> consumerRecord, Acknowledgment acknowledgment) {
log.info( "分区1:key={} value={}",consumerRecord.key(),consumerRecord.value() );
acknowledgment.acknowledge();
}
@KafkaListener(topicPartitions = {
@TopicPartition( topic = "${kafka.topic.name}",partitions = "2")
})
public void handleMessageByPartition2(ConsumerRecord<String,String> consumerRecord, Acknowledgment acknowledgment) {
log.info( "分区2:key={} value={}",consumerRecord.key(),consumerRecord.value() );
acknowledgment.acknowledge();
}
kafka消息丢失问题
通过上面一些简单的例子,就可以发现在某些情况下消息是会丢失的,例如:
- 生产者发送消息时,由于网络原因或者系统故障,消息并没有发送到对应的Kafka服务器
- Kafka服务器收到信息后还没有写入磁盘,系统就宕机了
- 消费者收到消息后,准备进行处理,但系统又宕机了
生产者防止丢失
可以使用上面的发送回调方法来防止,当监听到发送失败时就进行相关的消息补偿机制,同时也可以加入多次发送重试。
# 重试次数
spring.kafka.producer.retries=3
kafka防止丢失
这个我们可以通过多副本和acks配合来实现,例如将acks设为all,来最大程度上防止消息的丢失。
消费者防止丢失
取消ack的自动提交,而改为处理完业务后,手动提交,但需要注意避免多次消费的问题。