1. Kafka更新历史
对kafka版本的理解,普遍分为了两个大版本:0.9版本之前,0.9版本之后(包含)。两个大版本对offset的存储管理有很大的改革。0.9+之后的版本,将offset的存储管理迁移到了kafka内部,减轻zk对offset频繁的维护带来的压力。
Version | offset存储位置 |
0.9之前 | zk:/consumer/groupid/topic/partition |
0.9之后(包含) | kafka topic:__consumer_offsets 也可兼容 zk 存储offset |
2. 自定义offset存储
在实际开发中,不免会碰到对offset的自定义存储,使用较多的是借助MySQL, es, redis等第三方组件进行存储管理。最近的个人学习中,自己采用了较为常用便捷的MySQL进行存储。
1. 创建 custom_offsets 表,用于存储 offset 信息
-- custom_offsets definition
CREATE TABLE `custom_offsets` (
`group_id` varchar(100) NOT NULL,
`topic_name` varchar(100) NOT NULL,
`partition_id` int(10) NOT NULL,
`offset` int(10) NOT NULL,
`create_time` varchar(100) NOT NULL DEFAULT '1990-01-01 00:00:00',
`update_time` varchar(100) NOT NULL DEFAULT '1990-01-01 00:00:00',
PRIMARY KEY (`group_id`,`topic_name`,`partition_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表格创建后,开始代码的处理。
2. 自定存储offset的关键是需要自定义一个 ConsumerRebalanceListener 类
使用默认的消费者代码时,我们简单的如下API即可:
/** 指定需要消费的 topic */
Set<String> topics = new HashSet<>();
topics.add(topicName);
consumer.subscribe(topics);
consumer.subscribe(Collection<String> topics),看底层代码会发现,其实是调用了consumer.subscribe(Collection<String> topics, new NoConsumerRebalanceListener())方法。NoConsumerRebalanceListener类实现了ConsumerRebalanceListener接口,但是没有做任何的逻辑处理。阅读ConsumerRebalanceListener接口的注释,会发现这是一个回调接口,可以触发自定义offset的分配方式。因此,需要创建一个实现ConsumerRebalanceListener接口的自定义类,来进行offset的分配管理。如下是个人的简单实现,供大家借鉴:
package com.private.kafkademo;
import com.private.utils.DBUtils;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.common.TopicPartition;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
/**
* Author Wan
* Date 2021-08-10
* Version 1.0
* Description : 自定义存储 kafka 消费的 offset 到 mysql
*/
public class CustomRebalanceListener implements ConsumerRebalanceListener {
private Consumer consumer;
private String groupId;
private String topicName;
private DBUtils dbUtils;
public CustomRebalanceListener(Consumer consumer, String groupId, String topicName, DBUtils dbUtils) {
this.consumer = consumer;
this.groupId = groupId;
this.topicName = topicName;
this.dbUtils = dbUtils;
}
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("Store offset to mysql ...");
Iterator<TopicPartition> iterator = partitions.iterator();
while (iterator.hasNext()) {
TopicPartition next = iterator.next();
int partitionId = next.partition();
long offset = consumer.position(next);
CustomOffset customOffset = new CustomOffset(groupId, topicName, partitionId, offset);
try {
dbUtils.insertDB(customOffset);
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("Custom assigned offset ...");
Iterator<TopicPartition> iterator = partitions.iterator();
while (iterator.hasNext()) {
TopicPartition next = iterator.next();
int partition = next.partition();
long offset = 0L;
try {
offset = dbUtils.queryOffset(groupId, topicName, partition);
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
consumer.seek(next, offset);
}
}
}
onPartitionsRevoked(Collection<TopicPartition> partitions) : 这个回调函数的作用是,将当前的 TopicPartition 信息对应的offset进行存储
onPartitionsAssigned(Collection<TopicPartition> partitions) : 这个回调函数的作用是,从存储容器中取出当前 TopicPartition 信息对应的 offset 进行重分配
完成了这步之后,可以使用 subscribe(Collection<String> topics, ConsumerRebalanceListener listener) 方法订阅所需要进行消费的 topic。
/** 指定需要消费的 topic */
Set<String> topics = new HashSet<>();
topics.add(topicName);
consumer.subscribe(topics, new CustomRebalanceListener(consumer, groupId, topicName, dbUtils));
正常消费后可以从mysql中读取到当前消费者消费到的 offset
3. 总结
自定存储offset,1. 可以很直观的看到数据消费的情况;2. 可以通过操作mysql,从而达到对kafka的消息进行从指定的offset开始进行重新消费;3. offset信息不再存储到 topic:__consumer_offsets下,减少了对kafka的读写操作。
综上都是个人的一些理解,如有不正确的地方,可留言私信,相互探讨,感谢~~~