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

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

1 啥是广播 

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

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

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

2 用法总结

  1. //1 初始化数据
  2.  
  3. DataSet<Integer> toBroadcast = env.fromElements(1,2,3)
  4.  
  5. //2 广播数据 api
  6.  
  7. withBroadcastSet(toBroadcast,"broadcastSetName")
  8.  
  9. //3 获取数据
  10.  
  11. Collection<integer> broadcastSet = getRuntimeContext().getBroadcastVariable("broadcastSetName");

注意:

 

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

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

3 基础案例演示

  • 基础案例广播变量使用

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

  1. /**
  2. * @author 大数据江湖
  3. * @version 1.0
  4. * @date 2021/5/17.
  5. *
  6. */
  7. public class BaseBroadCast {
  8.  
  9. /**
  10. * broadcast广播变量
  11. * 需求:
  12. * flink会从数据源中获取到用户的姓名
  13. * 最终需要把用户的姓名和年龄信息打印出来
  14. * 分析:
  15. * 所以就需要在中间的map处理的时候获取用户的年龄信息
  16. * 建议吧用户的关系数据集使用广播变量进行处理
  17. *
  18. */
  19. public static void main(String[] args) throws Exception {
  20.  
  21. //获取运行环境
  22. ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
  23. //1:准备需要广播的数据
  24. ArrayList<Tuple2<String, Integer>> broadData = new ArrayList<>();
  25. broadData.add(new Tuple2<>("zs", 18));
  26. broadData.add(new Tuple2<>("ls", 20));
  27. broadData.add(new Tuple2<>("ww", 17));
  28. DataSet<Tuple2<String, Integer>> tupleData =
  29. env.fromCollection(broadData);
  30.  
  31. //1.1:处理需要广播的数据,把数据集转换成map类型,map中的key就是用户姓名,value就是用户年龄
  32.  
  33. DataSet<HashMap<String, Integer>> toBroadcast = tupleData.map(new MapFunction<Tuple2<String, Integer>, HashMap<String, Integer>>() {
  34. @Override
  35. public HashMap<String, Integer> map(Tuple2<String, Integer> value)
  36. throws Exception {
  37. HashMap<String, Integer> res = new HashMap<>();
  38. res.put(value.f0, value.f1);
  39. return res;
  40. }
  41. });
  42. //源数据
  43. DataSource<String> data = env.fromElements("zs", "ls", "ww");
  44. //注意:在这里需要使用到RichMapFunction获取广播变量
  45. DataSet<String> result = data.map(new RichMapFunction<String, String>() {
  46.  
  47. List<HashMap<String, Integer>> broadCastMap = new ArrayList<HashMap<String, Integer>>();
  48.  
  49. HashMap<String, Integer> allMap = new HashMap<String, Integer>();
  50.  
  51. /**
  52. * 这个方法只会执行一次
  53. * 可以在这里实现一些初始化的功能
  54. * 所以,就可以在open方法中获取广播变量数据
  55. */
  56. @Override
  57. public void open(Configuration parameters) throws Exception {
  58. super.open(parameters);
  59. //3:获取广播数据
  60. this.broadCastMap = getRuntimeContext().getBroadcastVariable("broadCastMapName");
  61. for (HashMap map : broadCastMap) {
  62. allMap.putAll(map);
  63. }
  64. }
  65.  
  66. @Override
  67. public String map(String value) throws Exception {
  68. Integer age = allMap.get(value);
  69. return value + "," + age;
  70. }
  71. }).withBroadcastSet(toBroadcast, "broadCastMapName");//2:执行广播数据的操作
  72. result.print();
  73. }
  74.  
  75. }

生产案例演示

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

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

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

 

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

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

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

  1. /**
  2. * 模拟数据源
  3. */
  4. public class kafkaProducer {
  5.  
  6. public static void main(String[] args) throws Exception{
  7. Properties prop = new Properties();
  8. //指定kafka broker地址
  9. prop.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  10. //指定key value的序列化方式
  11. prop.put("key.serializer", StringSerializer.class.getName());
  12. prop.put("value.serializer", StringSerializer.class.getName());
  13. //指定topic名称
  14. String topic = "data_flink_bigdata_test";
  15.  
  16. //创建producer链接
  17. KafkaProducer<String, String> producer = new KafkaProducer<String,String>(prop);
  18.  
  19. //{"dt":"2018-01-01 10:11:11","countryCode":"US","data":[{"type":"s1","score":0.3,"level":"A"},{"type":"s2","score":0.2,"level":"B"}]}
  20.  
  21. while(true){
  22. String message = "{\"dt\":\""+getCurrentTime()+"\",\"countryCode\":\""+getCountryCode()+"\",\"data\":[{\"type\":\""+getRandomType()+"\",\"score\":"+getRandomScore()+",\"level\":\""+getRandomLevel()+"\"},{\"type\":\""+getRandomType()+"\",\"score\":"+getRandomScore()+",\"level\":\""+getRandomLevel()+"\"}]}";
  23. System.out.println(message);
  24. //同步的方式,往Kafka里面生产数据
  25.  
  26. producer.send(new ProducerRecord<String, String>(topic,message));
  27.  
  28. Thread.sleep(2000);
  29. }
  30. //关闭链接
  31. //producer.close();
  32. }
  33.  
  34. public static String getCurrentTime(){
  35. SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
  36. return sdf.format(new Date());
  37. }
  38.  
  39. public static String getCountryCode(){
  40. String[] types = {"US","TW","HK","PK","KW","SA","IN"};
  41. Random random = new Random();
  42. int i = random.nextInt(types.length);
  43. return types[i];
  44. }
  45.  
  46. public static String getRandomType(){
  47. String[] types = {"s1","s2","s3","s4","s5"};
  48. Random random = new Random();
  49. int i = random.nextInt(types.length);
  50. return types[i];
  51. }
  52.  
  53. public static double getRandomScore(){
  54. double[] types = {0.3,0.2,0.1,0.5,0.8};
  55. Random random = new Random();
  56. int i = random.nextInt(types.length);
  57. return types[i];
  58. }
  59.  
  60. public static String getRandomLevel(){
  61. String[] types = {"A","A+","B","C","D"};
  62. Random random = new Random();
  63. int i = random.nextInt(types.length);
  64. return types[i];
  65. }
  66.  
  67. }
  1.  
  • redis 数据作为广播数据

  1. /**
  2. * redis中准备的数据源
  3. * source:
  4. *
  5. * hset areas AREA_US US
  6. * hset areas AREA_CT TW,HK
  7. * hset areas AREA_AR PK,KW,SA
  8. * hset areas AREA_IN IN
  9. *
  10. * result:
  11. *
  12. * HashMap
  13. *
  14. * US,AREA_US
  15. * TW,AREA_CT
  16. * HK,AREA_CT
  17. *
  18. */
  19. public class BigDataRedisSource implements SourceFunction<HashMap<String,String>> {
  20.  
  21. private Logger logger= LoggerFactory.getLogger(BigDataRedisSource.class);
  22.  
  23. private Jedis jedis;
  24. private boolean isRunning=true;
  25.  
  26. @Override
  27. public void run(SourceContext<HashMap<String, String>> cxt) throws Exception {
  28. this.jedis = new Jedis("localhost",6379);
  29. HashMap<String, String> map = new HashMap<>();
  30. while(isRunning){
  31. try{
  32. map.clear();
  33. Map<String, String> areas = jedis.hgetAll("areas");
  34. /**
  35. * AREA_CT TT,AA
  36. *
  37. * map:
  38. * TT,AREA_CT
  39. * AA,AREA_CT
  40. */
  41. for(Map.Entry<String,String> entry: areas.entrySet()){
  42. String area = entry.getKey();
  43. String value = entry.getValue();
  44. String[] fields = value.split(",");
  45. for(String country:fields){
  46. map.put(country,area);
  47. }
  48.  
  49. }
  50. if(map.size() > 0 ){
  51. cxt.collect(map);
  52. }
  53. Thread.sleep(60000);
  54. }catch (JedisConnectionException e){
  55. logger.error("redis连接异常",e.getCause());
  56. this.jedis = new Jedis("localhost",6379);
  57. }catch (Exception e){
  58. logger.error("数据源异常",e.getCause());
  59. }
  60.  
  61. }
  62.  
  63. }
  64.  
  65. @Override
  66. public void cancel() {
  67. isRunning=false;
  68. if(jedis != null){
  69. jedis.close();
  70. }
  71.  
  72. }
  73. }
  1.  
  • 程序入口类

  1. /**
  2. * @author 大数据江湖
  3. * @version 1.0
  4. * @date 2021/4/25.
  5. *
  6. *
  7. * 使用 kafka 输出流和 redis 输出流 进行合并清洗
  8. *
  9. *
  10. */
  11. public class 广播方式1分两个流进行connnect操作 {
  12. public static void main(String[] args) throws Exception {
  13.  
  14. //1 获取执行环境
  15.  
  16. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  17.  
  18. env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致
  19.  
  20. //2 设置 checkpoint
  21.  
  22. //开启checkpoint 一分钟一次
  23. env.enableCheckpointing(60000);
  24. //设置checkpoint 仅一次语义
  25. env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
  26. //两次checkpoint的时间间隔
  27. env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
  28. //最多只支持1个checkpoint同时执行
  29. env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
  30. //checkpoint超时的时间
  31. env.getCheckpointConfig().setCheckpointTimeout(60000);
  32. // 任务失败后也保留 checkPonit数据
  33. env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
  34.  
  35. env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
  36. 3, // 尝试重启的次数
  37. Time.of(10, TimeUnit.SECONDS) // 间隔
  38. ));
  39.  
  40. // 设置 checkpoint 路径
  41. // env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint"));
  42.  
  43. //3 设置 kafka Flink 消费
  44.  
  45. //创建 Kafka 消费信息
  46.  
  47. String topic="data_flink_bigdata_test";
  48. Properties consumerProperties = new Properties();
  49. consumerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  50. consumerProperties.put("group.id","data_test_new_1");
  51. consumerProperties.put("enable.auto.commit", "false");
  52. consumerProperties.put("auto.offset.reset","earliest");
  53.  
  54. //4 获取 kafka 与 redis 数据源
  55.  
  56. FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties);
  57.  
  58. DataStreamSource<String> kafkaSourceData = env.addSource(consumer);
  59.  
  60. //直接使用广播的方式 后续作为两个数据流来操作
  61.  
  62. DataStream<HashMap<String, String>> redisSourceData = env.addSource(new NxRedisSource()).broadcast();
  63.  
  64. //5 两个数据源进行 ETL 处理 使用 connect 连接处理
  65.  
  66. SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(redisSourceData).flatMap(new MyETLProcessFunction());
  67.  
  68. //6 新创建一个 kafka 生产者 进行发送
  69. String outputTopic="allDataClean";
  70.  
  71. // 输出给下游 kafka
  72.  
  73. Properties producerProperties = new Properties();
  74. producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  75.  
  76. FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
  77. new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
  78. producerProperties);
  79.  
  80. etlData.addSink(producer);
  81.  
  82. //7 提交任务执行
  83.  
  84. env.execute("DataClean");
  85.  
  86. }
  87.  
  88. /**
  89. * in 1 kafka source :
  90. *
  91. * {"dt":"2018-01-01 10:11:11","countryCode":"US","data":[{"type":"s1","score":0.3,"level":"A"},{"type":"s2","score":0.2,"level":"B"}]}
  92. *
  93. *
  94. * in 2 redis source
  95. *
  96. *
  97. * US,AREA_US
  98. * TW,AREA_CT
  99. * HK,AREA_CT
  100. *
  101. *
  102. *
  103. * out 合并后的source
  104. */
  105. private static class MyETLProcessFunction implements CoFlatMapFunction<String,HashMap<String,String>,String> {
  106.  
  107. //用来存储 redis 中的数据
  108. HashMap<String,String> allMap = new HashMap<String,String>();
  109.  
  110. @Override
  111. public void flatMap1(String line, Collector<String> collector) throws Exception {
  112.  
  113. //将 kafka 数据 按 redis 数据进行替换
  114. // s -> kafka 数据
  115. //allMap -> redis 数据
  116.  
  117. JSONObject jsonObject = JSONObject.parseObject(line);
  118. String dt = jsonObject.getString("dt");
  119. String countryCode = jsonObject.getString("countryCode");
  120. //可以根据countryCode获取大区的名字
  121. String area = allMap.get(countryCode);
  122. JSONArray data = jsonObject.getJSONArray("data");
  123. for (int i = 0; i < data.size(); i++) {
  124. JSONObject dataObject = data.getJSONObject(i);
  125. System.out.println("大区:"+area);
  126. dataObject.put("dt", dt);
  127. dataObject.put("area", area);
  128. //下游获取到数据的时候,也就是一个json格式的数据
  129. collector.collect(dataObject.toJSONString());
  130. }
  131.  
  132. }
  133.  
  134. @Override
  135. public void flatMap2(HashMap<String, String> stringStringHashMap, Collector<String> collector) throws Exception {
  136. //将 redis 中 数据进行赋值
  137. allMap = stringStringHashMap;
  138.  
  139. }
  140. }
  141. }

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

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

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

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

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

  1. /**
  2. * @author 大数据江湖
  3. * @version 1.0
  4. * @date 2021/4/25.
  5. * <p>
  6. * 使用 kafka 输出流和 redis 输出流 进行合并清洗
  7. * <p>
  8. * 线上使用的方式
  9. */
  10. public class 广播方式2使用MapState对方式1改造 {
  11. public static void main(String[] args) throws Exception {
  12.  
  13. //1 获取执行环境
  14.  
  15. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  16.  
  17. env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致
  18.  
  19. //2 设置 checkpoint
  20.  
  21. //开启checkpoint 一分钟一次
  22. env.enableCheckpointing(60000);
  23. //设置checkpoint 仅一次语义
  24. env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
  25. //两次checkpoint的时间间隔
  26. env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
  27. //最多只支持1个checkpoint同时执行
  28. env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
  29. //checkpoint超时的时间
  30. env.getCheckpointConfig().setCheckpointTimeout(60000);
  31. // 任务失败后也保留 checkPonit数据
  32. env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
  33.  
  34. env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
  35. 3, // 尝试重启的次数
  36. Time.of(10, TimeUnit.SECONDS) // 间隔
  37. ));
  38.  
  39. // 设置 checkpoint 路径
  40. //env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint"));
  41.  
  42. //3 设置 kafka Flink 消费
  43.  
  44. //创建 Kafka 消费信息
  45.  
  46. String topic = "data_flink_bigdata_test";
  47. Properties consumerProperties = new Properties();
  48. consumerProperties.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  49. consumerProperties.put("group.id", "data_flink_fpy_test_consumer");
  50. consumerProperties.put("enable.auto.commit", "false");
  51. consumerProperties.put("auto.offset.reset", "earliest");
  52.  
  53. //4 获取 kafka 与 redis 数据源
  54.  
  55. FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties);
  56.  
  57. DataStreamSource<String> kafkaSourceData = env.addSource(consumer);
  58.  
  59. // 获取 redis 数据源并且进行广播 线上的广播也是 source + 广播方法
  60.  
  61. MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<String, String>(
  62. "RedisBdStream",
  63. String.class,
  64. String.class
  65. );
  66.  
  67. //5 两个数据源进行 ETL 处理 使用 connect 连接处理 TODO process 替换 FlatMap
  68. //TODO 使用 MapState 来进行广播
  69. BroadcastStream<HashMap<String, String>> redisSourceData = env.addSource(new NxRedisSource()).broadcast(descriptor);
  70.  
  71. SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(redisSourceData).process(new MyETLProcessFunction());
  72.  
  73. //6 新创建一个 kafka 生产者 进行发送
  74. String outputTopic = "allDataClean";
  75.  
  76. // 输出给下游 kafka
  77.  
  78. Properties producerProperties = new Properties();
  79. producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  80.  
  81. FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
  82. new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
  83. producerProperties);
  84.  
  85. etlData.addSink(producer);
  86.  
  87. etlData.print();
  88.  
  89. //7 提交任务执行
  90.  
  91. env.execute("DataClean");
  92.  
  93. }
  94.  
  95. /**
  96. * in 1 kafka source
  97. * in 2 redis source
  98. * <p>
  99. * out 合并后的source
  100. */
  101. private static class MyETLProcessFunction extends BroadcastProcessFunction<String, HashMap<String, String>, String> {
  102.  
  103. // TODO 注意此处 descriptor 的名称需要与 广播时 (99行代码) 名称一致
  104. MapStateDescriptor<String, String> descriptor =
  105. new MapStateDescriptor<String, String>(
  106. "RedisBdStream",
  107. String.class,
  108. String.class
  109. );
  110.  
  111. //逻辑的处理方法 kafka 的数据
  112. @Override
  113. public void processElement(String line, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
  114. //将 kafka 数据 按 redis 数据进行替换
  115. // s -> kafka 数据
  116. //allMap -> redis 数据
  117. System.out.println("into processElement ");
  118. JSONObject jsonObject = JSONObject.parseObject(line);
  119. String dt = jsonObject.getString("dt");
  120. String countryCode = jsonObject.getString("countryCode");
  121. //可以根据countryCode获取大区的名字
  122.  
  123. // String area = allDataMap.get(countryCode);
  124. //TODO 从MapState中获取对应的Code
  125. String area = readOnlyContext.getBroadcastState(descriptor).get(countryCode);
  126.  
  127. JSONArray data = jsonObject.getJSONArray("data");
  128.  
  129. for (int i = 0; i < data.size(); i++) {
  130. JSONObject dataObject = data.getJSONObject(i);
  131. System.out.println("大区:" + area);
  132. dataObject.put("dt", dt);
  133. dataObject.put("area", area);
  134. //下游获取到数据的时候,也就是一个json格式的数据
  135. collector.collect(dataObject.toJSONString());
  136. }
  137.  
  138. }
  139.  
  140. //广播流的处理方法
  141. @Override
  142. public void processBroadcastElement(HashMap<String, String> stringStringHashMap, Context context, Collector<String> collector) throws Exception {
  143.  
  144. // 将接收到的控制数据放到 broadcast state 中
  145. //key , flink
  146. // 将 RedisMap中的值放入 MapState 中
  147. for (Map.Entry<String, String> entry : stringStringHashMap.entrySet()) {
  148. //TODO 使用 MapState 存储 redis 数据
  149. context.getBroadcastState(descriptor).put(entry.getKey(), entry.getValue());
  150. System.out.println(entry);
  151. }
  152.  
  153. }
  154. }
  155. }
  1.  

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

 

需求:

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

思路:

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

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

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

  • 数据库表广播源

  1. /**
  2. * @author 大数据江湖
  3. * @Date:2021-5-17
  4. * DB source 源头 进行广播
  5. */
  6. public class BigDataDBBroadSource extends RichSourceFunction<Map<String,Object>> {
  7.  
  8. private final Logger logger = LoggerFactory.getLogger(BigDataDBBroadSource.class);
  9. private volatile boolean isRunning = true;
  10.  
  11. public BigDataDBBroadSource() {
  12.  
  13. }
  14.  
  15. @Override
  16. public void open(Configuration parameters) throws Exception {
  17. super.open(parameters);
  18.  
  19. }
  20.  
  21. @Override
  22. public void run(SourceContext<Map<String,Object>> sourceContext) throws Exception {
  23.  
  24. while (isRunning) {
  25. //TODO 使用的是一个 DB 源头的 source 60 s 刷新一次 进行往下游发送
  26. TimeUnit.SECONDS.sleep(60);
  27.  
  28. Map<String,Object> map = new HashMap<String,Object>();
  29.  
  30. //规则匹配关键词
  31.  
  32. final DbBroadCastListInitUtil.Build ruleListInitUtil = new DbBroadCastListInitUtil.Build();
  33.  
  34. ruleListInitUtil.reloadRule();
  35.  
  36. map.put("dbsource", ruleListInitUtil);
  37.  
  38. if(map.size() > 0) {
  39. sourceContext.collect(map);
  40. }
  41. }
  42. }
  43.  
  44. @Override
  45. public void cancel() {
  46. this.isRunning = false;
  47. }
  48.  
  49. @Override
  50. public void close() throws Exception {
  51. super.close();
  52.  
  53. }
  54. }
  1.  
  • 执行数据库操作类

  1. /**
  2. * 数据库规则表初始化
  3. *
  4. * @author 大数据江湖
  5. * @Date:2021-5-17
  6. *
  7. * US,AREA_US
  8. * TW,AREA_CT
  9. * HK,AREA_CT
  10. *
  11. */
  12. public class DbBroadCastListInitUtil implements Serializable {
  13.  
  14. private static final Logger LOG = LoggerFactory.getLogger(DbBroadCastListInitUtil.class);
  15.  
  16. // 数据库规则信息
  17.  
  18. public static Map<String, String> areasMap = new HashMap<String, String>();
  19.  
  20. static {
  21. LOG.info("初始化 db 模块");
  22. Connection dbConn = null;
  23.  
  24. try {
  25.  
  26. if (dbConn == null || dbConn.isClosed()) {
  27. LOG.info("init dbConn start....");
  28. LOG.info("init dbConn end....");
  29. }
  30.  
  31. HashMap<String, String> map = Maps.newHashMap();
  32.  
  33. map.put("US","AREA_US");
  34. map.put("TW","AREA_CT");
  35. map.put("HK","AREA_CT");
  36.  
  37. areasMap = map;
  38.  
  39. } catch (Exception e) {
  40. LOG.error("init database [status:error]", e);
  41. throw new RuntimeException(" static article rule list db select error! , "+e.getMessage()) ;
  42.  
  43. } finally {
  44. if(dbConn != null) {
  45. try {
  46. dbConn.close();
  47. } catch (SQLException e) {
  48. LOG.error("dbConn conn close error!",e);
  49. }
  50. }
  51.  
  52. }
  53. }
  54.  
  55. public static class Build {
  56.  
  57. // 数据库规则信息
  58.  
  59. public static Map<String, String> newAreasMap = new HashMap<String, String>();
  60.  
  61. public void reloadRule() throws Exception {
  62. LOG.info("重新初始化 DB reloadRule 模块");
  63. Connection dbConn = null;
  64. try {
  65. if (dbConn == null || dbConn.isClosed()) {
  66. LOG.info("init dbConn start....");
  67. LOG.info("init dbConn end....");
  68. }
  69.  
  70. HashMap<String, String> map = Maps.newHashMap();
  71.  
  72. map.put("US","AREA_US");
  73. map.put("TW","AREA_CT");
  74. map.put("HK","AREA_CT");
  75. map.put("AM","AREA_CT");
  76.  
  77. newAreasMap = map;
  78.  
  79. } catch (Exception e) {
  80. LOG.error("init database [status:error]", e);
  81. throw e;
  82. } finally {
  83. if(dbConn != null) {
  84. try {
  85. dbConn.close();
  86. } catch (SQLException e) {
  87. LOG.error("dbConn conn close error!",e);
  88. }
  89. }
  90.  
  91. }
  92. }
  93.  
  94. public static Map<String, String> getNewAreasMap() {
  95. return newAreasMap;
  96. }
  97. }
  98.  
  99. public static Build build() throws Exception {
  100. final DbBroadCastListInitUtil.Build build = new DbBroadCastListInitUtil.Build();
  101. build.reloadRule();
  102. return build;
  103. }
  104.  
  105. }
  1.  
  • 程序入口类

  1. /**
  2. * @author 大数据江湖
  3. * @version 1.0
  4. * @date 2021/4/25.
  5. * <p>
  6. * 使用 kafka 输出流和 redis 输出流 进行合并清洗
  7. * <p>
  8. * 线上使用的方式
  9. */
  10. public class 广播方式3使用DB对方式广播 {
  11. public static void main(String[] args) throws Exception {
  12.  
  13. //1 获取执行环境
  14.  
  15. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  16.  
  17. env.setParallelism(3);//并行度取决于 kafka 中的分区数 保持与kafka 一致
  18.  
  19. //2 设置 checkpoint
  20.  
  21. //开启checkpoint 一分钟一次
  22. env.enableCheckpointing(60000);
  23. //设置checkpoint 仅一次语义
  24. env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
  25. //两次checkpoint的时间间隔
  26. env.getCheckpointConfig().setMinPauseBetweenCheckpoints(5000);
  27. //最多只支持1个checkpoint同时执行
  28. env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
  29. //checkpoint超时的时间
  30. env.getCheckpointConfig().setCheckpointTimeout(60000);
  31. // 任务失败后也保留 checkPonit数据
  32. env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
  33.  
  34. env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
  35. 3, // 尝试重启的次数
  36. Time.of(10, TimeUnit.SECONDS) // 间隔
  37. ));
  38.  
  39. // 设置 checkpoint 路径
  40. //env.setStateBackend(new FsStateBackend("hdfs://192.168.123.103:9000/flink/checkpoint"));
  41.  
  42. //3 设置 kafka Flink 消费
  43. //创建 Kafka 消费信息
  44.  
  45. String topic = "data_flink_bigdata_test";
  46. Properties consumerProperties = new Properties();
  47. consumerProperties.put("bootstrap.servers", "10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  48. consumerProperties.put("group.id", "data_flink_bigdata_test_consumer");
  49. consumerProperties.put("enable.auto.commit", "false");
  50. consumerProperties.put("auto.offset.reset", "earliest");
  51.  
  52. //4 获取 kafka 与 redis 数据源
  53.  
  54. FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), consumerProperties);
  55.  
  56. DataStreamSource<String> kafkaSourceData = env.addSource(consumer);
  57.  
  58. // 获取 redis 数据源并且进行广播 线上的广播也是 source + 广播方法
  59.  
  60. MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<String, String>(
  61. "RedisBdStream",
  62. String.class,
  63. String.class
  64. );
  65.  
  66. //使用 数据库源 来进行广播
  67.  
  68. BroadcastStream<Map<String, Object>> broadcast = env.addSource(new BigDataDBBroadSource()).broadcast(descriptor);
  69.  
  70. //5 两个数据源进行 ETL 处理 使用 connect 连接处理 数据库表信息进行广播
  71.  
  72. SingleOutputStreamOperator<String> etlData = kafkaSourceData.connect(broadcast).process(new MyETLProcessFunction());
  73.  
  74. //6 新创建一个 kafka 生产者 进行发送
  75. String outputTopic = "allDataClean";
  76.  
  77. // 输出给下游 kafka
  78.  
  79. /* Properties producerProperties = new Properties();
  80. producerProperties.put("bootstrap.servers","10.20.7.20:9092,10.20.7.51:9092,10.20.7.50:9092");
  81.  
  82. FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>(outputTopic,
  83. new KeyedSerializationSchemaWrapper<String>(new SimpleStringSchema()),
  84. producerProperties);
  85.  
  86. etlData.addSink(producer);*/
  87.  
  88. etlData.print();
  89.  
  90. //7 提交任务执行
  91.  
  92. env.execute("DataClean");
  93.  
  94. }
  95.  
  96. /**
  97. * in 1 kafka source
  98. * in 2 redis source
  99. * <p>
  100. * out 合并后的source
  101. *
  102. *
  103. * TODO 程序启动后发生的事:
  104. *
  105. * 1 运行 open 方法 ,触发静态方法给 areasMap 赋值
  106. * 2 运行 processElement 方法前, areasMap 肯定是值的,正常进行处理
  107. * 3 当到 BigDataDBBroadSource 轮训的时间后 ,刷新数据库表数据到 areasMap ,此时 areasMap 会加入新值,完成广播变量的更新
  108. * 4 广播变量更新后 继续进行 processElement 数据处理
  109. *
  110. */
  111. private static class MyETLProcessFunction extends BroadcastProcessFunction<String, Map<String, Object>, String> {
  112.  
  113. public Map<String, String> areasMap = new HashMap<String, String>();
  114.  
  115. @Override
  116. public void open(Configuration parameters) throws Exception {
  117. super.open(parameters);
  118.  
  119. //触发静态方法去赋值
  120. areasMap = DbBroadCastListInitUtil.areasMap;
  121.  
  122. }
  123.  
  124. //逻辑的处理方法 kafka 的数据
  125. @Override
  126. public void processElement(String line, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
  127. //将 kafka 数据 按 redis 数据进行替换
  128. // s -> kafka 数据
  129. //allMap -> redis 数据
  130. System.out.println("into processElement ");
  131. JSONObject jsonObject = JSONObject.parseObject(line);
  132. String dt = jsonObject.getString("dt");
  133. String countryCode = jsonObject.getString("countryCode");
  134. //可以根据countryCode获取大区的名字
  135. // String area = allDataMap.get(countryCode);
  136. //从MapState中获取对应的Code
  137. String area =areasMap.get(countryCode);
  138.  
  139. JSONArray data = jsonObject.getJSONArray("data");
  140. for (int i = 0; i < data.size(); i++) {
  141. JSONObject dataObject = data.getJSONObject(i);
  142. System.out.println("大区:" + area);
  143. dataObject.put("dt", dt);
  144. dataObject.put("area", area);
  145. //下游获取到数据的时候,也就是一个json格式的数据
  146. collector.collect(dataObject.toJSONString());
  147. }
  148.  
  149. }
  150.  
  151. @Override
  152. public void processBroadcastElement(Map<String, Object> value, Context ctx, Collector<String> out) throws Exception {
  153.  
  154. //广播算子定时刷新后 将数据发送到下游
  155. if (value != null && value.size() > 0) {
  156. Object obj = value.getOrDefault("dbsource", null);
  157. if (obj != null) {
  158.  
  159. DbBroadCastListInitUtil.Build biulder = (DbBroadCastListInitUtil.Build) obj;
  160. //更新了 数据库数据
  161. areasMap = biulder.getNewAreasMap();
  162. System.out.println("数据库刷新算子运行完成!");
  163.  
  164. }
  165. }
  166.  
  167. }
  168. }
  169.  
  170. }
  1.  

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

     

  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. 1.mysql读写

    一.数据库读取(mysql) 参数 接受 作用 默认 sql or table_name string 读取的表名,或sql语句 无 con 数据库连接 数据库连接信息 无 index_col Int ...

  2. python-类的隐藏和封装

    7 """ 8 封装是面对对象的三大特征之一(另外两个是集成和多态),它指的是将对象> 的信息隐藏在对象的内部,不允许外部程序直接访问对象内部信息,而是通> ...

  3. Kubernetes 常见问题总结

    Kubernetes 常见问题总结 如何删除不一致状态下的 rc,deployment,service 在某些情况下,经常发现 kubectl 进程挂起现象,然后在 get 时候发现删了一半,而另外的 ...

  4. Golang学习的方法和建议

    学习方法: 学习方向:go方向是没有问题的 学习方法:多思考多练习,注重语法和关键词练习,切记哑巴学习,会看不会写,切记注意多写 课外学习,数据结构和算法:清华 谭浩强老师(链表.数组.排序...等等 ...

  5. 我的xshell配色方案,绿色/护眼/留存/备份

    [mycolor] text(bold)=e9e9e9 magenta(bold)=ff00ff text=00ff80 white(bold)=fdf6e3 green=80ff00 red(bol ...

  6. 一文带大家彻底搞懂Hystrix!

    前言? Netflix Hystrix断路器是什么? Netflix Hystrix是SOA/微服务架构中提供服务隔离.熔断.降级机制的工具/框架.Netflix Hystrix是断路器的一种实现,用 ...

  7. SpringBoot 集成测试

    一. 测试一般程序(Service/DAO/Util类) 1. 在pom.xml中引入依赖 2. 生成测试类 <1> 如果使用IntelliJ IDEA,可以使用快捷键直接生成: Wind ...

  8. python工业互联网应用实战13—基于selenium的功能测试

    本章节我们再来说说测试,单元测试和功能测试.单元测试我们在数据验证章节简单提过了,本章我们进一步如何用单元测试来测试view的功能代码:同时,也涉及一下基于selenium的功能测试做法.笔者过去的项 ...

  9. 破解class文件的第一步:深入理解JAVA Class文件

    摘要: java定义了一套与操作系统,硬件无关的字节码格式,这个字节码就是用java class文件来表示的,java class文件内部定义了虚拟机可以识别的字节码格式,这个格式是平台无关性的. j ...

  10. Day16_89_通过反射机制获取所有构造方法

    通过反射机制获取某个特定的构造方法 * 代码 import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; publ ...