Springboot集成Kafka

引入Kafka依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

Kafka配置

  1. application.yml
yaml 复制代码
server:
  port: 8000
spring:
  kafka:

    # 生产者
    producer:
      # Kafka服务器
      bootstrap-servers: 42.194.132.44:9092
      # 开启事务,必须在开启了事务的方法中发送,否则报错
      transaction-id-prefix: TxKafka-
      # 发生错误后,消息重发的次数,开启事务必须设置大于0。
      retries: 3
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      # 开启事务时,必须设置为all
      acks: all
      # 当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
      batch-size: 16384
      # 生产者内存缓冲区的大小。
      buffer-memory: 1024000
      # 键的序列化方式
      key-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      # 值的序列化方式(建议使用Json,这种序列化方式可以无需额外配置传输实体类)
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

    # 消费者
    consumer:
      # Kafka服务器
      bootstrap-servers: 42.194.132.44:9092
      group-id: oceanGroup
      # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
      #auto-commit-interval: 2s
      # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费分区的记录
      # latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据(在消费者启动之后生成的记录)
      # none:当各分区都存在已提交的offset时,从提交的offset开始消费;只要有一个分区不存在已提交的offset,则抛出异常
      auto-offset-reset: earliest
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
      enable-auto-commit: false
      # 键的反序列化方式
      #key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      key-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      # 值的反序列化方式(建议使用Json,这种序列化方式可以无需额外配置传输实体类)
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      # 配置消费者的 Json 反序列化的可信赖包,反序列化实体类需要
      properties:
        spring:
          json:
            trusted:
              packages: "*"
      # 这个参数定义了poll方法最多可以拉取多少条消息,默认值为500。如果在拉取消息的时候新消息不足500条,那有多少返回多少;如果超过500条,每次只返回500。
      # 这个默认值在有些场景下太大,有些场景很难保证能够在5min内处理完500条消息,
      # 如果消费者无法在5分钟内处理完500条消息的话就会触发reBalance,
      # 然后这批消息会被分配到另一个消费者中,还是会处理不完,这样这批消息就永远也处理不完。
      # 要避免出现上述问题,提前评估好处理一条消息最长需要多少时间,然后覆盖默认的max.poll.records参数
      # 注:需要开启BatchListener批量监听才会生效,如果不开启BatchListener则不会出现reBalance情况
      max-poll-records: 50
    properties:
      # 两次poll之间的最大间隔,默认值为5分钟,如果超过这个间隔会触发reBalance
      max:
        poll:
          interval:
            ms: 300000
      # 当broker多久没有收到consumer的心跳请求后就触发reBalance,默认值是10s
      session:
        timeout:
          ms: 10000

    # 侦听器
    listener:
      # 侦听器运行的线程数,一般设置为 分区数 / 服务节点数,Kafka分区数为5,一个节点消费,所以这里我设置为5
      concurrency: 5
      # 自动提交关闭,需要设置手动消息确认
      ack-mode: manual
      # 消费监听接口监听的主题不存在时,默认会报错,设置为false忽略错误
      missing-topics-fatal: false
      # 两次poll之间的最大间隔,默认值为5分钟。如果超过这个间隔会触发reBalance
      poll-timeout: 600000
  1. 生产者Kafka配置
java 复制代码
package com.angel.ocean.kafka.config;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;
import org.springframework.kafka.transaction.KafkaTransactionManager;
import java.util.HashMap;
import java.util.Map;

/**
 * kafka 生产者配置
 */
@SpringBootConfiguration
public class KafkaProviderConfig {

    @Value("${spring.kafka.producer.bootstrap-servers}")
    private String bootstrapServers;
    @Value("${spring.kafka.producer.transaction-id-prefix}")
    private String transactionIdPrefix;
    @Value("${spring.kafka.producer.acks}")
    private String acks;
    @Value("${spring.kafka.producer.retries}")
    private String retries;
    @Value("${spring.kafka.producer.batch-size}")
    private String batchSize;
    @Value("${spring.kafka.producer.buffer-memory}")
    private String bufferMemory;

    @Bean
    public Map<String, Object> producerConfigs() {
        Map<String, Object> props = new HashMap<>(16);
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ProducerConfig.ACKS_CONFIG, acks);
        props.put(ProducerConfig.RETRIES_CONFIG, retries);
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
        props.put(ProducerConfig.LINGER_MS_CONFIG, "5000");
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return props;
    }

    @Bean
    public ProducerFactory<Object, Object> producerFactory() {
        DefaultKafkaProducerFactory<Object, Object> factory = new DefaultKafkaProducerFactory<>(producerConfigs());
        // 开启事务,会导致 LINGER_MS_CONFIG 配置失效
        factory.setTransactionIdPrefix(transactionIdPrefix);
        return factory;
    }

    @Bean
    public KafkaTransactionManager<Object, Object> kafkaTransactionManager(ProducerFactory<Object, Object> producerFactory) {
        return new KafkaTransactionManager<>(producerFactory);
    }

    @Bean
    public KafkaTemplate<Object, Object> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}
  1. 消费者Kafka配置
java 复制代码
package com.angel.ocean.kafka.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import java.util.HashMap;
import java.util.Map;

/**
 * kafka消费者配置
 */
@SpringBootConfiguration
public class KafkaConsumerConfig {

    @Value("${spring.kafka.consumer.bootstrap-servers}")
    private String bootstrapServers;
    @Value("${spring.kafka.consumer.group-id}")
    private String groupId;
    @Value("${spring.kafka.consumer.enable-auto-commit}")
    private boolean enableAutoCommit;
    @Value("${spring.kafka.properties.session.timeout.ms}")
    private String sessionTimeout;
    @Value("${spring.kafka.properties.max.poll.interval.ms}")
    private String maxPollIntervalTime;
    @Value("${spring.kafka.consumer.max-poll-records}")
    private String maxPollRecords;
    @Value("${spring.kafka.consumer.auto-offset-reset}")
    private String autoOffsetReset;
    @Value("${spring.kafka.listener.concurrency}")
    private Integer concurrency;
    @Value("${spring.kafka.listener.missing-topics-fatal}")
    private boolean missingTopicsFatal;
    @Value("${spring.kafka.listener.poll-timeout}")
    private long pollTimeout;

    @Bean
    public Map<String, Object> consumerConfigs() {

        Map<String, Object> propsMap = new HashMap<>(16);
        propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
        propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "2000");
        propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
        propsMap.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalTime);
        propsMap.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
        propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout);
        propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);

        return propsMap;
    }

    @Bean
    public ConsumerFactory<Object, Object> consumerFactory() {

        // 配置消费者的Json反序列化的可信赖包,反序列化实体类时需要
        try (JsonDeserializer<Object> deserializer = new JsonDeserializer<>()) {
            deserializer.trustedPackages("*");
            return new DefaultKafkaConsumerFactory<>(consumerConfigs(), new JsonDeserializer<>(), deserializer);
        }
    }

    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Object, Object>> singleFactory() {
        ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        // 侦听器运行的线程数,多个服务节点设置的concurrency之和不超过Kafka分区数
        factory.setConcurrency(concurrency);
        // 消费监听接口监听的主题不存在时,默认会报错,设置为false可以忽略该错误
        factory.setMissingTopicsFatal(missingTopicsFatal);
        // 设置手动消息确认,让消费端更灵活的控制数据的消费,一定程度上可以保证数据一致性
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        factory.getContainerProperties().setPollTimeout(pollTimeout);
        return factory;
    }

    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Object, Object>> batchFactory() {
        ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        // 侦听器运行的线程数,多个服务节点设置的concurrency之和不超过Kafka分区数
        factory.setConcurrency(concurrency);
        // 消费监听接口监听的主题不存在时,默认会报错,设置为false可以忽略该错误
        factory.setMissingTopicsFatal(missingTopicsFatal);
        // 设置手动消息确认,让消费端更灵活的控制数据的消费,一定程度上可以保证数据一致性
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        factory.getContainerProperties().setPollTimeout(pollTimeout);
        // 设置为批量监听,需要用List接收数据
        factory.setBatchListener(true);
        return factory;
    }
}

Kafka 使用验证

  1. 发消息到kafka
java 复制代码
package com.angel.ocean.kafka;

import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson2.JSON;
import com.angel.ocean.kafka.domain.UserInfo;
import com.angel.ocean.kafka.service.KafkaService;
import com.angel.ocean.kafka.thread.KafkaSendThread;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.*;

@Slf4j
@SpringBootTest
class ApplicationTest {

    @Resource
    private KafkaService kafkaService;

    private final static ExecutorService pool = new ThreadPoolExecutor(20, 50, 2L, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(5000), new ThreadFactoryBuilder().build(),
            new ThreadPoolExecutor.DiscardPolicy());

    @Test
    void test() {

        for (int i = 0; i < 5000; i++) {
            UserInfo userInfo = new UserInfo(i, "Name" + i, RandomUtil.randomString(32), new Date());
            Integer partition = userInfo.getUserId() % 5;
            KafkaSendThread thread = new KafkaSendThread(kafkaService, partition, JSON.toJSONString(userInfo));
            pool.submit(thread);
        }

    }
}
java 复制代码
package com.angel.ocean.kafka.thread;

import com.angel.ocean.kafka.constant.KafkaTopicConstant;
import com.angel.ocean.kafka.service.KafkaService;

public class KafkaSendThread extends Thread {

    private final KafkaService kafkaService;
    private final Integer partition;
    private final String data;

    public KafkaSendThread(KafkaService kafkaService, Integer partition, String data) {
        this.kafkaService = kafkaService;
        this.partition = partition;
        this.data = data;
    }

    @Override
    public void run() {
        kafkaService.sendData(KafkaTopicConstant.USER_INFO_TOPIC, partition, data);
    }
}
java 复制代码
package com.angel.ocean.kafka.service;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class KafkaService {

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    @Transactional
    public void sendData(String topic, String key, String data) {
        kafkaTemplate.send(topic, null, key, data);
    }

    @Transactional
    public void sendData(String topic, Integer partition, String data) {
        kafkaTemplate.send(topic, partition, null, data);
    }
}
  1. Kafka数据消费
java 复制代码
package com.angel.ocean.kafka.listener;

import com.angel.ocean.kafka.constant.KafkaTopicConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
import java.util.List;

@Slf4j
@Component
public class UserInfoConsumerListener {

    /**
     * 单条消费
     */
    @KafkaListener(topics = KafkaTopicConstant.USER_INFO_TOPIC, containerFactory = "singleFactory", groupId = "userInfoGroup")
    public void listen(ConsumerRecord<String, String> record, Acknowledgment ack) {
        try {
            log.info("UserInfoConsumerListener.listen(), record: {}", record.value());
        } catch (Exception e) {
            log.error("UserInfoConsumerListener.listen() Exception:{}", e.getMessage(), e);
        } finally {
            // 手动确认
            ack.acknowledge();
        }
    }

    /**
     * 批量消费
     */
    @KafkaListener(topics = KafkaTopicConstant.USER_INFO_TOPIC, containerFactory = "batchFactory", groupId = "batchUserInfoGroup")
    public void batchListen(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
        try {
            log.info("UserInfoConsumerListener.batchListen(), records.size: {}", records.size());
            for (ConsumerRecord<String, String> record : records) {
                Thread.sleep(100);
            }
        } catch (Exception e) {
            log.error("UserInfoConsumerListener.batchListen() Exception:{}", e.getMessage(), e);
        } finally {
            // 手动确认
            ack.acknowledge();
        }
    }
}

由于Kafka分区数为5,如下图:

而且发送数据时,指定了分区,将数据均匀发送到Kafka各个分区中,所以本文Kafka concurrency配置设置为5,即五个线程都能消费到消费Kafka数据。

单条消费截图:可以看到有五个线程消费了Kafka的数据。

批量消费截图:可以看到有五个线程消费了Kafka的数据,每次最多消费50条,跟我们配置的 max-poll-records: 50 一致。

那如果我们把Kafka concurrency配置设置为10 (实际Kafka分区数为5),会有几个线程能消费到数据呢?

图1:

图2:

从图1和图2对比可以看出,本地确实起了10个线程想要去消费Kafka的数据,但是最终只有5个线程消费到了Kafka的数据,其他线程都浪费了,因此,我们可以得出这样的结论,服务节点的concurrency配置之和最好不要超过Kafka实际数据分区数。

注意事项

  1. Kafka 服务类发送消息时要开启事务(因为配置里面配置了),为了保证生产者发送消息不丢失
  1. 侦听器运行的线程数,一般设置为 Kafa 分区数 / 服务节点数
  1. 批次拉取的数据量不应设置过大,要结合业务耗时去设置
yaml 复制代码
max-poll-records: 50
  1. 要设置成手动提交,消费者自己去ack,可以减少数据丢失的风险
yaml 复制代码
enable-auto-commit: false
ack-mode: manual
相关推荐
罗政2 小时前
PDF书籍《手写调用链监控APM系统-Java版》第10章 插件与链路的结合:SpringBoot环境插件获取应用名
java·spring boot·pdf
sin22012 小时前
springboot测试类里注入不成功且运行报错
spring boot·后端·sqlserver
得谷养人3 小时前
flink-1.16 table sql 消费 kafka 数据,指定时间戳位置消费数据报错:Invalid negative offset 问题解决
sql·flink·kafka
kirito学长-Java4 小时前
springboot/ssm网上宠物店系统Java代码编写web宠物用品商城项目
java·spring boot·后端
键盘侠0074 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp
AI人H哥会Java5 小时前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
计算机学长felix5 小时前
基于SpringBoot的“大学生社团活动平台”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·后端
sin22015 小时前
springboot数据校验报错
spring boot·后端·python
大大怪将军~~~~6 小时前
SpringBoot 入门
java·spring boot·后端