前言
最近搞了一个多线程灵活配置kafka消费链接的演示demo,过程还比较有意思,由于有一阵子没搞kafka了简单记录一下。
@KafkaListener
在类或方法上使用@KafkaListener注解定义消费者是最简单明了的一种方式,适用于处理简单的单个topic或者少量topic的情况,毕竟使用注解无法灵活扩展消费者。
虽然topic支持从配置文件中读取,且支持多个topic读取,但在某些场景下比较笨重,比如需要根据前端交互增加kafka消费者,使用注解显然实现不了,不可能前端注册一个kafka链接后端服务新生成一个class,在这个场景下用这方案在技术评审上会被喷死。
typescript
@Component
public class KafkaConsumer {
@KafkaListener(topics = "myTopic", groupId = "test1")
public void consume(String message) {
System.out.println("Received message: " + message);
}
}
这种比较适合模块与模块之间,或者服务之间,约定好topic建立通信的场景,比较简单且实用。
KafkaConsumer
根据刚才的场景,产品交互上有一个注册kafka链接的地方,前端填写完注册信息后传递给后端,后端需要生成一个kafka消费者不断监听数据,这种场景如何实现。
首先有多个用户创建消费链接,显然是一个多线程的场景,我们需要一个池子去管理kafka的链接,又是多线程又是池化,首先想到的线程池去管理,我们把kafka的消费链接和client做成线程私有的,启动的时候用线程池提交,这样线程池中的每一个线程都是一个kafka client。
我们使用apache.kafka包提供的现成的org.apache.kafka.clients.consumer.KafkaConsumer类
xml
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.0</version>
</dependency>
首先定义一个class实现Runable接口,每次一提Runable就想起校招的时候问的最多的就是实现多线程有哪几种方式。。。。
typescript
@Slf4j
@Component
public class KafkaConsumerTask implements Runnable {
// 每个线程维护私有的KafkaConsumer实例
@Getter
private KafkaConsumer<String, String> consumer;
@Getter
private String topicName;
/**
* 封装必要信息
* @param bootServer 生产者ip
* @param groupId 分组信息
* @param topic 订阅主题
*/
public KafkaConsumerRunnable(String bootServer, String groupId, String topic) {
topicName = topic;
Properties props = new Properties();
props.put("bootstrap.servers", bootServer);
props.put("group.id", groupId);
props.put("enable.auto.commit", "false");
props.put("auto.commit.interval.ms", "100"); //自动提交时间间隔 毫秒单位
props.put("session.timeout.ms", "30000");
props.put("auto.offset.reset", "latest");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); //键反序列化方式
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
this.consumer = new KafkaConsumer<>(props);
}
@Override
public void run() {
consumer.subscribe(Arrays.asList(topicName));
Thread.currentThread().setName(topicName);
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100); // 本例使用100ms作为获取超时时间
for (ConsumerRecord<String, String> record : records) {
String value = record.value();
log.info("线程 {} 消费kafka数据 -> {} \n 偏移量offset -> {} \n 分区partition -> {}",
Thread.currentThread().getName(), value, record.offset(), record.partition());
// 2、反序列化数据
HashMap resMap = JSON.parseObject(value, HashMap.class);
log.info("resMap -> {}", resMap);
}
}
} catch (WakeupException) {
log.info("consumer ");
} catch (Exception e) {
throw new HoraException("数据消费异常 -> ", e);
} finally {
consumer.commitAsync();
}
}
// 退出后关掉consumer
public void shutDown() {
consumer.wakeup();
}
}
代码贴出来结构就很明朗了,consumer每个线程私有变量,通过构造方法传参的方式初始化consumer,run方法里while true 去常驻监听数据,当需要退出后外部调用shutDown() 方法,内部consumer.wakeup() 方法调用后consumer会抛出异常跳出while循环结束任务。
提交任务时可以用线程池去提交,也可以自己搞个map维护链接都行,由于是个简单的演示demo,细节做的都不到位,还需要考虑下offset的维护,consumer链接挂了之后如何重新拉起,数据如何不丢失等等等问题。
不过工作又不是面试,xdm,我说的对吗。