使用Spring Boot轻松集成Kafka

读完本文你会对Kafka有更加深入的了解并且在使用时遇到问题也会迎刃而解

实时处理数据并确保应用程序各部分之间实现无缝通信的重要性显而易见。在这一领域中,Apache Kafka技术备受瞩目。在本文章中,我们将探讨Kafka的基本概念、关键特性以及它如何完美融入Spring Boot生态系统。

什么是Apache Kafka?

Apache Kafka主要是一个用于分布式事件流处理和流处理的开源平台。它最初是由LinkedIn的工程师开发的,后来成为Apache软件基金会的一部分并被开源。Kafka的设计目标是解决实时处理大量数据的挑战,因此它非常适合那些需要高吞吐量、容错能力和实时数据流的应用程序。

为什么使用Apache Kafka?

Kafka的架构和功能使其在各种场景中成为强大的工具。

  1. 实时数据处理:Kafka擅长处理实时数据流,使其成为需要即时数据更新和事件驱动处理的应用程序的理想选择。
  2. 可扩展性:其分布式特性使得无缝扩展成为可能,让您在不妨碍性能的情况下处理大量数据。
  3. 容错性:Kafka的复制机制确保即使在代理故障的情况下也不会丢失数据。
  4. 事件溯源:Kafka是事件溯源架构的基本组成部分,其中捕获应用程序状态更改作为一系列事件。
  5. 日志聚合:Kafka通过捕获和存储应用程序状态更改作为一系列有序事件,发挥着关键作用。

Kafka的核心概念

  1. 主题(Topic):Kafka中的主题充当发布记录的容器。生产者将数据发送到特定主题,消费者则从主题中读取数据。
  2. 生产者(Producer):生产者是负责将数据推送到Kafka主题的过程。它们充当数据流的源头,将消息发送到适当的主题。
  3. 消费者(Consumer):消费者订阅感兴趣的主题,并处理生产者推送的记录。它们充当数据流的接收器,消费来自主题的消息。
  4. 代理(Broker):Kafka集群由一个或多个代理组成,这些代理负责存储数据和管理跨主题记录的分发。代理充当生产者和消费者之间的中介。
  5. 分区(Partition):为了提高性能和可扩展性,每个主题可以划分为多个分区。分区允许在集群中进行并行处理和数据分布。每个分区都有一个领导者代理,负责处理所有读写请求,以及多个跟随者代理,用于复制领导者的数据。

Kafka架构的核心

在Kafka架构中,消息构成了系统的核心元素。生产者负责生成并将消息发送至特定主题,这些主题起到分类的作用。为了提升处理效率,这些主题进一步细分为多个分区。
消费者则订阅这些主题,并从相应的分区中获取消息。为确保负载均衡,每个分区同一时刻仅分配给单个消费者。消费者根据自身需求对消息进行处理,这可能包括数据分析、数据存储或其他应用场景。

这种架构使Kafka能够高效地处理大量数据流,提供容错性和可扩展性,现在我们已经了解了Kafka的工作原理,让我们深入了解代码!

在Spring Boot中设置Kafka:代码实现

在继续之前,请确保您的本地环境中已经运行了一个可用的Kafka服务。

将spring-kafka maven依赖项添加到pom.xml文件中。

<dependency>
   <groupId>org.springframework.kafka</groupId>
   <artifactId>spring-kafka</artifactId>
</dependency>

配置生产者

要开始创建消息,我们首先需要设置一个生产工厂(ProducerFactory)
接下来,我们使用KafkaTemplate,它包装了生产者实例,并提供简单的方法将消息发送到特定的Kafka主题。生产者实例设计为线程安全,这意味着在应用程序上下文中使用单个实例可以提高性能。

使用ProducerConfig属性配置生产者

@Configuration
public class KafkaProducerConfig {
    
    // bootstrapAddress Kafka服务地址
    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }

    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}

以下是使用的主要属性的说明:

  • BOOTSTRAP_SERVERS_CONFIG:此属性指定Kafka代理的地址,这些地址是逗号分隔的主机-端口对列表。
  • KEY_SERIALIZER_CLASS_CONFIGVALUE_SERIALIZER_CLASS_CONFIG:这些属性确定消息的键和值在发送到Kafka之前如何序列化。在此示例中,我们对键和值序列化都使用StringSerializer。
    bootstrapAddress则是配在配置文件中。
spring.kafka.bootstrap-servers=localhost:9092

创建Kafka主题:

在发送消息之前创建一个主题。
这里创建了具有1个分区(默认)的主题topic-1和具有3个分区的主题topic-2
TopicBuilder提供了用于创建主题的各种方法。

@Configuration
public class KafkaTopic {

    @Bean
    public NewTopic topic1() {
        return TopicBuilder.name("topic-1").build();
    }

    @Bean
    public NewTopic topic2() {
        return TopicBuilder.name("topic-2").partitions(3).build();
    }
}

发送消息:

KafkaTemplate提供多种方法可以将消息发送到主题:

@Component
@Slf4j
public class KafkaSender {

    @Autowired
    private KafkaTemplateString, String> kafkaTemplate;

        public void sendMessage(String message, String topicName) {
        log.info("发送 : {}", message);
        log.info("--------------------------------");

        kafkaTemplate.send(topicName, message);
    }
}

我们只需要调用send()方法并传递消息和主题名称作为参数即可发布消息。

配置消费者

KafkaMessageListenerContainerFactory单线程接收所有主题的所有消息。
另外还需要配置consumerFacotry。

@Configuration
@EnableKafka
public class KafkaConsumer {

    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public ConsumerFactoryString, String> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory<>(props);
    }
}

使用@KafkaListener注解来消费消息。它告诉Spring扫描带有@KafkaListener注解的bean,并配置处理Kafka消息所需的基本配置。

@Component
@Slf4j
public class KafkaListenerExample {

    @KafkaListener(topics = "topic-1", groupId = "group1")
    void listener(String data) {
        log.info("接受消息[{}] in 组ID", data);
    }
}

groupId是一个字符串,唯一标识此消费者所属的消费者组。我们可以在单个消费者组中指定多个要监听的主题。同样,多个方法可以监听同一主题。
还可以使用@Header()注解接收和消费者有关的其他元数据。

@KafkaListener(topics = "topic-1,topic-2", groupId = "group1")
void listener(@Payload String data,
              @Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
              @Header(KafkaHeaders.OFFSET) int offset) {
    log.info("接受消息 [{}] 来自group1, 分区号-{} 偏移量-{}",
            data,
            partition,
            offset);
}

一下是从特定分区并以初始偏移量来消费消息
在某些情况下,可能需要从Kafka主题的特定分区开始消费消息或者特定偏移量开始。
在你想处理特定消息或者想从任意位置开始消费时以下案例对于控制消费的颗粒度非常有用。

@KafkaListener(
  groupId = "group2",
  topicPartitions = @TopicPartition(topic = "topic-2",
  partitionOffsets = {
    @PartitionOffset(partition = "0", initialOffset = "0"), 
    @PartitionOffset(partition = "3", initialOffset = "0")}))
public void listenToPartition(
                                @Payload String message, 
                                @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
      log.info("接受消息 [{}] 分区号-{}", message, partition);
}

将initialOffset设置为"0"告诉Kafka从分区的开头开始消费消息。如果你只想指定分区而不设置initialOffset,按照下面的写法:

@KafkaListener(groupId = "group2", topicPartitions 
  = @TopicPartition(topic = "topicName", partitions = { "0", "3" }))

KafkaListner标记类级别

当你想将相关的消息处理逻辑分组在一起时,类级别标记会将主题的消息根据它们的参数分配到同类方法中。
通过下方代码则可以将方法分组,这些方法将从特定主题消费数据。
使用带有@KafkaHandler注解的方法消费不同类型的数据。
方法中的参数决定了数据的接收方式,如果没有匹配的数据类型,则使用默认方法。

@Component
@Slf4j
@KafkaListener(id = "class-level", topics = "multi-type")
class KafkaClassListener {

  @KafkaHandler
  void listenString(String message) {
    log.info("KafkaHandler [String] {}", message);
  }

  @KafkaHandler(isDefault = true)
  void listenDefault(Object object) {
    log.info("KafkaHandler [Default] {}", object);
  }
}

通过上方的教程读者应该基本掌握了生产者和消费者的基础知识,下面让我们继续探讨各种场景中的使用案例。

使用RoutingKafkaTemplate

在有多个不同配置的生产者时并且我们希望系统启动时根据主题名称选择一个生产者时就可以使用RoutingKafkaTemplate。

@Bean
public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context) {

    // ProducerFactory 字节序列化
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
    DefaultKafkaProducerFactory<Object, Object> bytesPF = new DefaultKafkaProducerFactory<>(props);
    context.registerBean(DefaultKafkaProducerFactory.class, "bytesPF", bytesPF);

    // 字符串序列化
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    DefaultKafkaProducerFactory<Object, Object> stringPF = new DefaultKafkaProducerFactory<>(props);

    Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();
    map.put(Pattern.compile(".*-bytes"), bytesPF);
    map.put(Pattern.compile("strings-.*"), stringPF);
    return new RoutingKafkaTemplate(map);
}

RoutingKafkaTemplate将消息路由到与主题名称匹配的第一个Factory实例。
下面这句话时从官方文档中翻译出来的,有些不顺口

从正则表达式和ProducerFactory实例的映射中选择
如果有两种模式,如str-.*strings-.*,则应首先放置strings-.*模式,否则str-.*模式将“覆盖”它。

在上面的示例中,我们创建了两个模式(-.-bytes和strings-.),至于使用何种消息序列化取决于运行时的主题的名称。
以’-bytes’结尾的主题将使用字节序列化。
而以’strings-.*’开头的主题名称将使用字符串序列化。

过滤消息

符合过滤条件的消息都会在被消费之前丢弃掉,以下案例:包含单词“ignored”的消息将被丢弃。

@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<String, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setRecordFilterStrategy(record -> record.value().contains("ignored"));
    return factory;
}

消费者被FilteringMessageListenerAdapter所封装。
此适配器依赖于RecordFilterStrategy的实现,我们在其中定义了过滤方法。在当前消费者工厂中,你只需简单地添加一行代码来调用该过滤器即可。

自定义消息

如何发送、接收Java对象。下面示例中,我们将发送和接收User对象。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    String msg;
}

生产者和消费者配置使用JSON序列化器:

@Bean
public ProducerFactory<String, User> userProducerFactory() {
    Map<String, Object> configProps = new HashMap<>();
    configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
    return new DefaultKafkaProducerFactory<>(configProps);
}

@Bean
public KafkaTemplate<String, User> userKafkaTemplate() {
    return new KafkaTemplate<>(userProducerFactory());
}

消费者端需要使用JSON反序列化器:

public ConsumerFactory<String, User> userConsumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer(User.class));
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, User> userKafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<String, User> factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(userConsumerFactory());
    return factory;
}

spring-kafka中的JSON序列化器和反序列化使用的是Jackson库。

<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.12.7.1</version>
</dependency>

这是一个可选依赖项,如果想使用它需要使用与spring-kafka相同的版本。

发送Java对象

发送User对象。

@Component
@Slf4j
public class KafkaSender {

    @Autowired
    private KafkaTemplate<String, User> userKafkaTemplate;


    void sendCustomMessage(User user, String topicName) {
        log.info("发送 Json Serializer : {}", user);
        log.info("--------------------------------");

        userKafkaTemplate.send(topicName, user);
    }
}

接收Java对象

@Component
@Slf4j
public class KafkaListenerExample {


    @KafkaListener(topics = "topic-3", groupId = "user-group", containerFactory = "userKafkaListenerContainerFactory")
    void listenerWithMessageConverter(User user) {
        log.info("接受消息 through MessageConverterUserListener [{}]", user);
    }
}

结论

在这篇关于Apache Kafka的文章中,我们首先介绍了基础知识,深入探讨了Kafka的核心概念。接着,我们详细说明了如何在Spring Boot应用程序中配置Kafka,并展示了如何使用Kafka模板和监听器进行消息的生产和消费。此外,我们还讨论了如何处理不同类型的消息、实现消息路由、进行消息过滤以及自定义数据格式的转换。

Kafka作为构建实时数据管道和事件驱动应用程序的多功能且强大的工具,我们希望本指南能为您提供必要的知识和技能,以便有效地利用其各项功能。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Eddie_920

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值