在上一篇《Kafka Consumer多线程实例》中我们讨论了KafkaConsumer多线程的两种写法:多KafkaConsumer多线程以及单KafkaConsumer多线程。在第二种用法中我使用的是自动提交的方式,省去了多线程提交位移的麻烦。很多人跑来问如果是手动提交应该怎么写?由于KafkaConsumer不是线程安全的,因此我们不能简单地在多个线程中直接调用consumer.commitSync来提交位移。本文将给出一个实际的例子来模拟多线程消费以及手动提交位移。

  本例中包含3个类:

  • ConsumerThreadHandler类:consumer多线程的管理类,用于创建线程池以及为每个线程分配任务。另外consumer位移的提交也在这个类中进行
  • ConsumerWorker类:本质上是一个Runnable,执行真正的消费逻辑并上报位移信息给ConsumerThreadHandler
  • Main类:测试主方法类

测试代码

ConsumerWorker类

package huxi.test.consumer.multithreaded;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition; import java.util.List;
import java.util.Map; public class ConsumerWorker<K, V> implements Runnable { private final ConsumerRecords<K, V> records;
private final Map<TopicPartition, OffsetAndMetadata> offsets; public ConsumerWorker(ConsumerRecords<K, V> record, Map<TopicPartition, OffsetAndMetadata> offsets) {
this.records = record;
this.offsets = offsets;
} @Override
public void run() {
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<K, V>> partitionRecords = records.records(partition);
for (ConsumerRecord<K, V> record : partitionRecords) {
// 插入消息处理逻辑,本例只是打印消息
System.out.println(String.format("topic=%s, partition=%d, offset=%d",
record.topic(), record.partition(), record.offset()));
} // 上报位移信息
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
synchronized (offsets) {
if (!offsets.containsKey(partition)) {
offsets.put(partition, new OffsetAndMetadata(lastOffset + 1));
} else {
long curr = offsets.get(partition).offset();
if (curr <= lastOffset + 1) {
offsets.put(partition, new OffsetAndMetadata(lastOffset + 1));
}
}
}
}
}
}

ConsumerThreadHandler类

package huxi.test.consumer.multithreaded;

import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.WakeupException; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; public class ConsumerThreadHandler<K, V> { private final KafkaConsumer<K, V> consumer;
private ExecutorService executors;
private final Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>(); public ConsumerThreadHandler(String brokerList, String groupId, String topic) {
Properties props = new Properties();
props.put("bootstrap.servers", brokerList);
props.put("group.id", groupId);
props.put("enable.auto.commit", "false");
props.put("auto.offset.reset", "earliest");
props.put("key.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer");
consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
consumer.commitSync(offsets);
} @Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
offsets.clear();
}
});
} /**
* 消费主方法
* @param threadNumber 线程池中线程数
*/
public void consume(int threadNumber) {
executors = new ThreadPoolExecutor(
threadNumber,
threadNumber,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
try {
while (true) {
ConsumerRecords<K, V> records = consumer.poll(1000L);
if (!records.isEmpty()) {
executors.submit(new ConsumerWorker<>(records, offsets));
}
commitOffsets();
}
} catch (WakeupException e) {
// swallow this exception
} finally {
commitOffsets();
consumer.close();
}
} private void commitOffsets() {
// 尽量降低synchronized块对offsets锁定的时间
Map<TopicPartition, OffsetAndMetadata> unmodfiedMap;
synchronized (offsets) {
if (offsets.isEmpty()) {
return;
}
unmodfiedMap = Collections.unmodifiableMap(new HashMap<>(offsets));
offsets.clear();
}
consumer.commitSync(unmodfiedMap);
} public void close() {
consumer.wakeup();
executors.shutdown();
}
}

Main类

package huxi.test.consumer.multithreaded;

public class Main {

    public static void main(String[] args) {
String brokerList = "localhost:9092";
String topic = "test-topic";
String groupID = "test-group";
final ConsumerThreadHandler<byte[], byte[]> handler = new ConsumerThreadHandler<>(brokerList, groupID, topic);
final int cpuCount = Runtime.getRuntime().availableProcessors(); Runnable runnable = new Runnable() {
@Override
public void run() {
handler.consume(cpuCount);
}
};
new Thread(runnable).start(); try {
// 20秒后自动停止该测试程序
Thread.sleep(20000L);
} catch (InterruptedException e) {
// swallow this exception
}
System.out.println("Starting to close the consumer...");
handler.close();
}
}  

测试步骤

1. 首先创建一个测试topic: test-topic,10个分区,并使用kafka-producer-perf-test.sh脚本生产50万条消息

2. 运行Main,假定group.id设置为test-group

3. 新开一个终端,不断地运行以下脚本监控consumer group的消费进度

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group test-group

测试结果

LAG列全部为0表示consumer group的位移提交正常。值得一提的是,各位可以通过控制consumer.poll的超时时间来控制ConsumerThreadHandler类提交位移的频率。

感谢QQ群友的提醒,这种方式有丢失数据的时间窗口——假设T1线程在t0时间消费分区0的位移=100的消息M1,而T2线程在t1时间消费分区0的位移=101的消息M2。现在假设t3时T2线程先完成处理,于是上报位移101给Handler,但此时T1线程尚未处理完成。t4时handler提交位移101,之后T1线程发生错误,抛出异常导致位移100的消息消费失败,但由于位移已经提交到101,故消息丢失~。

【原创】Kafka Consumer多线程实例续篇的更多相关文章

  1. 【原创】Kafka Consumer多线程实例

    Kafka 0.9版本开始推出了Java版本的consumer,优化了coordinator的设计以及摆脱了对zookeeper的依赖.社区最近也在探讨正式用这套consumer API替换Scala ...

  2. kafka系列 -- 多线程消费者实现

    看了一下kafka,然后写了消费Kafka数据的代码.感觉自己功力还是不够. 不能随心所欲地操作数据,数据结构没学好,spark的RDD操作没学好. 不能很好地组织代码结构,设计模式没学好,面向对象思 ...

  3. 【原创】kafka consumer源代码分析

    顾名思义,就是kafka的consumer api包. 一.ConsumerConfig.scala Kafka consumer的配置类,除了一些默认值常量及验证参数的方法之外,就是consumer ...

  4. 【原创】美团二面:聊聊你对 Kafka Consumer 的架构设计

    在上一篇中我们详细聊了关于 Kafka Producer 内部的底层原理设计思想和细节, 本篇我们主要来聊聊 Kafka Consumer 即消费者的内部底层原理设计思想. 1.Consumer之总体 ...

  5. kafka consumer assign 和 subscribe模式差异分析

    转载请注明原创地址:http://www.cnblogs.com/dongxiao-yang/p/7200971.html 最近需要研究flink-connector-kafka的消费行为,发现fli ...

  6. Kafka设计解析(四)- Kafka Consumer设计解析

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/08/09/KafkaColumn4 摘要 本文主要介绍了Kafka High Level Con ...

  7. 读Kafka Consumer源码

    最近一直在关注阿里的一个开源项目:OpenMessaging OpenMessaging, which includes the establishment of industry guideline ...

  8. kafka consumer 配置详解

    1.Consumer Group 与 topic 订阅 每个Consumer 进程都会划归到一个逻辑的Consumer Group中,逻辑的订阅者是Consumer Group.所以一条message ...

  9. [Big Data - Kafka] Kafka设计解析(四):Kafka Consumer解析

    High Level Consumer 很多时候,客户程序只是希望从Kafka读取数据,不太关心消息offset的处理.同时也希望提供一些语义,例如同一条消息只被某一个Consumer消费(单播)或被 ...

随机推荐

  1. Linux-vim学习入门

    1.前言     vi/vim是linux中很重要的文本编辑器.我第一次使用这个编辑器时,很不习惯,甚至都不知道如何移动光标和插入字符.慢慢地经过学习,才知道如何使用vi/vim.     vi/vi ...

  2. 域名系统DNS和FTP

    域名系统概述 域名系统DNS(Domain Name System)是英特网使用的命名系统,用于把便于人们使用机器名字转化为IP地址. 为什么机器在处理IP数据报时要使用IP地址而不使用域名呢?IP地 ...

  3. 点击Input框弹出日期选项

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. Vuejs实例-01使用vue-cli脚手架搭建Vue.js项目

    [TOC] 1. 前言 vue-cli 一个简单的构建Vue.js项目的命令行界面 整体过程: $ npm install -g vue-cli $ vue init webpack vue-admi ...

  5. Spring+SpringMVC+MyBatis深入学习及搭建(三)——MyBatis全局配置文件解析

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/6874672.html 前面有写到Spring+SpringMVC+MyBatis深入学习及搭建(二)——My ...

  6. 【JAVAWEB学习笔记】06_jQuery基础

    接05的学习笔记. 四.使用JQ完成省市二级联动 1.需求分析 使用jquery完成省市二级联动 2.技术分析 2.1数组的遍历操作 方式一: $(function(){ // 全选/ 全不选 $(& ...

  7. Tomcat8-源码编译及开发

    前言 下载Tomcat8源码进行分析,最好的方式,可以编译及运行,从网上查询了很多方式,总是不能完整的运行,由于本人采用idea编辑器,所以喜欢maven的方式,所以综合了网上的多种方案,最终可以在i ...

  8. Word Ladder 2015年6月3日

    Given two words (beginWord and endWord), and a dictionary, find the length of shortest transformatio ...

  9. springcloud(六):配置中心(一)

    随着线上项目变的日益庞大,每个项目都散落着各种配置文件,如果采用分布式的开发模式,需要的配置文件随着服务增加而不断增多.某一个基础服务信息变更,都会引起一系列的更新和重启,运维苦不堪言也容易出错.配置 ...

  10. swfit - 实现类似今日头条顶部标签和底部内容的动态解决方案

    TYPageView TYPageView 类似今日头条 的标签导航解决方案,支持多种样式选择,基于swift3.0,支持文字颜色动态变化,底部选中线的动态变化 配图: 使用方法: let title ...