kafka java 多线程_实现一个简单的Kafka多线程消费模型

最近项目上用到了Kafka(作为数据源接入),这里将自己的实践分享出来,供大家参考或针砭。

从网上查阅资料发现,基本上有2中与Kafka对接的方式:

1.Spring-Kafka2.调用Kafka API自己实现ConsumerClient

Spring-Kafka的基本原理就是Spring自动轮询Poll数据,通过监听器MessageListener.onMessage()向用户自定义的消费入口(@KafkaListener)推送数据。因此对于用户来说,仅需要关注自己的业务实现即可,Kafka数据对于业务来说就是一个方法的入参而已。这种设计很有意思,因为Kafka是不支持主动Push的,但是Spring-Kafka自己实现了这种角色反转。Spring-Kafka本身就是一个很好的实现,而且上手相对简单,推荐大家使用这种方式。

温馨提示:Spring-Kafka和kafka-clients之间有版本的兼容性问题需特别注意,另外如果你使用SpringBoot开发的话也需要匹配特定的版本。

kafka java 多线程_实现一个简单的Kafka多线程消费模型_第1张图片

kafka java 多线程_实现一个简单的Kafka多线程消费模型_第2张图片

#Spring-Kafka KafkaConsumer消费模型(来源于网络)

kafka java 多线程_实现一个简单的Kafka多线程消费模型_第3张图片

不过抱着学习研究的目的,本篇选择第2中实现方式,其实和Spring-Kafka殊途同归。

直奔主题,本篇就不阐述太多理论性的东西,仅介绍一些基本的Kafka API对象和概念:

1.KafkaConsumer,顾名思义就是Kafka的数据消费者,其主要作用是连接Kafka订阅(subscribe)相关主题(topic)并拉取(poll)数据并提交消费偏移(offset)。2.ConsumerRecord,Kafka数据接收记录,其中有些重要的属性:topic(主题),patition(分区),offset(偏移),key(主键),value(数据值)。

PS:KafkaConsumer是非线程安全的

对于一个Kafka消费客户端有些基本的配置:

1.bootstrap.servers--连接Kafka集群的地址,多个地址以逗号分隔2.key.deserializer--消息中key反序列化类,需要和Producer中key序列化类相对应3.value.deserializer--消息中value的反序列化类,需要和Producer中Value序列化类相对应4.group.id--消费者所属消费组的唯一标识

为了提高单线程消费Kafka数据的效率,我们要在此基础上创建一个专门用于处理数据的线程池。简单来说,就是一个线程只用来Poll数据,然后丢给线程池去处理。

消费线程:KafkaConsumerClient.java

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

/*** 消费线程

*

*@authorlichmama

**/

public class KafkaConsumerClient extendsThread {/**读取超时 **/

private static final int timeout = 5000;/**核心线程数 **/

private static final int corePoolSize = 5;/**最大线程数 **/

private static final int maximumPoolSize = 20;/**空闲存活时间 **/

private static final long keepAliveTime = 30L;/**队列容量 **/

private static final int capacity = 10000;/**告警数据处理线程池 **/

private ExecutorService executor = newThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,

TimeUnit.SECONDS,new LinkedBlockingQueue<>(capacity));/**kafka配置 **/

privateProperties props;/**kafka主题 **/

privateString topics;private KafkaConsumer consumer = null;privateKafkaConsumerClient(Properties props, String topics) {super("KafkaConsumerClient");this.props =props;this.topics =topics;

}

@Overridepublic voidrun() {

System.out.println("KafkaConsumerClient is running...");try{

consumer= new KafkaConsumer(props);

consumer.subscribe(Collections.singletonList(topics));

}catch(Exception e) {

e.printStackTrace();return;

}try{while (true) { //循环读取

consumer.poll(timeout).forEach(record ->{

process(record);

});

commitOffset();

}

}finally{

System.out.println("KafkaConsumerClient terminated for some unexpected exception!");

consumer.close();

}

}/*** 提交偏移*/

private voidcommitOffset() {try{

consumer.commitAsync();

}catch(Exception e) {

System.out.println("异步提交失败,尝试主动提交。。。");

consumer.commitSync();

}

}/*** 处理数据

*

*@paramrecord*/

private void process(ConsumerRecordrecord) {try{

executor.submit(newKafkaDataProcessor(record));

}catch(Exception e) {

e.printStackTrace();

}

}public static voiddoStart(Properties props, String topics) {

KafkaConsumerClient client= newKafkaConsumerClient(props, topics);//异常退出后自动重启

client.setUncaughtExceptionHandler(newUncaughtExceptionHandler() {

@Overridepublic voiduncaughtException(Thread t, Throwable throwable) {

System.out.println("KafkaConsumerClient异常退出,重启中。。。");try{

Thread.sleep(1000 * 5); //等待5秒钟

} catch(InterruptedException e) {

e.printStackTrace();

}

KafkaConsumerClient.doStart(props, topics);

}

});

client.start();

}

}

View Code

数据处理线程:KafkaDataProcessor.java

/*** 处理线程

*@authorlichmama

**/

public class KafkaDataProcessor extendsThread {private ConsumerRecordrecord;public KafkaDataProcessor(ConsumerRecordrecord) {this.record =record;

}

@Overridepublic voidrun() {

System.out.println(String.format("topic:%s, partition:%d, offset:%d, message:%s",

record.topic(), record.partition(), record.offset(), record.value()));

}

}

启动程序:StartKafkaConsumer.java

public classStartKafkaConsumer {public static void main(String[] args) throwsException {

Properties props= newProperties();

props.load(new FileInputStream("consumer-config.properties"));

String topics= (String) props.remove("topics");//启动消费线程

KafkaConsumerClient.doStart(props, topics);

}

}

kafka消费者配置:consumer-config.properties

##-*- 消费者配置 -*-##

#kafka集群地址

bootstrap.servers=localhost:9092#消费者归属组ID

group.id=test_group

#单次最大拉取记录数

max.poll.records=20#关闭自动提交

enable.auto.commit=false#key反序列化类名

key.deserializer=org.apache.kafka.common.serialization.StringDeserializer

#value反序列化类名

value.deserializer=org.apache.kafka.common.serialization.StringDeserializer

#订阅主题

topics=test

#本例使用JDK8开发,kafka-clients版本为0.10.2.0

根据实际情况来调整线程池的队列长度、线程数以及单次最大拉取记录数,才能确保程序运行良好,否则可能会造成告警丢失。

因为Kafka有分区的概念,每个topic可能有N个partition,为了更进一步提高消费速度可以同时启动M(M<=N)个KafkaConsumerClient。需要特别说明的是,如果M>N的话,那么多余的Client是无法Poll到数据的,这是Kafka的特性所决定的。

本篇到这基本就结束了,快把程序跑起来看看效果吧。

你可能感兴趣的:(kafka,java,多线程)