目录
1. Kafka简介
Kafka是一种消息队列,主要用来处理大量数据状态下的消息队列,一般用来做日志的处理。既然是消息队列,那么
Kafka
也就拥有消息队列的相应的特性了。
2.kafka的相关配置
配置 | 作用 |
auto.commit.interval.ms | 自动提交偏移量的时间间隔,当消费者启用自动提交偏移量功能时,它会定期地将当前的偏移量提交到 Kafka 服务器,以确保消费者的进度被持久 auto.commit.interval.ms 参数指定了自动提交的时间间隔,单位是毫秒。 |
auto.offset.reset | 当消费者启动时或偏移量无效时,如何处理偏移量,默认值为
|
bootstrap.servers | kafka服务器的地址 |
check.crcs | 指定是否检查消息的CRC32,以确保消息的完整性 |
client.id | 客户端标识符,用于在服务器日志中追踪请求源 |
connections.max.idle.ms | 连接在关闭之前那可以保持空闲的最大时间,单位为毫秒,默认9分钟 |
default.api.timeout.ms | 默认api调用的超时时间,单位为毫秒,该参数用于定义 Kafka 客户端发起 API 请求时的默认超时时间,适用于消费者、生产者、管理员客户端等。超时时间的设定有助于防止操作因网络或系统问题而无限期地挂起。在达到这个超时时间后,如果请求没有成功完成,操作会终止并返回超时错误。 |
enable.auto.commit | 是否启用自动提交偏移量 |
exclude.internal.topics | 是否在自动偏移量管理中排除内部主题,内部主题是 Kafka 内部使用的特殊主题,通常用于存储一些系统元数据信息或者日志。这些主题对于普通应用程序来说并不需要被消费,因此可以通过设置 exclude.internal.topics 参数来排除它们,避免消费者拉取这些内部主题的消息。 |
fetch.max.bytes | 每个分区从服务器获取的最大数据量,单位为字节 |
fetch.max.wait.ms | 在发出拉取请求之前等待数据可用的最长时间,单位为毫秒,当消费者调用拉取消息的 API 时,它会指定一个等待时间,即在这段时间内等待服务器响应, fetch.max.wait.ms 属性定义了消费者在一次拉取请求中最长的等待时间。如果在这个时间内没有足够的数据可供拉取,则消费者将会收到较小数量的消息或者空响应。 |
fetch.min.bytes | 每个分区从服务器获取的最小数据量,单位为字节 |
group.id | 消费者所属的消费者组标识符 |
heartbeat.interval.ms | 心跳间隔的频率,用于维持与消费者组协调器的活动连接,消费者在与消费者组协调器(Consumer Group Coordinator)通信时,会定期发送心跳以表明自己的存活状态。这些心跳用于告知协调器消费者仍然处于活动状态。heartbeat.interval.ms 属性定义了发送心跳的频率,即消费者多久发送一次心跳。 |
isolation.level | isolation.level 参数用于设置消费者的隔离级别,它决定了消费者在读取消息时能够看到的数据。默认情况下,isolation.level 的值是 read_uncommitted ,即读取未提交的数据 |
key.deserializer | 键的反序列化 |
max.partition.fetch.bytes | 单次调用 poll() 可以返回的最大数据量,控制消费者在单次拉取请求中可以获取的单个分区的最大数据量(以字节为单位),默认值是 1MB(1,048,576 字节),当消费者从 Kafka 服务器拉取消息时,它会向服务器发送一个拉取请求,指定要拉取的分区以及每个分区的偏移量和最大拉取字节数。max.partition.fetch.bytes 属性就是用来限制每个分区拉取的最大字节数。如果某个分区的消息大小超过了这个限制,那么该分区的消息可能会被截断,消费者只能获取部分消息。 |
max.poll.interval.ms | 消费者处理消息的最大时间间隔,用于检测消费者故障 |
max.poll.records | 每次调用poll()最多返回的记录数,默认500条 |
metadata.max.age.ms | 元数据的最大缓存时间,用于控制消费者是否更新分区元数据,消费者在启动时会从服务器获取最新的元数据信息,以便知道从哪些分区拉取消息。metadata.max.age.ms 属性定义了消费者在多久之后应该重新获取元数据信息。在这段时间内,消费者将会重用之前获取的元数据信息,而不会向服务器发起新的元数据请求。 |
3. 需求:从kafka中动态获取数据,可指定条数
4.功能点:
1.kafka的配置
2.如何从kafka中获取数据,如何从kafka中拉取数据
3.如何获取指定的条数的数据
4.如何只提交“获取数据”的偏移量
5.实现步骤:
5-1、kafka的pom文件
<!-- Spring Kafka --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <!-- Spring for Apache Kafka --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifatId> </dependency> <!-- Kafka Clients --> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> </dependency>
package com.xldatacloud.monitorserverapi.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Properties;
@Component
public class KafkaConfig {
private static Logger logger = Logger.getLogger(KafkaConfig.class);
public static KafkaConsumer<String, String> consumer;
@Value("${spring.kafka.bootstrap-servers}")
public String bootstrapServers;
public String groupId = "subscriptionConsumer";
public String topic = "subject_9920";
@Value("${spring.kafka.consumer.enable-auto-commit}")
public boolean enableAutoCommit;
@Value("${spring.kafka.consumer.auto-offset-reset}")
public String autoOffsetReset;
@Value("${spring.kafka.consumer.properties.sasl.jaas.config}")
public String saslJaasConfig;
@Value("${spring.kafka.consumer.properties.sasl.mechanism}")
public String saslMechanism;
@Value("${spring.kafka.consumer.properties.security.protocol}")
public String securityProtocol;
public final String SASL_JAAS_CONFIG = "sasl.jaas.config";
public final String SASL_MECHANISM = "sasl.mechanism";
public final String SECURITY_PROTOCOL = "security.protocol";
@PostConstruct
public void init() {
initDATA(groupId,topic);
}
public void updateConfig(String groupId, String topic) {
// 初始化新的 KafkaConsumer 实例
initDATA(groupId, topic);
}
public void initDATA(String groupId, String topic) {
logger.info("当前groupId为"+groupId+",当前topic为"+topic);
// 初始化Kafka消费者
Properties props = new Properties();
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);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
props.put(SASL_JAAS_CONFIG, saslJaasConfig);
props.put(SASL_MECHANISM, saslMechanism);
props.put(SECURITY_PROTOCOL, securityProtocol);
props.put("max.poll.records", "100"); // 默认设置为500条记录
consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic));
}
public KafkaConsumer<String, String> getConsumer() {
return consumer;
}
}
5-2、初版代码功能实现
初版代码存在得问题:接口调用时间过长,大致在2~3秒,分析后得知,当前代码每次在接口调用的时候,都会新建消费者实例,导致接口调用之间过长,在终版代码中已解决这个问题,已将时间优化到100ms以内。
package com.xldatacloud.monitorserverapi.controller;
import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xldatacloud.monitorserverapi.common.LocalData;
import com.xldatacloud.monitorserverapi.common.ResultEntity;
import com.xldatacloud.monitorserverapi.config.KafkaConfig;
import com.xldatacloud.monitorserverapi.entity.datasubscription.Subscription;
import com.xldatacloud.monitorserverapi.utils.DateHelper;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class KafkaConsumerController {
private static Logger logger = Logger.getLogger(KafkaConsumerController.class);
private static KafkaConsumer<String, String> consumer;
private final KafkaConfig kafkaConfig;
@Value("${subscription.data.send.count}")
private int sendCount;
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
public KafkaConsumerController(KafkaConfig kafkaConfig) {
this.kafkaConfig = kafkaConfig;
this.consumer = kafkaConfig.getConsumer();
}
@PostMapping(test/list")
public ResultEntity consumeKafka(@RequestBody String param) {
String date = DateHelper.getCurrentDateTime();
try {
JSONObject jsonObject = JSONObject.parseObject(param);
Integer count = (Integer)jsonObject.get("count");
String organization = (String)jsonObject.get("organization");
logger.info("推送数据开始,调用时间为" + date);
logger.info("客户传递的参数: count = " + count + ", organization = " + organization);
// 对数据进行校验
ResultEntity checkResult = checkData(organization);
if (checkResult != null) return checkResult;
// 动态设定groupId、topic
kafkaConfig.updateConfig((groupId + organization), ("subject_" + organization));
this.consumer = kafkaConfig.getConsumer();
// 如果没传返回条数大小,则设置为默认10
sendCount = count == null ? sendCount : count;
// 如果count不在0到200之间,返回一个包含错误信息的Result对象
if (count != null && (count <= 0 || count > 200)) {
return ResultEntity.fail(-10001, "发送条数必须在1到200之间");
}
logger.info("本次返回条数限定为【 " + sendCount + " 】条");
List<String> msgList = new ArrayList<>();
Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = new HashMap<>();
// 获取kafka中的数据
List<ConsumerRecord<String, String>> allRecords = new ArrayList<>();
logger.info("从kafka中获取数据-----》开始");
long startTime = System.currentTimeMillis();
int i =0;
Iterable<ConsumerRecord<String, String>> recordList = consumer.poll(Duration.ofSeconds(10)).records("subject_" + organization);
for (ConsumerRecord<String, String> record : recordList) {
ConsumerRecord<String, String> consumerRecord = new ConsumerRecord<>(record.topic(),record.partition(),record.offset(),record.key(),record.value());
allRecords.add(consumerRecord);
if (sendCount == ++i){
break;
}
}
long endTime = System.currentTimeMillis();
logger.info("从kafka中获取数据-----》结束");
logger.info("----------从kafka服务器上获取数据总用时----------"+(endTime-startTime)+"ms");
logger.info("对获取的数据进行json转换-----》开始");
startTime = System.currentTimeMillis();
for (ConsumerRecord<String, String> record : allRecords) {
// json转换
Subscription subscription = JSON.parseObject(record.value(), Subscription.class);
String sendJsonData = JSON.toJSONString(subscription);
msgList.add(sendJsonData);
// 提交当前记录的偏移量
TopicPartition topicPart = new TopicPartition(record.topic(), record.partition());
long nextOffset = record.offset() + 1;
offsetsToCommit.put(topicPart, new OffsetAndMetadata(nextOffset));
// 每次推送的数据量限制
if (msgList.size() == sendCount) {
break;
}
}
endTime = System.currentTimeMillis();
logger.info("对获取的数据进行json转换-----》结束");
logger.info("----------对获取的数据进行数据封装总用时----------"+(endTime-startTime)+"ms");
// 如果存在偏移量,则提交;记录接口调用次数
if (!offsetsToCommit.isEmpty()) {
consumer.commitSync(offsetsToCommit);
logger.info("推送数据,推送数量为:" + msgList.size() + ",推送的数据为:" + msgList);
}
return ResultEntity.ok(msgList);
} catch (Exception e) {
e.printStackTrace();
logger.error("推送的数据发生异常,调用时间为" + date + ",报错原因为" + e);
return ResultEntity.fail(-1, "请求失败");
} finally {
logger.info("推送数据结束,调用时间为" + date);
consumer.close();
}
}
/**
* 数据校验
* @param organization
* @return
*/
private static ResultEntity checkData(String organization) {
// 校验机构id是否传递
if (StringUtils.isBlank(organization)) {
logger.info("机构id未传递");
return ResultEntity.fail(-10001, "机构id未传递");
}
// 从threadlocl中获取机构信息
List<Long> orgList = LocalData.getOrgList();
// 校验传递的机构id是否正确
boolean flag = orgList.stream().anyMatch(org -> org.equals(Long.parseLong(organization)));
if (!flag) {
logger.info("机构信息不匹配,请确认机构id是否正确");
return ResultEntity.fail(-10001, "机构信息不匹配,请确认机构id是否正确");
}
return null;
}
}
5-3、终版代码功能实现-对初版代码进行优化(提高响应速度)
优化接口调用时间,目前接口调用已经提到100ms以内,在多次进行接口调用时,只需第一次新建消费者实例,后续不在新建消费者,避免了客户端与服务器连接的耗时问题。
import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xldatacloud.monitorserverapi.common.LocalData;
import com.xldatacloud.monitorserverapi.common.ResultEntity;
import com.xldatacloud.monitorserverapi.config.KafkaConfig;
import com.xldatacloud.monitorserverapi.entity.datasubscription.Subscription;
import com.xldatacloud.monitorserverapi.utils.DateHelper;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@RestController
@RequestMapping("test")
public class KafkaConsumerController {
private static Logger logger = Logger.getLogger(KafkaConsumerController.class);
public static ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
private static KafkaConsumer<String, String> consumer;
private final KafkaConfig kafkaConfig;
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
public KafkaConsumerController(KafkaConfig kafkaConfig) {
this.kafkaConfig = kafkaConfig;
this.consumer = kafkaConfig.getConsumer();
}
@PostMapping("/list")
public ResultEntity consumeKafka(@RequestBody String param) {
String date = DateHelper.getCurrentDateTime();
try {
logger.info("推送数据开始,调用时间为" + date);
JSONObject jsonObject = JSONObject.parseObject(param);
String organization = (String) jsonObject.get("organization");
// 增加一个标识,方便做测试
String tag = (String) jsonObject.get("tag");
logger.info("客户传递的参数:" + "organization = " + organization);
// 对数据进行校验
ResultEntity checkResult = checkData(organization);
if (checkResult != null) return checkResult;
// 动态设定groupId、topic,增加这个tag方便做测试
String key = "kafka_consumer_" + organization + tag;
if (map.get(key) == null) {
map.put(key, "organization");
kafkaConfig.updateConfig((groupId + organization + tag), ("subject_" + organization));
this.consumer = kafkaConfig.getConsumer();
}
// 从kafka中获取数据,并进行json转换
List<String> msgList = getList(organization);
// 提交偏移量
consumer.commitSync();
logger.info("本次推送数量为:" + msgList.size() + ",推送的数据为:" + msgList);
return ResultEntity.ok(msgList);
} catch (Exception e) {
e.printStackTrace();
logger.error("推送的数据发生异常,调用时间为" + date + ",报错原因为" + e);
return ResultEntity.fail(-1, "请求失败");
} finally {
logger.info("推送数据结束,调用时间为" + date);
}
}
/**
* 从kafka中获取数据,并进行json转换
*
* @param organization
* @return
*/
private static List<String> getList(String organization) {
List<String> msgList = new ArrayList<>();
// 获取kafka中的数据
List<ConsumerRecord<String, String>> allRecords = new ArrayList<>();
logger.info("从kafka中获取数据-----》开始");
long startTime = System.currentTimeMillis();
Iterable<ConsumerRecord<String, String>> recordList = consumer.poll(Duration.ofSeconds(10)).records("subject_" + organization);
for (ConsumerRecord<String, String> record : recordList) {
ConsumerRecord<String, String> consumerRecord = new ConsumerRecord<>(record.topic(), record.partition(), record.offset(), record.key(), record.value());
allRecords.add(consumerRecord);
}
long endTime = System.currentTimeMillis();
logger.info("----------从kafka服务器上获取数据总用时----------" + (endTime - startTime) + "ms");
logger.info("从kafka中获取数据-----》结束");
logger.info("对获取的数据进行json转换-----》开始");
startTime = System.currentTimeMillis();
msgList = allRecords.stream().map(record -> {
Subscription subscription = JSON.parseObject(record.value(), Subscription.class);
return JSON.toJSONString(subscription);
}).collect(Collectors.toList());
endTime = System.currentTimeMillis();
logger.info("----------进行json数据封装总用时----------" + (endTime - startTime) + "ms");
logger.info("对获取的数据进行json转换-----》结束");
return msgList;
}
/**
* 数据校验
*
* @param organization
* @return
*/
private static ResultEntity checkData(String organization) {
// 校验机构id是否传递
if (StringUtils.isBlank(organization)) {
logger.info("机构id未传递");
return ResultEntity.fail(-10001, "机构id未传递");
}
// 从ThreadLocal中获取机构信息
List<Long> orgList = LocalData.getOrgList();
logger.info("本人所包含的机构为: " + orgList);
// 校验传递的机构id是否正确
boolean flag = orgList.stream().anyMatch(org -> org.equals(Long.parseLong(organization)));
if (!flag) {
logger.info("机构信息不匹配,当前客户端传递的机构id为 " + organization);
return ResultEntity.fail(-10001, "机构信息不匹配,请确认机构id是否正确");
}
return null;
}
}