Flink 广播变量在实时处理程序中扮演着很重要的角色,适当的使用广播变量会大大提升程序处理效率。

本文从简单的 demo 场景出发,引入生产中实际的需求并提出思路与部分示例代码,应对一般需求应该没有什么问题,话不多说,赶紧来看看这篇干货满满的广播程序使用实战吧。

1 啥是广播 

Flink 支持广播变量,允许在每台机器上保留一个只读的缓存变量,数据存在内存中,在不同的 task 所在的节点上的都能获取到,可以减少大量的 shuffle 操作。

换句话说,广播变量可以理解为一个公共的共享变量,可以把一个 dataset 的数据集广播出去,然后不同的 task 在节点上都能够获取到,这个数据在每个节点上只会存在一份。

如果不使用 broadcast,则在每个节点中的每个 task 中都需要拷贝一份 dataset 数据集,比较浪费内存 (也就是一个节点中可能会存在多份 dataset 数据)

2 用法总结

//1 初始化数据

DataSet<Integer>  toBroadcast = env.fromElements(1,2,3)

//2 广播数据 api

withBroadcastSet(toBroadcast,"broadcastSetName")

//3 获取数据

Collection<integer> broadcastSet = getRuntimeContext().getBroadcastVariable("broadcastSetName"); 

注意:

 

  • 广播变量由于要常驻内存,程序结束时才会失效,所以数据量不宜过大

  • 广播变量广播在初始化后不支持修改 (修改场景也有办法)

3 基础案例演示

  • 基础案例广播变量使用

这种场景下广播变量就是加载参数表,参数表不会变化,记住第二部分常用总结公式即可。

/**
* @author 大数据江湖
* @version 1.0
* @date 2021/5/17.
*
*/
public class BaseBroadCast { /**
* broadcast广播变量
* 需求:
* flink会从数据源中获取到用户的姓名
* 最终需要把用户的姓名和年龄信息打印出来
* 分析:
* 所以就需要在中间的map处理的时候获取用户的年龄信息
* 建议吧用户的关系数据集使用广播变量进行处理
*
*/
public static void main(String[] args) throws Exception { //获取运行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//1:准备需要广播的数据
ArrayList<Tuple2<String, Integer>> broadData = new ArrayList<>();
broadData.add(new Tuple2<>("zs", 18));
broadData.add(new Tuple2<>("ls", 20));
broadData.add(new Tuple2<>("ww", 17));
DataSet<Tuple2<String, Integer>> tupleData =
env.fromCollection(broadData); //1.1:处理需要广播的数据,把数据集转换成map类型,map中的key就是用户姓名,value就是用户年龄 DataSet<HashMap<String, Integer>> toBroadcast = tupleData.map(new MapFunction<Tuple2<String, Integer>, HashMap<String, Integer>>() {
@Override
public HashMap<String, Integer> map(Tuple2<String, Integer> value)
throws Exception {
HashMap<String, Integer> res = new HashMap<>();
res.put(value.f0, value.f1);
return res;
}
});
//源数据
DataSource<String> data = env.fromElements("zs", "ls", "ww");
//注意:在这里需要使用到RichMapFunction获取广播变量
DataSet<String> result = data.map(new RichMapFunction<String, String>() { List<HashMap<String, Integer>> broadCastMap = new ArrayList<HashMap<String, Integer>>(); HashMap<String, Integer> allMap = new HashMap<String, Integer>(); /**
* 这个方法只会执行一次
* 可以在这里实现一些初始化的功能
* 所以,就可以在open方法中获取广播变量数据
*/
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//3:获取广播数据
this.broadCastMap = getRuntimeContext().getBroadcastVariable("broadCastMapName");
for (HashMap map : broadCastMap) {
allMap.putAll(map);
}
} @Override
public String map(String value) throws Exception {
Integer age = allMap.get(value);
return value + "," + age;
}
}).withBroadcastSet(toBroadcast, "broadCastMapName");//2:执行广播数据的操作
result.print();
} }

生产案例演示

实际生产中有时候是需要更新广播变量的,但不是实时更新的,一般会设置一个更新周期,几分钟,几小时的都很常见,根据业务而定。

由于广播变量需要更新,解决办法一般是需要将广播变量做成另一个 source,进行流与流之间的 connect 操作,定时刷新广播的source,从而达到广播变量修改的目的。

4.1.1 使用 redis 中的数据作为广播变量的思路:

 

消费 kafka 中的数据,使用 redis 中的数据作为广播数据,进行数据清洗后 写到 kafka中。

示例代码分为三个部分:kafka 生产者,redis 广播数据源,执行入口类

  • 构建 kafka 生成者,模拟数据 (以下代码的消费消息来源均是此处生产)

/**
* 模拟数据源
*/
public class kafkaProducer { public static void main(String[] args) throws Exception{
Properties prop = new Properties();
//指定kafka broker地址
prop.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
//指定key value的序列化方式
prop.put("key.serializer", StringSerializer.class.getName());
prop.put("value.serializer", StringSerializer.class.getName());
//指定topic名称
String topic = "data_flink_bigdata_test"; //创建producer链接
KafkaProducer<String, String> producer = new KafkaProducer<String,String>(prop); //{"dt":"2018-01-01 10:11:11","countryCode":"US","data":[{"type":"s1","score":0.3,"level":"A"},{"type":"s2","score":0.2,"level":"B"}]} while(true){
String message = "{\"dt\":\""+getCurrentTime()+"\",\"countryCode\":\""+getCountryCode()+"\",\"data\":[{\"type\":\""+getRandomType()+"\",\"score\":"+getRandomScore()+",\"level\":\""+getRandomLevel()+"\"},{\"type\":\""+getRandomType()+"\",\"score\":"+getRandomScore()+",\"level\":\""+getRandomLevel()+"\"}]}";
System.out.println(message);
//同步的方式,往Kafka里面生产数据 producer.send(new ProducerRecord<String, String>(topic,message)); Thread.sleep(2000);
}
//关闭链接
//producer.close();
} public static String getCurrentTime(){
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
return sdf.format(new Date());
} public static String getCountryCode(){
String[] types = {"US","TW","HK","PK","KW","SA","IN"};
Random random = new Random();
int i = random.nextInt(types.length);
return types[i];
} public static String getRandomType(){
String[] types = {"s1","s2","s3","s4","s5"};
Random random = new Random();
int i = random.nextInt(types.length);
return types[i];
} public static double getRandomScore(){
double[] types = {0.3,0.2,0.1,0.5,0.8};
Random random = new Random();
int i = random.nextInt(types.length);
return types[i];
} public static String getRandomLevel(){
String[] types = {"A","A+","B","C","D"};
Random random = new Random();
int i = random.nextInt(types.length);
return types[i];
} }
 
  • redis 数据作为广播数据

/**
* redis中准备的数据源
* source:
*
* hset areas AREA_US US
* hset areas AREA_CT TW,HK
* hset areas AREA_AR PK,KW,SA
* hset areas AREA_IN IN
*
* result:
*
* HashMap
*
* US,AREA_US
* TW,AREA_CT
* HK,AREA_CT
*
*/
public class BigDataRedisSource implements SourceFunction<HashMap<String,String>> { private Logger logger= LoggerFactory.getLogger(BigDataRedisSource.class); private Jedis jedis;
private boolean isRunning=true; @Override
public void run(SourceContext<HashMap<String, String>> cxt) throws Exception {
this.jedis = new Jedis("localhost",6379);
HashMap<String, String> map = new HashMap<>();
while(isRunning){
try{
map.clear();
Map<String, String> areas = jedis.hgetAll("areas");
/**
* AREA_CT TT,AA
*
* map:
* TT,AREA_CT
* AA,AREA_CT
*/
for(Map.Entry<String,String> entry: areas.entrySet()){
String area = entry.getKey();
String value = entry.getValue();
String[] fields = value.split(",");
for(String country:fields){
map.put(country,area);
} }
if(map.size() > 0 ){
cxt.collect(map);
}
Thread.sleep(60000);
}catch (JedisConnectionException e){
logger.error("redis连接异常",e.getCause());
this.jedis = new Jedis("localhost",6379);
}catch (Exception e){
logger.error("数据源异常",e.getCause());
} } } @Override
public void cancel() {
isRunning=false;
if(jedis != null){
jedis.close();
} }
}
 
  • 程序入口类

/**
* @author 大数据江湖
* @version 1.0
* @date 2021/4/25.
*
*
* 使用 kafka 输出流和 redis 输出流 进行合并清洗
*
*
*/
public class 广播方式1分两个流进行connnect操作 {
public static void main(String[] args) throws Exception { //1 获取执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致 //2 设置 checkpoint //开启checkpoint 一分钟一次
env.enableCheckpointing(60000);
//设置checkpoint 仅一次语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//两次checkpoint的时间间隔
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
//最多只支持1个checkpoint同时执行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
//checkpoint超时的时间
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 任务失败后也保留 checkPonit数据
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
3, // 尝试重启的次数
Time.of(10, TimeUnit.SECONDS) // 间隔
)); // 设置 checkpoint 路径
// env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint")); //3 设置 kafka Flink 消费 //创建 Kafka 消费信息 String topic="data_flink_bigdata_test";
Properties consumerProperties = new Properties();
consumerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
consumerProperties.put("group.id","data_test_new_1");
consumerProperties.put("enable.auto.commit", "false");
consumerProperties.put("auto.offset.reset","earliest"); //4 获取 kafka 与 redis 数据源 FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties); DataStreamSource<String> kafkaSourceData = env.addSource(consumer); //直接使用广播的方式 后续作为两个数据流来操作 DataStream<HashMap<String, String>> redisSourceData = env.addSource(new NxRedisSource()).broadcast(); //5 两个数据源进行 ETL 处理 使用 connect 连接处理 SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(redisSourceData).flatMap(new MyETLProcessFunction()); //6 新创建一个 kafka 生产者 进行发送
String outputTopic="allDataClean"; // 输出给下游 kafka Properties producerProperties = new Properties();
producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092"); FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
producerProperties); etlData.addSink(producer); //7 提交任务执行 env.execute("DataClean"); } /**
* in 1 kafka source :
*
* {"dt":"2018-01-01 10:11:11","countryCode":"US","data":[{"type":"s1","score":0.3,"level":"A"},{"type":"s2","score":0.2,"level":"B"}]}
*
*
* in 2 redis source
*
*
* US,AREA_US
* TW,AREA_CT
* HK,AREA_CT
*
*
*
* out 合并后的source
*/
private static class MyETLProcessFunction implements CoFlatMapFunction<String,HashMap<String,String>,String> { //用来存储 redis 中的数据
HashMap<String,String> allMap = new HashMap<String,String>(); @Override
public void flatMap1(String line, Collector<String> collector) throws Exception { //将 kafka 数据 按 redis 数据进行替换
// s -> kafka 数据
//allMap -> redis 数据 JSONObject jsonObject = JSONObject.parseObject(line);
String dt = jsonObject.getString("dt");
String countryCode = jsonObject.getString("countryCode");
//可以根据countryCode获取大区的名字
String area = allMap.get(countryCode);
JSONArray data = jsonObject.getJSONArray("data");
for (int i = 0; i < data.size(); i++) {
JSONObject dataObject = data.getJSONObject(i);
System.out.println("大区:"+area);
dataObject.put("dt", dt);
dataObject.put("area", area);
//下游获取到数据的时候,也就是一个json格式的数据
collector.collect(dataObject.toJSONString());
} } @Override
public void flatMap2(HashMap<String, String> stringStringHashMap, Collector<String> collector) throws Exception {
//将 redis 中 数据进行赋值
allMap = stringStringHashMap; }
}
}

4.1.2 使用 MapState 进行广播程序优化:

优化的点在于 (下面代码中 TODO 标识点):

  1. 进行数据广播时需要使用 MapStateDescriptor 进行注册

  2. 进行两个流合并处理时 使用 process 函数

  3. 处理函数中使用 MapState  来存取 redis 中的数据

/**
* @author 大数据江湖
* @version 1.0
* @date 2021/4/25.
* <p>
* 使用 kafka 输出流和 redis 输出流 进行合并清洗
* <p>
* 线上使用的方式
*/
public class 广播方式2使用MapState对方式1改造 {
public static void main(String[] args) throws Exception { //1 获取执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致 //2 设置 checkpoint //开启checkpoint 一分钟一次
env.enableCheckpointing(60000);
//设置checkpoint 仅一次语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//两次checkpoint的时间间隔
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
//最多只支持1个checkpoint同时执行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
//checkpoint超时的时间
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 任务失败后也保留 checkPonit数据
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
3, // 尝试重启的次数
Time.of(10, TimeUnit.SECONDS) // 间隔
)); // 设置 checkpoint 路径
//env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint")); //3 设置 kafka Flink 消费 //创建 Kafka 消费信息 String topic = "data_flink_bigdata_test";
Properties consumerProperties = new Properties();
consumerProperties.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
consumerProperties.put("group.id", "data_flink_fpy_test_consumer");
consumerProperties.put("enable.auto.commit", "false");
consumerProperties.put("auto.offset.reset", "earliest"); //4 获取 kafka 与 redis 数据源 FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties); DataStreamSource<String> kafkaSourceData = env.addSource(consumer); // 获取 redis 数据源并且进行广播 线上的广播也是 source + 广播方法 MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<String, String>(
"RedisBdStream",
String.class,
String.class
); //5 两个数据源进行 ETL 处理 使用 connect 连接处理 TODO process 替换 FlatMap
//TODO 使用 MapState 来进行广播
BroadcastStream<HashMap<String, String>> redisSourceData = env.addSource(new NxRedisSource()).broadcast(descriptor); SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(redisSourceData).process(new MyETLProcessFunction()); //6 新创建一个 kafka 生产者 进行发送
String outputTopic = "allDataClean"; // 输出给下游 kafka Properties producerProperties = new Properties();
producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092"); FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
producerProperties); etlData.addSink(producer); etlData.print(); //7 提交任务执行 env.execute("DataClean"); } /**
* in 1 kafka source
* in 2 redis source
* <p>
* out 合并后的source
*/
private static class MyETLProcessFunction extends BroadcastProcessFunction<String, HashMap<String, String>, String> { // TODO 注意此处 descriptor 的名称需要与 广播时 (99行代码) 名称一致
MapStateDescriptor<String, String> descriptor =
new MapStateDescriptor<String, String>(
"RedisBdStream",
String.class,
String.class
); //逻辑的处理方法 kafka 的数据
@Override
public void processElement(String line, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
//将 kafka 数据 按 redis 数据进行替换
// s -> kafka 数据
//allMap -> redis 数据
System.out.println("into processElement ");
JSONObject jsonObject = JSONObject.parseObject(line);
String dt = jsonObject.getString("dt");
String countryCode = jsonObject.getString("countryCode");
//可以根据countryCode获取大区的名字 // String area = allDataMap.get(countryCode);
//TODO 从MapState中获取对应的Code
String area = readOnlyContext.getBroadcastState(descriptor).get(countryCode); JSONArray data = jsonObject.getJSONArray("data"); for (int i = 0; i < data.size(); i++) {
JSONObject dataObject = data.getJSONObject(i);
System.out.println("大区:" + area);
dataObject.put("dt", dt);
dataObject.put("area", area);
//下游获取到数据的时候,也就是一个json格式的数据
collector.collect(dataObject.toJSONString());
} } //广播流的处理方法
@Override
public void processBroadcastElement(HashMap<String, String> stringStringHashMap, Context context, Collector<String> collector) throws Exception { // 将接收到的控制数据放到 broadcast state 中
//key , flink
// 将 RedisMap中的值放入 MapState 中
for (Map.Entry<String, String> entry : stringStringHashMap.entrySet()) {
//TODO 使用 MapState 存储 redis 数据
context.getBroadcastState(descriptor).put(entry.getKey(), entry.getValue());
System.out.println(entry);
} }
}
}
 

4.2 关系型数据库广播变量案例思路:

 

需求:

在 flink 流式处理中常常需要加载数据库中的数据作为条件进行数据处理,有些表作为系统表,实时查询效率很低,这时候就需要将这些数据作为广播数据,而同时这些数据可能也需要定期的更新。

思路:

数据库表的广播变量思路同redis等缓存广播数据的思路类似,也是使用 两个source 进行 connect 处理 , 在数据库表的 source 中定时刷新数据就可以了。

不同点在于这里把数据库查询的操作转成另一个工具类,在初始化时使用了静态代码块,在广播时使用了流的 connect 操作。

示例代码分为三个部分:数据库表广播源,数据库操作类,执行入口类

  • 数据库表广播源

/**
* @author 大数据江湖
* @Date:2021-5-17
* DB source 源头 进行广播
*/
public class BigDataDBBroadSource extends RichSourceFunction<Map<String,Object>> { private final Logger logger = LoggerFactory.getLogger(BigDataDBBroadSource.class);
private volatile boolean isRunning = true; public BigDataDBBroadSource() { } @Override
public void open(Configuration parameters) throws Exception {
super.open(parameters); } @Override
public void run(SourceContext<Map<String,Object>> sourceContext) throws Exception { while (isRunning) {
//TODO 使用的是一个 DB 源头的 source 60 s 刷新一次 进行往下游发送
TimeUnit.SECONDS.sleep(60); Map<String,Object> map = new HashMap<String,Object>(); //规则匹配关键词 final DbBroadCastListInitUtil.Build ruleListInitUtil = new DbBroadCastListInitUtil.Build(); ruleListInitUtil.reloadRule(); map.put("dbsource", ruleListInitUtil); if(map.size() > 0) {
sourceContext.collect(map);
}
}
} @Override
public void cancel() {
this.isRunning = false;
} @Override
public void close() throws Exception {
super.close(); }
}

  • 执行数据库操作类

/**
* 数据库规则表初始化
*
* @author 大数据江湖
* @Date:2021-5-17
*
* US,AREA_US
* TW,AREA_CT
* HK,AREA_CT
*
*/
public class DbBroadCastListInitUtil implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(DbBroadCastListInitUtil.class); // 数据库规则信息 public static Map<String, String> areasMap = new HashMap<String, String>(); static {
LOG.info("初始化 db 模块");
Connection dbConn = null; try { if (dbConn == null || dbConn.isClosed()) {
LOG.info("init dbConn start....");
LOG.info("init dbConn end....");
} HashMap<String, String> map = Maps.newHashMap(); map.put("US","AREA_US");
map.put("TW","AREA_CT");
map.put("HK","AREA_CT"); areasMap = map; } catch (Exception e) {
LOG.error("init database [status:error]", e);
throw new RuntimeException(" static article rule list db select error! , "+e.getMessage()) ; } finally {
if(dbConn != null) {
try {
dbConn.close();
} catch (SQLException e) {
LOG.error("dbConn conn close error!",e);
}
} }
} public static class Build { // 数据库规则信息 public static Map<String, String> newAreasMap = new HashMap<String, String>(); public void reloadRule() throws Exception {
LOG.info("重新初始化 DB reloadRule 模块");
Connection dbConn = null;
try {
if (dbConn == null || dbConn.isClosed()) {
LOG.info("init dbConn start....");
LOG.info("init dbConn end....");
} HashMap<String, String> map = Maps.newHashMap(); map.put("US","AREA_US");
map.put("TW","AREA_CT");
map.put("HK","AREA_CT");
map.put("AM","AREA_CT"); newAreasMap = map; } catch (Exception e) {
LOG.error("init database [status:error]", e);
throw e;
} finally {
if(dbConn != null) {
try {
dbConn.close();
} catch (SQLException e) {
LOG.error("dbConn conn close error!",e);
}
} }
} public static Map<String, String> getNewAreasMap() {
return newAreasMap;
}
} public static Build build() throws Exception {
final DbBroadCastListInitUtil.Build build = new DbBroadCastListInitUtil.Build();
build.reloadRule();
return build;
} }
 
  • 程序入口类

/**
* @author 大数据江湖
* @version 1.0
* @date 2021/4/25.
* <p>
* 使用 kafka 输出流和 redis 输出流 进行合并清洗
* <p>
* 线上使用的方式
*/
public class 广播方式3使用DB对方式广播 {
public static void main(String[] args) throws Exception { //1 获取执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致 //2 设置 checkpoint //开启checkpoint 一分钟一次
env.enableCheckpointing(60000);
//设置checkpoint 仅一次语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//两次checkpoint的时间间隔
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
//最多只支持1个checkpoint同时执行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
//checkpoint超时的时间
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 任务失败后也保留 checkPonit数据
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
3, // 尝试重启的次数
Time.of(10, TimeUnit.SECONDS) // 间隔
)); // 设置 checkpoint 路径
//env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint")); //3 设置 kafka Flink 消费
//创建 Kafka 消费信息 String topic = "data_flink_bigdata_test";
Properties consumerProperties = new Properties();
consumerProperties.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
consumerProperties.put("group.id", "data_flink_bigdata_test_consumer");
consumerProperties.put("enable.auto.commit", "false");
consumerProperties.put("auto.offset.reset", "earliest"); //4 获取 kafka 与 redis 数据源 FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties); DataStreamSource<String> kafkaSourceData = env.addSource(consumer); // 获取 redis 数据源并且进行广播 线上的广播也是 source + 广播方法 MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<String, String>(
"RedisBdStream",
String.class,
String.class
); //使用 数据库源 来进行广播 BroadcastStream<Map<String, Object>> broadcast = env.addSource(new BigDataDBBroadSource()).broadcast(descriptor); //5 两个数据源进行 ETL 处理 使用 connect 连接处理 数据库表信息进行广播 SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(broadcast).process(new MyETLProcessFunction()); //6 新创建一个 kafka 生产者 进行发送
String outputTopic = "allDataClean"; // 输出给下游 kafka /* Properties producerProperties = new Properties();
producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092"); FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
producerProperties); etlData.addSink(producer);*/ etlData.print(); //7 提交任务执行 env.execute("DataClean"); } /**
* in 1 kafka source
* in 2 redis source
* <p>
* out 合并后的source
*
*
* TODO 程序启动后发生的事:
*
* 1 运行 open 方法 ,触发静态方法给 areasMap 赋值
* 2 运行 processElement 方法前, areasMap 肯定是值的,正常进行处理
* 3 当到 BigDataDBBroadSource 轮训的时间后 ,刷新数据库表数据到 areasMap ,此时 areasMap 会加入新值,完成广播变量的更新
* 4 广播变量更新后 继续进行 processElement 数据处理
*
*/
private static class MyETLProcessFunction extends BroadcastProcessFunction<String, Map<String, Object>, String> { public Map<String, String> areasMap = new HashMap<String, String>(); @Override
public void open(Configuration parameters) throws Exception {
super.open(parameters); //触发静态方法去赋值
areasMap = DbBroadCastListInitUtil.areasMap; } //逻辑的处理方法 kafka 的数据
@Override
public void processElement(String line, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
//将 kafka 数据 按 redis 数据进行替换
// s -> kafka 数据
//allMap -> redis 数据
System.out.println("into processElement ");
JSONObject jsonObject = JSONObject.parseObject(line);
String dt = jsonObject.getString("dt");
String countryCode = jsonObject.getString("countryCode");
//可以根据countryCode获取大区的名字
// String area = allDataMap.get(countryCode);
//从MapState中获取对应的Code
String area =areasMap.get(countryCode); JSONArray data = jsonObject.getJSONArray("data");
for (int i = 0; i < data.size(); i++) {
JSONObject dataObject = data.getJSONObject(i);
System.out.println("大区:" + area);
dataObject.put("dt", dt);
dataObject.put("area", area);
//下游获取到数据的时候,也就是一个json格式的数据
collector.collect(dataObject.toJSONString());
} } @Override
public void processBroadcastElement(Map<String, Object> value, Context ctx, Collector<String> out) throws Exception { //广播算子定时刷新后 将数据发送到下游
if (value != null && value.size() > 0) {
Object obj = value.getOrDefault("dbsource", null);
if (obj != null) { DbBroadCastListInitUtil.Build biulder = (DbBroadCastListInitUtil.Build) obj;
//更新了 数据库数据
areasMap = biulder.getNewAreasMap();
System.out.println("数据库刷新算子运行完成!"); }
} }
} }
 

注意看最后处理函数启动后发生的事:

     

  1.  运行 open 方法 ,触发数据库操作工具类静态方法给 areasMap 赋值

  2.  运行执行类 processElement 方法前,此时 areasMap 肯定是值的,正常进行处理

  3.  当到数据库源轮训的时间后 ,刷新数据库表数据到 areasMap ,此时 areasMap 会加入新值,完成广播变量的更新

  4.  广播变量更新后 继续进行执行类 processElement 数据处理


至此 广播程序的使用介绍完了, 对于广播数据不需要改变的情况 参考基础样例;对于从缓存或数据库等获取广播变量,同时又需要改变的情况,参考生成样例即可。

PS:  文中代码地址  ----   https://gitee.com/fanpengyi0922/flink-window-broadcast

—   THE END  —

从 demo 到生产 - 手把手写出实战需求的 Flink 广播程序的更多相关文章

  1. 在Seismic.NET下用最少的语句写出一个剖面显示程序

    用Seismic.NET开发地震剖面显示程序可以节省大量的时间,下面的代码展开了如何用最少的代码显示一个SEGY文件. // 用一行语句把 reader, pipeline, view 和 plot ...

  2. 《重学 Java 设计模式》PDF 出炉了 - 小傅哥,肝了50天写出18万字271页的实战编程资料

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! @ 目录 一.前言 二.简介 1. 谁发明了设计模式? 2. 我怎么学不会设计模式? 3. 适 ...

  3. Swing:LookAndFeel 教程第一篇——手把手教你写出自己的 LookAndFeel

    本文是 LookAndFeel 系列教程的第一篇. 是我在对 Swing 学习摸索中的一些微薄经验. 我相信,细致看全然系列之后.你就能写出自己的 LookAndFeel. 你会发现 Swing 原来 ...

  4. 写出易调试的SQL(修订版)

    h4 { background: #698B22 !important; color: #FFFFFF; font-family: "微软雅黑", "宋体", ...

  5. 写出易调试的SQL

    h4 { background: #698B22 !important; color: #FFFFFF; font-family: "微软雅黑", "宋体", ...

  6. setTimeout和setInterval的区别以及如何写出效率高的倒计时

    1.setTimeout和setInterval都属于js中的定时器,可以规定延迟时间再执行某个操作,不同的是setTimeout在规定时间后执行完某个操作就停止了,而setInterval则可以一直 ...

  7. 如何用java写出无副作用的代码

    搞java的同学们可能对无副作用这个概念比较陌生,这是函数式编程中的一个概念,无副作用的意思就是: 一个函数(java里是方法)的多次调用中,只要输入参数的值相同,输出结果的值也必然相同,并且在这个函 ...

  8. 手把手写一个html_json信息源

    html_json用于从网页里提取json数据. 这里用新浪读书的书讯举个例子,手把手写一个html_json信息源. 打开新浪读书的首页,可以看到页面下方有最新.书讯.童书.小说等几个Tab,这里我 ...

  9. 写出形似QML的C++代码

    最开始想出的标题是<Declarative C++ GUI库>,但太标题党了.只写了两行代码,连Demo都算不上,怎么能叫库呢……后来想换掉“库”这个字,但始终找不到合适词来替换.最后还是 ...

随机推荐

  1. 翻译:《实用的Python编程》06_01_Iteration_protocol

    目录 | 上一节 (5.2 封装) | 下一节 (6.2 自定义迭代) 6.1 迭代协议 本节将探究迭代的底层过程. 迭代无处不在 许多对象都支持迭代: a = 'hello' for c in a: ...

  2. Tomcat详解系列(1) - 如何设计一个简单的web容器

    Tomcat - 如何设计一个简单的web容器 在学习Tomcat前,很多人先入为主的对它的认知是巨复杂的:所以第一步,在学习它之前,要打破这种观念,我们通过学习如何设计一个最基本的web容器来看它需 ...

  3. python登陆界面尝试

    示例1: """ 编写一个程序 用户可以输入用户名和密码 用户有三次机会 登录成功可以进行相应的操作 输入Q退出系统 """ name_li ...

  4. Mybatis自定义拦截器与插件开发

    在Spring中我们经常会使用到拦截器,在登录验证.日志记录.性能监控等场景中,通过使用拦截器允许我们在不改动业务代码的情况下,执行拦截器的方法来增强现有的逻辑.在mybatis中,同样也有这样的业务 ...

  5. 一本关于HTTP的恋爱日记

    1991年 8月 我叫客户端,英文名字 client. 她叫服务端,英文名字 server. 这一年,我们出生了. 是的,我们都是90后. 我爱她,可是她却远在天边. 为了和她可以互诉衷肠,我同时发明 ...

  6. Ansible 教程

    [注]本文译自:https://www.edureka.co/blog/ansible-tutorial/   在阅读本文之前,你应该已经知道,Ansible 构成了 DevOps 认证的关键部分,它 ...

  7. HTML(二):HTML常用标签(上)

    标签语义 学习标签是有技巧的,重点是记住每个标签的语义.简单理解就是指标签的含义,即这个标签是用来干嘛的. 根据标签的语义,在合适的地方给一个最为合理的标签,可以让页面结构更清晰. 标题标签<h ...

  8. 记一次metasploitable2内网渗透之8180端口tomcat

    扫描网段存活主机,确定内网metasploitable主机位置 nmap -T4 -sP 192.168.1.0/24 对目标主机进行扫描端口开放和系统信息 nmap -T4 -sV -Pn 192. ...

  9. Go-06-数据类型、常量、运算符

    数据类型转换 Go语言采用数据类型前置加括号的方式进行类型转换,格式如:T(表达式).T表示要转换的类型:表达式包括变量.数值.函数返回值等. var a int =100 b := float(a) ...

  10. (一)Docker-in-Docker on Kubernetes

    1. 场景 请参考docker in docker 文章 2. DinD 我们将采用主机Docker守护程序作为外部守护程序,Docker守护程序作为内部守护程序在容器内运行.运行DinD的一个重要方 ...