备注:当前技术总结在两年前就已经完成,仅针对2年前版本负责,目前完成该文档,为了记录后续工作可能存在需要,避免遗忘,故而行程文档记录。
一、环境版本:
CDH:5.15.2
Kafka:Kafka_2.1.1-0.10.0.0
Spark:2.3.0
SparkStreaming:2.3.0
下列三种使用方案,官方文档均有文档解释。
目前方案介绍分别为三种:
第一种方案:
使用 ssc.checkpoint("/sparkStreaming_consume_kafka/CP"); 方法,当前方法的弊端为偏移量数据存储在Hdfs文件夹目录上,第一最小的隐患会有很多小文件问题,
但是这并不是一次语义数据丢失最严重的的问题,小问题,可以靠内存解决,HDFS性能会下降,但是数据丢失最为致命。
问题1、当如果程序发生了升级会产生问题,之前的偏移量无法使用,并且必须删除掉checkpoint文件夹目录数据,否则升级程序无效(因为使用checkpoint会将运行代码 进行转换提交到HDFS目录,进行运行)。
问题2、如果解决前两个问题,当程序并未升级,删除掉checkpoint目录,当前kafka消费者宕机在两天前,删除了删除掉checkpoint目录,再次启动必定会读取前几天历史数据,会造成数据挤压,一次消费过多
直接内存溢出。
问题3、上述问题1,2解决方案,删除checkpoint目录,切换kafka 的Topics消费者组,可以解决当前这个问题,但是数据会只会从最新的地方消费,之前的数据会造成丢失。
备注【说明】:
//关闭消费之偏移量自动提交
kafkaParams.put("enable.auto.commit", false);
|=========================================================================================================|
| //设置Kafka的消费方式 ,一共3种 |
| earliest 当前各个分区下已经有提交的offset时,从提交的offset 开始消费,无提交的offset时,从头消费 |
| latest 当前各个分区下已经有提交的offset时,从提交的offset 开始消费,无提交的offset时,消费最新产生的该分区下的数据。|
| none(不推荐) 当前各个分区下都存在已提交的offset时,从offset 后开始消费,只要有一个分区不存在已经提交的offset,则抛出异常。 |
| 但是在实际生产环境中,使用的最多的就是使用latest,没有人使用none,earliest 都存在各自的弊端。 |
| =========================================================================================================|
kafkaParams.put("auto.offset.reset", "latest");
//从提交的Kafka 偏移量开始消费 如果偏移量文件夹 不存在 即在最新的数据进行消费 ,如果使用earliest 会从数据源头开始消费,会造成数据量巨大,并且内存溢出(需要开启数据反压机制)。
//如果配置为earliest Kafka 默认会读取所有数据 从头开始 ,然后内存溢出 ,需要测试,如果修改为自动提交,看看kafka 会不会从 ssc.checkpoint 中读取偏移量 然后增量添加
//经过测试后验证,开启代码手动提交,然后默认 使用earliest 进行偏移量控制, 可以进行增量读取,并且不会数据丢失。
异常备注说明:当前如果记录了偏移量数据,当时当前记录的偏移量为3天之前,但是Kafka的存储数据为三天时间,必须清空偏移量,从有记录的三天前的启示时间开始消费,否则查找三天之前的偏移量,数据不存在,程序会发生异常。
样例代码如下:
package com.chepai;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka010.ConsumerStrategies;
import org.apache.spark.streaming.kafka010.KafkaUtils;
import org.apache.spark.streaming.kafka010.LocationStrategies;
import java.util.*;
/**
* sparkStreaming消费kafka数据1分钟调用一次
* 处理车牌数据,进行车怕布控 废弃版本
* 作者 liuwunan
*/
public class ChePaiDeploy {
public static void main(String[] args) {
//初始化spark
SparkConf conf = new SparkConf().setAppName("ChePaiDeploy")
.set("spark.dynamicAllocation.enabled", "false")
.set("spark.serializer","org.apache.spark.serializer.KryoSerializer");
System.setProperty("es.set.netty.runtime.available.processors","false");
//代表启用线程数量,根据数据量的级别进行设置,但是如果调用 .coalesce(1,true)
//就是就当前线程数数据进行数据汇聚,
// 如果启用10个线程数,但是在最终调用时一个合并文件,就会造成资源浪费,一个线程离线对应的一个Task,
//当前为5线程
conf.setMaster("local[5]");
JavaSparkContext sc = new JavaSparkContext(conf);
sc.setLogLevel("ERROR");
JavaStreamingContext ssc = new JavaStreamingContext(sc, Durations.seconds(60));
//偏移量记录在Hdfs文件系统中。项目升级,需要删除当前文件夹。
ssc.checkpoint("/sparkStreaming_consume_kafka/LinZaiDeploy/CPD");
//配置kafka参数(节点、消费者组、topic)
String groupId = "group-05"; //指定消费者组id
String topics = "topics"; //指定topic
String brokers = "IP:9092,IP:9092,IP:9092"; //指定kafka的IP地址
Set<String> topicsSet = new HashSet<>(Arrays.asList(topics.split(",")));
Map<String, Object> kafkaParams = new HashMap<>();
kafkaParams.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
kafkaParams.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
kafkaParams.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
kafkaParams.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
kafkaParams.put("auto.offset.reset", "latest");
//关闭消费之偏移量自动提交
kafkaParams.put("enable.auto.commit", "false");
//链接kafka,获得DStream对象
JavaInputDStream<ConsumerRecord<String, String>> messages = KafkaUtils.createDirectStream
(ssc, LocationStrategies.PreferConsistent(), ConsumerStrategies.<String, String>Subscribe(topicsSet, kafkaParams));
JavaDStream<String> map = messages.map(new Function<ConsumerRecord<String, String>, String>() {
@Override
public String call(ConsumerRecord<String, String> v1) throws Exception {
JSONObject data = JSONObject.parseObject(v1.value());
if("null".equals(data)){
return null;
}
StringBuilder sb = new StringBuilder();
String dwmc = data.getString("DWMC");
String hphm = data.getString("HPHM");
if("-".equals(hphm)){
return null;
}
String jgsj = data.getString("JGSJ");
String kkbh = data.getString("KKBH");
String tplj = data.getString("TPLJ");
String xsfx = data.getString("XSFX");
return sb.append(dwmc).append("^").append(hphm).append("^").append(jgsj).append("^").append(kkbh).append("^").append(tplj).append("^").append(xsfx).toString();
}
});
JavaDStream<String> filter = map.filter(new Function<String, Boolean>() {
@Override
public Boolean call(String v1) throws Exception {
if (StringUtils.isBlank(v1)) {
return false;
}
return true;
}
});
filter.foreachRDD(new VoidFunction<JavaRDD<String>>() {
@Override
public void call(JavaRDD<String> s) throws Exception {
//此处合并并且Shuffer 可以实现单线程入库,如果数据量大,可以提高数量
s.repartition(1).foreachPartition(new VoidFunction<Iterator<String>>() {
@Override
public void call(Iterator<String> st) throws Exception {
while (st.hasNext()) {
String[] split = st.next().split("\\^", -1);
if ("鄂XXXXXX".equals(split[1])) {
String year = split[2].substring(0, 4);
String yue = split[2].substring(4, 6);
String ri = split[2].substring(6, 8);
String shi = split[2].substring(8, 10);
String fen = split[2].substring(10, 12);
String miao = split[2].substring(12, 14);
Insert("业务操作方法")
}
}
}
});
}
});
ssc.start();
System.out.println("调用函数");
try {
ssc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第二种方案:官方提供,由于博主没有使用,认为作用没有第三种好,故而没有使用。
第二种方案:
使用官方文档提供的第二种使用官方一步提交偏移量,优点处高于第一种,但是也有缺陷,将偏移量存储在特定的Kafka 偏移的主题上,默认情况下,新消费者将默认定期自动提交偏移量,
但是相当于第一种checkpoint 相比,kafka 默认是一个耐用存储,不管代码如何修改,同样是可以保存偏移量,但是存在一个问题就是kafka 并非有事务性,所以我们的输出结果必须是幂等性。
目前车辆牌照数据入库使用是第二种方法,经过测试可用性高,但是不是最好的办法,代码详情省略。
【弊端】无法存在事务。
第三种方案:官方推荐方案,本博主文章使用(保证一次语义消费)
第三种方案:
事务性手动管理kafka 偏移量:将kafka 偏移量存储到第三方数据库,支持ZK,Mysql,Redis,Hbase等数据库。
同样为官方提供的第三种解决方案,使用数据库事务存储的方案,即使在故障的情况下,也可以在同一个事务中保存偏移量作为结果,以保持两者同步,可以完美解决一次性语义问题。
使用当时 本人使用Redis 作为偏移量存储,因为Redis 为内存型数据库,插入效率极快,并且支持Key 相同数据直接覆盖。
使用原理 详情见代码 com.chepai.TEST 。
目前第三种方案是保持一次性语义最好的办法。
本博主使用,将Kafka数据的偏移量记录在Redis中。
代码Demo如下:【数据实施消费Kafka,记录偏移量,并且批量插入ElasticSearch集群,提供实时查询服务】
package com.chepai;
import com.alibaba.fastjson.JSONObject;
import com.utils.DateUtil;
import com.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka010.*;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.io.Serializable;
import java.net.InetAddress;
import java.util.*;
/**
* sparkStreaming消费kafka数据1分钟调用一次,车牌数据查询最终版本 最稳定版本
* 作者 liuwunan
*/
public class ShengPanChePai_last implements Serializable {
//设置异常标记,如果发生异常,则记录异常的偏移量,如果正常 则记录实时消费的偏移量
public static Boolean ExceptionFlag = true;
//配置kafka参数(节点、消费者组、topic)
private static String topics = "cljlxx"; //指定topic
private static String groupId = "cs2"; //指定消费者组id
private static String SPCPOffset = "SPCPOffset"; //定义当前消费Kafka存偏移量的Key,存在Redis里
private static String brokers = "IP:9092,IP:9092,IP:9092"; //指定kafka地
public static void main(String[] args) {
//初始化spark 并且优化spark 设置启动名称
SparkConf conf = new SparkConf().setAppName("ShengPanChePai_last");
//关闭动态调度 开启动态资源配置,根据工作负载来衡量是否应该增加或减少executor,默认false
conf.set("spark.dynamicAllocation.enabled", "false")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
//启用反压
.set("spark.streaming.backpressure.enabled", "true")
//最小消费条数
.set("spark.streaming.backpressure.pid.minRate","1")
//最大消费条数(当前最大,最小消费条数为每秒,同时乘以Kafka每个分区的消费速率)
//比如当前为60秒调用一次,并且消费Kafka 分区为10分区,最大消费速率10,那么一次最大消费数据为
//60*100*10=60000条数据。故每分钟最大消费6W条数据,如果
.set("spark.streaming.kafka.maxRatePerPartition","100")
.set("spark.speculation", "true");
System.setProperty("es.set.netty.runtime.available.processors", "false");
//开启数据反压机制 上述配置是限制每秒的条数 conf.set("spark.streaming.kafka.maxRatePerPartition","1000"); 结果为每秒1000条,乘以30秒时间,等于当前批次拉取的数据最大值 可以增量拉取
//源头限制数据为30乘以限制条数 最终入库使用两个分区插入 最终会出现 每次插入1500条数据
//如果想提高插入效率 那么就使用 多分区批量插入
//代表启用线程数量,根据数据量的级别进行设置,但是如果调用 .coalesce(1,true) 就是就当前线程数数据进行数据汇聚,
// 如果启用10个线程数,但是在最终调用时一个合并文件,就会造成资源浪费,一个线程离线对应的一个Task,当前为6个
conf.setMaster("local[6]");//本地模式,client 集群模式切记去掉当前数目。
JavaSparkContext sc = new JavaSparkContext(conf);
sc.setLogLevel("ERROR");
JavaStreamingContext ssc = new JavaStreamingContext(sc, Durations.seconds(60));
Set<String> topicsSet = new HashSet<>(Arrays.asList(topics.split(",")));
Map<String, Object> kafkaParams = new HashMap<>();
kafkaParams.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
kafkaParams.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
kafkaParams.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
kafkaParams.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
//关闭消费之偏移量自动提交 手动维护偏移量
kafkaParams.put("enable.auto.commit", "false");
//如果有偏移量则读取偏移量 如果没有消费最新,此处还可以配置其他两种方案。
kafkaParams.put("auto.offset.reset", "latest");
//创建偏移量对象 ,如果redis 没有,偏移量对象为空,如果有,则传入当前kafka 偏移量对象 持续消费
HashMap<TopicPartition, Long> mapTopic = new HashMap<>();
//获取redis 中当前kafka 当前操作有没有存在偏移量,如果有存在偏移量 即从偏移量开始读取,
//如果没有偏移量,即从最新数据读取,不从头消费,会造成数据堆积 内存溢出(如果从头消费,记得数据反压机制)
Boolean flag = RedisUtil.FlagExits(ShengPanChePaiOffset, 1);
JavaInputDStream<ConsumerRecord<String, String>> directStream = null;
String[] s =null;
String offsetlast =null;
if (flag) {
Map<String, String> offsets = RedisUtil.getAll(ShengPanChePaiOffset, 1);
for (Map.Entry<String, String> entry : offsets.entrySet()) {
String partition = entry.getKey();
String offset = entry.getValue();
//偏移量切割 将下标为0 数据拿到 为当前kafak 偏移量
s = offset.split("_", -1);
offsetlast = s[0];
TopicPartition topicPartition = new TopicPartition(topics, Integer.valueOf(partition));
mapTopic.put(topicPartition, Long.valueOf(offsetlast));
}
directStream = KafkaUtils.createDirectStream(ssc, LocationStrategies.PreferConsistent(), ConsumerStrategies.<String, String>Subscribe(topicsSet, kafkaParams, mapTopic));
} else {
directStream = KafkaUtils.createDirectStream(ssc, LocationStrategies.PreferConsistent(), ConsumerStrategies.<String, String>Subscribe(topicsSet, kafkaParams));
}
JavaDStream<String> map = directStream.map(new Function<ConsumerRecord<String, String>, String>() {
@Override
public String call(ConsumerRecord<String, String> v1) throws Exception {
JSONObject data = JSONObject.parseObject(v1.value());
if ("null".equals(data)) {
return null;
}
StringBuilder sb = new StringBuilder();
String dwmc = data.getString("DWMC");
String hphm = data.getString("HPHM");
String jgsj = data.getString("JGSJ");
String kkbh = data.getString("KKBH");
String tplj = data.getString("TPLJ");
String xsfx = data.getString("XSFX");
return sb.append(dwmc).append("^").append(hphm).append("^").append(jgsj).append("^").append(kkbh).append("^").append(tplj).append("^").append(xsfx).toString();
}
});
JavaDStream<String> filter = map.filter(new Function<String, Boolean>() {
@Override
public Boolean call(String v1) throws Exception {
if (StringUtils.isBlank(v1)) {
return false;
}
return true;
}
});
filter.foreachRDD(new VoidFunction<JavaRDD<String>>() {
@Override
public void call(JavaRDD<String> s) throws Exception {
//将数据分两个分区提交,如果一分钟 5000条数据 那么一次则提交2500条数据 不能太多,切记计算时间,数据一次提交ES,也需要需要很长时间
s.repartition(2).foreachPartition(new VoidFunction<Iterator<String>>() {
@Override
public void call(Iterator<String> st) throws Exception {
TransportClient client = null;
try {
client = new PreBuiltTransportClient(Settings.builder()
.put("cluster.name", "ga-chepai")
.put("client.transport.sniff", true)
.put("thread_pool.search.size", 5)
.build()).addTransportAddress(new TransportAddress(InetAddress.getByName("IP"), 9300));
BulkRequestBuilder builder = client.prepareBulk();
//加工厂(2)^-^20210708193014^9950fd61cb9746f8beb4dc5d6be58cb2^http://ip:28092/pic/download/0000qySS8UIHrf4cahyx2028G0HVzk^4
HashMap<String, String> esMap = null;
IndexRequestBuilder request = null;
String[] split = null;
String next = null;
while (st.hasNext()) {
esMap = new HashMap<>();
next = st.next();
split = next.split("\\^", -1);
//去掉尚未识别数据,后续集群可以全部存储
if ("-".equals(split[1])) {
continue;
}
esMap.put("dw_name", split[0]);
esMap.put("hp_name", split[1]);
esMap.put("cp_time", split[2]);
esMap.put("kk_number", split[3]);
esMap.put("img_url", split[4]);
esMap.put("xs_fangxiang", split[5]);
request = client.prepareIndex("chepai", "doc", UUID.randomUUID().toString()).setSource(esMap);
builder.add(request);
}
//System.out.println("当前条数为+ "+ builder.numberOfActions());
//判断当前批量请求数据为0条以上 才会进行请求发送。
if (builder.numberOfActions() > 0) {
BulkResponse response = builder.get();
if (response.hasFailures()) {
System.out.println("操作失败,再从请求一次");
builder.execute().actionGet();
}
}
} catch (Exception e) {
//如果发生异常将 标记切换 解决 数据重复问题
ExceptionFlag = false;
e.printStackTrace();
}
client.close();
}
});
}
});
//此处必须调用 KafkaUtils.createDirectStream 生成对象,因为第二步后续都无法拿到Kafka 数据偏移量。
directStream.foreachRDD(new VoidFunction<JavaRDD<ConsumerRecord<String, String>>>() {
@Override
public void call(JavaRDD<ConsumerRecord<String, String>> v3) throws Exception {
v3.repartition(2).foreachPartition(new VoidFunction<Iterator<ConsumerRecord<String, String>>>() {
@Override
public void call(Iterator<ConsumerRecord<String, String>> st) throws Exception {
HashMap<String, String> redisMapOk = new HashMap<>();
HashMap<String, String> redisMapErro = new HashMap<>();
String time = DateUtil.formatDateString(new Date(), DateUtil.DATE_FORMAT_12W);
OffsetRange[] offsetRanges = ((HasOffsetRanges) v3.rdd()).offsetRanges();
for (OffsetRange offsetRange : offsetRanges) {
//记录正确的偏移量 如果没有发生错误,则记录当前偏移量的结束位置,因为起始位置已经数据入库, 下次从上次的结束开始
redisMapOk.put(String.valueOf(offsetRange.partition()), offsetRange.untilOffset()+"_"+time+"_OK");
//记录错误的的偏移量 因为异常插入, 所以记录当前偏移量的起始位置。
redisMapErro.put(String.valueOf(offsetRange.partition()),offsetRange.fromOffset()+"_"+time+"_ERROR");
}
//当数据为空 不对数据添加到redis 然后减少redis 的压力
if(st.hasNext()){
if (ExceptionFlag) {
//System.out.println("正确偏移量~~~~~~~~~~~ 提交" + redisMapOk);
RedisUtil.PutAll(ShengPanChePaiOffset, redisMapOk, 1);
} else {
//System.out.println("错误偏移量~~~~~~~~~~~" + redisMapOk);
RedisUtil.PutAll(ShengPanChePaiOffset, redisMapErro, 1);
}
}
}
});
}
});
ssc.start();
try {
ssc.awaitTermination();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Redis读取Kafka数据偏移量代码
package com.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashMap;
import java.util.Map;
/**
* @Date 2019/7/12 19:12
*/
public class RedisUtil {
private static String host = "IP";
private static int port = 6379;
private static int timeout = 10000;
private static int maxIdle = 8;
private static long maxWaitMillis = 100;
private static String password = "password";
private static boolean blockWhenExhausted = true;
private static JedisPool jedisPool = null;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
// 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
// 是否启用pool的jmx管理功能, 默认true
jedisPoolConfig.setJmxEnabled(true);
jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
}
public static String get(String key, int indexdb) {
Jedis jedis = null;
String value = null;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
value = jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
return value;
}
public static Map getAll(String key, int indexdb) {
Jedis jedis = null;
Map<String, String> StringMap=null;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
StringMap= jedis.hgetAll(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
return StringMap;
}
public static void PutAll(String key, HashMap map, int indexdb) {
Jedis jedis = null;
Map<String, String> StringMap=null;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
jedis.hmset(key, map);
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
}
public static String del(String key, int indexdb) {
Jedis jedis = null;
String value = null;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
if(jedis.get(key) != null){
jedis.del(key);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
return value;
}
public static Boolean FlagExits(String key, int indexdb) {
Jedis jedis = null;
Boolean exists =true;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
exists = jedis.exists(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
return exists;
}
public static String set(String key, String value, int indexdb) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.select(indexdb);
jedis.set(key,value);
// value = jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
returnResource(jedisPool, jedis);
}
return value;
}
/**
* 返还到连接池
* @param jedisPool
* @param jedis
*/
public static void returnResource(JedisPool jedisPool, Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
public synchronized static Jedis getJedis() {
Jedis jedis = null;
if(jedisPool!=null){
jedis = jedisPool.getResource();
return jedis;
}else{
return null;
}
}
}
从上述第三种方案中代码可知,程序初始化的时候,会去默认读取偏移量,如果偏移量的标记为空,则开始从最新的地方消费(latest),如果从Redis中读取到了偏移量数据,按照对应分区消费读取,同时,如果配置的为earliest,没有记录偏移量,则为从开头读取,但是必须限制数据消费速率,开启数据反压机制,否则初始的消费速率巨大,Jvm内存会异常,不好的情况会将ElasticSearch集群写宕机,所以此处涉及到数据反压,代码配置文件已经写的非常清楚,可以直接配置使用。下列为反压解释的相关文档Scala版本。
至此:SprakStreaming 消费Kafka保持一次语义数据偏移量存储Redis,同时开启反压消费机制记录完毕。
POM文件坐标如下
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
<spark.version>2.3.0</spark.version>
<scala.version>2.11.8</scala.version>
<!--<scala.version>2.10.0</scala.version>-->
<elasticsearch.version>6.6.1</elasticsearch.version>
</properties>
<dependencies>
<!-- spark start -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
<!--<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>-->
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.10.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.hbase</groupId>
<artifactId>alihbase-client</artifactId>
<version>1.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>6.5.4</version>
</dependency>