基于Direct API 手动维护kafka 的偏移量, 将偏移量同步导了 redis 中,
我将对比较重要的代码拿出来说明, 完整代码在下方:
首先是通过Direct AIP 获取 JavaInputDStream 对象 ,
JavaInputDStream message = KafkaUtils.createDirectStream(jssc, String.class, String.class,
StringDecoder.class, StringDecoder.class, String.class, kafkaParams, maptopic,
new Function, String>() {
public String call(MessageAndMetadata v1) throws Exception {
return v1.message();
}
});
//参数说明, jssc , kafka中记录的key 类型, kafka 中记录的value类型, key 的解码方式,value 的解码方式, kafka 中记录的类型, 为kafka 设置的参数, offsets 的集合 (这个会单独拿出来介绍的),最终返回的结果类型
获取DStream 对象,接下来是如何获取的 offset (偏移量) 后门我都会称呼为offset的,
OffsetRange[] offsetRanges = ((HasOffsetRanges) stringJavaRDD.rdd()).offsetRanges();
上面这个rdd 转化的操作, 必须是DStream 第一个转换操作, 因为 DStream 读取数据是根据分区读取的 rdd的分区恰好何 kafka的分区成映射关系,如果你进行了其他转换操作, 当前rdd分区就会发送改变, 你就不能将它转化为 HasOffsetRanges
说明一下offsetRange 这个对象, 这个对象是用于封装 offset 的对象,
topic 为 当前 当前主题, parttion 为主题所对应的分区, fromOffset 为当前偏移量的位置, untilOffset 为偏移量的最大值,
从这里我们可以看出 ,偏移量这个概念是由 主题+分区+fromOffset 确立的, 获取到这个 之后就可以就实现偏移量的维护。
之后就可以指定偏移量读取数据,
将获取的offset 分别保存的redis 中 , 当我们程序异常终止后, 我们接着当前偏移量 继续处理操作,
接着回到上面的创建 JavaInputDStream 对象, 这里面 当我们 设置的maptopic,不为空时,我们将 根据maptopic 这个map 对象 里面的 offset ,读取数据 , 如果没有 默认从开始位置读取,
Mapmaptopic = new HashMap<>();
解释一下,TopicAndPartition这个对象, 用于指定 JavaInputDStream从那读取kafka, 也是用来封装 offset的,和offsetRange 差不多
public class TestStreaming {
private static final Pattern SPACE = Pattern.compile(" ");
public static void main(String[] args) throws InterruptedException {
args = new String[] { "localhost:9092", "test1" };
if (args.length < 2) {
System.err.println("Usage: JavaDirectKafkaWordCount \n"
+ " is a list of one or more Kafka brokers\n"
+ " is a list of one or more kafka topics to consume from\n\n");
System.exit(1);
}
String brokers = args[0];
String topics = args[1];
// Create context with a 2 seconds batch interval
SparkConf sparkConf = new SparkConf().setMaster("local[2]").setAppName("JavaDirectKafkaWordCount");
JavaStreamingContext jssc = new JavaStreamingContext(sparkConf, Durations.seconds(10));
Set topicsSet = new HashSet<>(Arrays.asList(topics.split(",")));
Map kafkaParams = new HashMap<>();
kafkaParams.put("metadata.broker.list", brokers);
kafkaParams.put("group.id", "group1");
kafkaParams.put("auto.offset.reset", "smallest");
Map maptopic = new HashMap<>();
Jedis jedis = RedisUtil.getJedis();
Boolean flag = jedis.exists("offset");
//判断redis 中是否有保存的偏移量, 如果有 直接读取redis 中的,没有的话 从头开始读取
if (flag) {
Map offsets = jedis.hgetAll("offset");
for (Map.Entry entry : offsets.entrySet()) {
TopicAndPartition topic = new TopicAndPartition(topics, Integer.valueOf(entry.getKey()));
maptopic.put(topic, Long.valueOf(entry.getValue()));
}
}
// // Create direct kafka stream with brokers and topics
// JavaPairInputDStream directKafkaStream = KafkaUtils.createDirectStream(jssc, String.class, String.class,
// StringDecoder.class, StringDecoder.class, kafkaParams, topicsSet);
// create direct stream
JavaInputDStream message = KafkaUtils.createDirectStream(jssc, String.class, String.class,
StringDecoder.class, StringDecoder.class, String.class, kafkaParams, maptopic,
new Function, String>() {
public String call(MessageAndMetadata v1) throws Exception {
return v1.message();
}
});
message.foreachRDD(new VoidFunction>() {
@Override
public void call(JavaRDD stringJavaRDD) throws Exception {
// 获取jedis 连接对象 ,
Jedis jedis = RedisUtil.getJedis();
OffsetRange[] offsetRanges = ((HasOffsetRanges) stringJavaRDD.rdd()).offsetRanges();
//每次操作之前 ,保存此次操作前的偏移量, 如果当前任务失败, 我们可以回到开始的偏移量 重新计算,
for (OffsetRange o : offsetRanges) {
jedis.hincrBy("offset", o.partition() + "", o.fromOffset());
}
//计算过程
stringJavaRDD.flatMapToPair(new PairFlatMapFunction() {
@Override
public Iterator> call(String s) throws Exception {
List> list = new ArrayList<>();
String[] split = s.split(" ");
for (String string : split) {
list.add(new Tuple2<>(string, 1));
}
return list.iterator();
}
}).reduceByKey(new Function2() {
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
});
}
});
// Start the computation
jssc.start();
jssc.awaitTermination();
jssc.close();
}
附上我的redis 工具类, 你也可以使用zk, 或者mysql
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtil {
//服务器IP地址
private static String ADDR = "127.0.0.1";
//端口
private static int PORT = 6379;
//密码
private static String AUTH = "123456";
//连接实例的最大连接数
private static int MAX_ACTIVE = 1024;
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = 200;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
private static int MAX_WAIT = 10000;
//连接超时的时间
private static int TIMEOUT = 10000;
// 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
private static JedisPool jedisPool = null;
/**
* 初始化Redis连接池
*/
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取Jedis实例
*/
public synchronized static Jedis getJedis() {
try {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/***
*
* 释放资源
*/
public static void returnResource(final Jedis jedis) {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
maven 依赖
redis.clients
jedis
2.5.0
org.apache.spark
spark-streaming_2.11
2.1.1
org.apache.spark
spark-streaming-kafka_2.11
1.6.3
最开始我在写这代码的时候, 看了官网,官网没有怎末介绍, 也看了很多人的实现方式, 但是大多数实现方式,都是通过scala 去实现的, 后来找到一个Java的版本, 那个博主确实写的挺不错, 但是我感觉 写的有点复杂 ,他是调用scala 的api , 他将Java 类型进行了转化 成scala 再执行的,
附上博主 链接 :https://blog.csdn.net/u013673976/article/details/52603817
官网spark整合kafka api : http://spark.apache.org/docs/2.1.1/streaming-kafka-0-10-integration.html