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(); } }
4 生產案例演示
實際生產中有時候是需要更新廣播變數的,但不是實時更新的,一般會設定一個更新週期,幾分鐘,幾小時的都很常見,根據業務而定。
由於廣播變數需要更新,解決辦法一般是需要將廣播變數做成另一個 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 標識點):
-
進行資料廣播時需要使用 MapStateDescriptor 進行註冊
-
進行兩個流合併處理時 使用 process 函式
-
處理函式中使用 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("資料庫重新整理運算元執行完成!"); } } } } }
注意看最後處理函式啟動後發生的事:
-
執行 open 方法 ,觸發資料庫操作工具類靜態方法給 areasMap 賦值
-
執行執行類 processElement 方法前,此時 areasMap 肯定是值的,正常進行處理
-
當到資料庫源輪訓的時間後 ,重新整理資料庫表資料到 areasMap ,此時 areasMap 會加入新值,完成廣播變數的更新
-
廣播變數更新後 繼續進行執行類 processElement 資料處理
至此 廣播程式的使用介紹完了, 對於廣播資料不需要改變的情況 參考基礎樣例;對於從快取或資料庫等獲取廣播變數,同時又需要改變的情況,參考生成樣例即可。
PS: 文中程式碼地址 ---- https://gitee.com/fanpengyi0922/flink-window-broadcast
— THE END —