书接上文,上次只是通过kafka提供的命令行来演示了一下生产者消费者的使用,这篇通过Java API的方式来跑一下。
生产者的代码如下:
public class Producer {
private static String topic = "test007";
public static void main(String[] args) throws Exception {
Producer p = new Producer();
p.producer();
}
public void producer() throws ExecutionException, InterruptedException {
Properties pro = new Properties();
pro.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node01:9092,node02:9092,node03:9092");
//kafka 是一个持久化数据的MQ,以byte[]方式存储,数据需要转换为byte[],kafka不会对数据做处理,生产和消费方需要约定编码
//kafka中的零拷贝 使用sendfile系统调用,实现数据快速消费
pro.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
pro.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(pro);
/*
topic : test007
partition数量:2
模拟三种商品,每种商品有线性的3个ID
相同的商品最好去到一个分区里
*/
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "item-" + j, "value-" + i);
Future<RecordMetadata> send = producer.send(record);
RecordMetadata rm = send.get();
int partition = rm.partition();
long offset = rm.offset();
System.out.println("key:" + record.key() + ", value:" +
record.value() + ", partition:" + partition + ", offset:" + offset);
}
}
}
}
代码中模拟了三种商品,每种商品有线性的3个ID,相同的商品会去到一个分区里(通过K、V消息的形式)。 执行打印的结果如下:
key:item-1, value:value-0, partition:1, offset:8
key:item-2, value:value-0, partition:1, offset:9
key:item-0, value:value-1, partition:0, offset:8
key:item-1, value:value-1, partition:1, offset:10
key:item-2, value:value-1, partition:1, offset:11
key:item-0, value:value-2, partition:0, offset:9
key:item-1, value:value-2, partition:1, offset:12
key:item-2, value:value-2, partition:1, offset:13
所有item-0的消息都会到partition 0分区,item-1、item-2都会到partition 1分区。
消费者代码:
public class Consumer {
private static String topic = "test007";
public static void main(String[] args) {
Consumer c = new Consumer();
c.cousumer();
}
public void cousumer() {
Properties pro = new Properties();
pro.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node01:9092,node02:9092,node03:9092");
//kafka 是一个持久化数据的MQ,以byte[]方式存储,数据需要转换为byte[],kafka不会对数据做处理,生产和消费方需要约定编码
//kafka中的零拷贝 使用sendfile系统调用,实现数据快速消费
pro.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
pro.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//设置消费分组
pro.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "TIGER_007");
/**
* What to do when there is no initial offset in Kafka or if the current offset does not exist any more on the server (e.g. because that data has been deleted):
* <ul>
* <li>earliest: automatically reset the offset to the earliest offset</li>
* <li>latest: automatically reset the offset to the latest offset</li>
* <li>none: throw exception to the consumer if no previous offset is found for the consumer's group</li>
* anything else: throw exception to the consumer.
* </li></ul>";
*
*/
pro.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
//offset 自动提交
pro.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
//pro.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "");//5秒
//pro.setProperty(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "");//poll 拉取多少条数据
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(pro);
consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
/**
* 消费组和对应的 partition 的rebalance
* 例如 刚开始只有一个消费者对应两个 partition
* 这时同一消费组中新增一个消费者,这时消费者和 partition的对应关系就会变成一个消费者对应一个partition
*/
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("---onPartitionsRevoked:");
Iterator<TopicPartition> iter = partitions.iterator();
while(iter.hasNext()){
System.out.println(iter.next().partition());
}
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("---onPartitionsAssigned:");
Iterator<TopicPartition> iter = partitions.iterator();
while (iter.hasNext()) {
System.out.println(iter.next().partition());
}
}
});
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(0));
if (!records.isEmpty()) {
Iterator<ConsumerRecord<String, String>> iter = records.iterator();
System.out.println("================="+records.count()+"==================");
//==================================================================================================================//
//自动提交 ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"
/*while (iter.hasNext()) {
ConsumerRecord<String, String> record = iter.next();
int partition = record.partition();
long offset = record.offset();
System.out.println("key: " + record.key() + " value:" + record.value() + " partition:" + partition + " offset:" + offset);
}*/
//==================================================================================================================//
Set<TopicPartition> partitions = records.partitions(); //每次poll的时候是取多个分区的数据
//且每个分区内的数据是有序的
/**
* 如果手动提交offset 即 ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"
* 可以按一下三种方式处理
* 1、按消息进度同步提交
* 2、按分区粒度同步提交
* 3、按当前poll的批次同步提交
*
* 思考:如果在多个线程下
* 1、以上1,3的方式不用多线程
* 2、以上2的方式最容易想到多线程方式处理,即每个分区用一个线程处理
*/
for (TopicPartition partition : partitions) {
List<ConsumerRecord<String, String>> pRecords = records.records(partition);
//在一个批次里,按分区获取poll回来的数据
//线性按分区处理,还可以并行按分区处理用多线程的方式
Iterator<ConsumerRecord<String, String>> piter = pRecords.iterator();
while(piter.hasNext()){
ConsumerRecord<String, String> next = piter.next();
int par = next.partition();
long offset = next.offset();
String key = next.key();
String value = next.value();
long timestamp = next.timestamp();
System.out.println("key: "+ key+" val: "+ value+ " partition: "+par + " offset: "+ offset+"time:: "+ timestamp);
TopicPartition sp = new TopicPartition("msb-items", par);
OffsetAndMetadata om = new OffsetAndMetadata(offset);
HashMap<TopicPartition, OffsetAndMetadata> map = new HashMap<>();
map.put(sp,om);
//这个是最安全的,每条记录级的更新,对应第一点,按消息进度同步提交
consumer.commitSync(map);
//单线程,多线程,都可以
}
long poff = pRecords.get(pRecords.size() - 1).offset();//获取分区内最后一条消息的offset
OffsetAndMetadata pom = new OffsetAndMetadata(poff);
HashMap<TopicPartition, OffsetAndMetadata> map = new HashMap<>();
map.put(partition,pom);
//这个是第二种,分区粒度提交offset
consumer.commitSync( map );
/**
* 因为你都分区了
* 拿到了分区的数据集
* 期望的是先对数据整体加工
*/
}
//这个就是按poll的批次提交offset,第3点
consumer.commitSync();
}
}
}
}
消费者代码中,如果设置自动提交,即ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG为"true",那这个就很简单,不用我们自己处理offset的更新。系统会为我们自动提交。
offset下标自动提交在有些场景可能不适用,因为自动提交是在kafka拉取到数据之后就直接提交,这样很容易丢失数据,尤其是在需要事物控制的时候。
很多情况下我们需要从kafka成功拉取数据之后,对数据进行相应的处理之后再进行提交。如拉取数据之后进行写入mysql这种,所以这时我们就需要进行手动提交kafka的offset下标。
如果手动提交offset 即 ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"
可以按一下三种方式处理
- 按消息进度同步提交
- 按分区粒度同步提交
- 按当前poll的批次同步提交
思考:如果在多个线程下如何处理?
- 以上1,3的方式不用多线程
- 以上2的方式最容易想到多线程方式处理,即每个分区用一个线程处理
每次更新能拿到对应partition的offset即可。