Java 版spark Streaming 維護kafka 的偏移量

基於Direct  API  手動維護kafka 的偏移量,  將偏移量同步導了 redis 中,

我將對比較重要的代碼拿出來說明, 完整代碼在下方:

 首先是通過Direct AIP 獲取 JavaInputDStream 對象 , 

JavaInputDStream<String> message = KafkaUtils.createDirectStream(jssc, String.class, String.class,
				StringDecoder.class, StringDecoder.class, String.class, kafkaParams, maptopic,
				new Function<MessageAndMetadata<String, String>, String>() {
					public String call(MessageAndMetadata<String, String> 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  ,讀取數據 , 如果沒有 默認從開始位置讀取,  

Map<TopicAndPartition, Long> maptopic = 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 <brokers> <topics>\n"
					+ "  <brokers> is a list of one or more Kafka brokers\n"
					+ "  <topics> 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<String> topicsSet = new HashSet<>(Arrays.asList(topics.split(",")));
		Map<String, String> kafkaParams = new HashMap<>();
		kafkaParams.put("metadata.broker.list", brokers);
		kafkaParams.put("group.id", "group1");
		kafkaParams.put("auto.offset.reset", "smallest");

		Map<TopicAndPartition, Long> maptopic = new HashMap<>();
		Jedis jedis = RedisUtil.getJedis();
		Boolean flag = jedis.exists("offset");
		//判斷redis 中是否有保存的偏移量, 如果有 直接讀取redis 中的,沒有的話 從頭開始讀取
		if (flag) {
			Map<String, String> offsets = jedis.hgetAll("offset");
			for (Map.Entry<String, String> 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<String, String> directKafkaStream = KafkaUtils.createDirectStream(jssc, String.class, String.class,
		//				StringDecoder.class, StringDecoder.class, kafkaParams, topicsSet);

		// create direct stream

		JavaInputDStream<String> message = KafkaUtils.createDirectStream(jssc, String.class, String.class,
				StringDecoder.class, StringDecoder.class, String.class, kafkaParams, maptopic,
				new Function<MessageAndMetadata<String, String>, String>() {
					public String call(MessageAndMetadata<String, String> v1) throws Exception {
						return v1.message();
					}
				});

		message.foreachRDD(new VoidFunction<JavaRDD<String>>() {
			@Override
			public void call(JavaRDD<String> 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<String, String, Integer>() {
					@Override
					public Iterator<Tuple2<String, Integer>> call(String s) throws Exception {
						List<Tuple2<String, Integer>> list = new ArrayList<>();
						String[] split = s.split(" ");

						for (String string : split) {
							list.add(new Tuple2<>(string, 1));
						}
						return list.iterator();
					}
				}).reduceByKey(new Function2<Integer, Integer, Integer>() {
					@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 依賴

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka_2.11</artifactId>
            <version>1.6.3</version>
        </dependency>

最開始我在寫這代碼的時候, 看了官網,官網沒有怎末介紹, 也看了很多人的實現方式, 但是大多數實現方式,都是通過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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章