Flink常用算子Transformation(轉換)

在之前的《Flink DataStream API》一文中,我們列舉了一些Flink自帶且常用的transformation算子,例如map、flatMap等。在Flink的編程體系中,我們獲取到數據源之後,需要經過一系列的處理即transformation操作,再將最終結果輸出到目的Sink(ES、mysql或者hdfs),使數據落地。因此,除了正確的繼承重寫RichSourceFunction<>和RichSinkFunction<>之外,最終要的就是實時處理這部分,下面的圖介紹了Flink代碼執行流程以及各模塊的組成部分。

在Storm中,我們常常用Bolt的層級關係來表示各個數據的流向關係,組成一個拓撲。在Flink中,Transformation算子就是將一個或多個DataStream轉換爲新的DataStream,可以將多個轉換組合成複雜的數據流拓撲。如下圖所示,DataStream會由不同的Transformation操作,轉換、過濾、聚合成其他不同的流,從而完成我們的業務要求。

那麼以《Flink從kafka中讀數據存入Mysql Sink》一文中的業務場景作爲基礎,在Flink讀取Kafka的數據之後,進行不同的算子操作來分別詳細介紹一下各個Transformation算子的用法。Flink消費的數據格式依然是JSON格式:{"city":"合肥","loginTime":"2019-04-17 19:04:32","os":"Mac OS","phoneName":"vivo"}

1、map

map:輸入一個元素,輸出一個元素,可以用來做一些清洗工作。

/**
 * create by xiax.xpu on @Date 2019/4/11 20:47
 */
public class FlinkSubmitter {
    public static void main(String[] args) throws Exception{
        //獲取運行時環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //checkpoint配置
        //爲了能夠使用支持容錯的kafka Consumer,開啓checkpoint機制,支持容錯,保存某個狀態信息
        env.enableCheckpointing(5000);
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
        env.getCheckpointConfig().setCheckpointTimeout(60000);
        env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);


        //kafka配置文件
        Properties props = new Properties();
        props.put("bootstrap.servers", "192.168.83.129:9092");
        props.setProperty("group.id","con1");
        props.put("zookeeper.connect","192.168.83.129:2181");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");  //key 反序列化
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); //value 反序列化

        System.out.println("ready to print");
        FlinkKafkaConsumer011<String> consumer = new FlinkKafkaConsumer011<>(
                "kafka_flink_mysql",
                new SimpleStringSchema(),
                props);
        consumer.setStartFromGroupOffsets();//默認消費策略


        SingleOutputStreamOperator<Entity> StreamRecord = env.addSource(consumer)
                .map(string -> JSON.parseObject(string, Entity.class))
                .setParallelism(1);
        //融合一些transformation算子進來
        //map:輸入一個元素,輸出一個元素,可以用來做一些清洗工作
        SingleOutputStreamOperator<Entity> result = StreamRecord.map(new MapFunction<Entity, Entity>() {
            @Override
            public Entity map(Entity value) throws Exception {
                Entity entity1 = new Entity();
                entity1.city = value.city+".XPU.Xiax";
                entity1.phoneName = value.phoneName.toUpperCase();
                entity1.loginTime = value.loginTime;
                entity1.os = value.os;
                return entity1;
            }
        });
        result.print().setParallelism(1);
        env.execute("new one");

    }
}

本例中我們將獲取的JSON字符串轉換到Entity object之後,使用map算子讓所有的phoneName編程大寫,city後面添加XPU.Xiax後綴。

2、flatMap

flatMap:打平操作,我們可以理解爲將輸入的元素壓平,從而對輸出結果的數量不做要求,可以爲0、1或者多個都OK。它和Map相似,但引入flatMap的原因是因爲一般java方法的返回值結果都是一個,因此引入flatMap來區別這個。

//flatMap, 輸入一個元素,返回0個、1個或者多個元素
SingleOutputStreamOperator<Entity> result = StreamRecord
      .flatMap(new FlatMapFunction<Entity, Entity>() {
       @Override
       public void flatMap(Entity entity, Collector<Entity> out) throws Exception {
            if (entity.city.equals("北京")) {
                 out.collect(entity);
             }
        }
});

這裏我們將所有city是北京的結果集聚合輸出,注意這裏並不是過濾,有些人可能會困惑這不是起了過濾filter的作用嗎,其實不然,只是這裏的用法剛好相似而已。簡單分析一下,new FlatMapFunction<Entity, Entity>,接收的輸入是Entity實體,發出的也是Entity實體類,看到這就可以與Map對應上了。

3、filter

filter:過濾篩選,將所有符合判斷條件的結果集輸出

//filter 判斷條件輸出
SingleOutputStreamOperator<Entity> result = StreamRecord
       .filter(new FilterFunction<Entity>() {
        @Override
       public boolean filter(Entity entity) throws Exception {
              if (entity.phoneName.equals("HUAWEI")) {
                  return true;
              }
              return false;
       }
});

這裏我們將所有phoneName是HUAWEI的值過濾,在直接輸出。

4、keyBy

keyBy:在邏輯上將Stream根據指定的Key進行分區,是根據key的Hash值進行分區的。

//keyBy 從邏輯上對邏輯分區
KeyedStream<Entity, String> result = StreamRecord
     .keyBy(new KeySelector<Entity, String>() {
     @Override
    public String getKey(Entity entity) throws Exception {
          return entity.os;
    }
});

這裏只是對DataStream進行分區而已,按照os進行分區,然而這輸出的效果其實沒什麼變化


由於下面這些操作,在之前模擬生成的數據,去做轉換操作不太適合。因此每個操作附上其他demo

5、reduce

reduce:屬於歸併操作,它能將3的keyedStream轉換成DataStream,Reduce 返回單個的結果值,並且 reduce 操作每處理每一天新數據時總是創建一個新值。常用聚合操作例如min、max等都可使用reduce方法實現。這裏通過實現一個Socket的wordCount簡單例子,來幫助瞭解flatMap/keyBy/reduce/window等操作的過程。

package com.bigdata.flink.Stream;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

/**
 * 滑動窗口的計算
 *
 * 通過socket模擬產生單詞數據 flink對其進行統計計數
 * 實現時間窗口:
 *              每隔1秒統計前兩秒的數據
 */
public class SocketWindowWordCount {
    public static void main(String[] args) throws Exception{
        //定義端口號,通過cli接收
        int port;
        try{
            ParameterTool parameterTool = ParameterTool.fromArgs(args);
            port = parameterTool.getInt("port");
        }catch(Exception e){
            System.err.println("No port Set, use default port---java");
            port = 9000;
        }

        //獲取運行時環境,必須要
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //綁定Source,通過master的nc -l 900 產生單詞
        String hostname = "192.168.83.129";
        String delimiter = "\n";
        //連接socket 綁定數據源
        DataStreamSource<String> socketWord = env.socketTextStream(hostname, port, delimiter);

        DataStream<WordWithCount> windowcounts = socketWord.flatMap(new FlatMapFunction<String, WordWithCount>() {
            public void flatMap(String value, Collector<WordWithCount> out) throws Exception {
                String[] splits = value.split("\\s");
                for (String word : splits) {
                    out.collect(new WordWithCount(word, 1));
                }
            }
        }).keyBy("word")     
                //.sum("count");//這裏求聚合 可以用reduce和sum兩種方式
                .reduce(new ReduceFunction<WordWithCount>() {
                    public WordWithCount reduce(WordWithCount a, WordWithCount b) throws Exception {
                        return new WordWithCount(a.word, a.count + b.count);
                    }
                });
        windowcounts.print().setParallelism(1);
        env.execute("socketWindow");
    }

    public static class  WordWithCount{
        public String word;
        public int count;
        //無參的構造函數
        public WordWithCount(){

        }
        //有參的構造函數
        public WordWithCount(String word, int count){
            this.count = count;
            this.word = word;
        }

        @Override
        public String toString() {
            return "WordWithCount{" +
                    "word='" + word + '\'' +
                    ", count=" + count +
                    '}';
        }
    }
}

 

這裏只是做單詞計數,至於爲什麼有的單詞重複出現,但是請注意它後面的count值都不一樣,我們直接生成了toString方法打印出的結果。

6、aggregations

aggregations:進行一些聚合操作,例如sum(),min(),max()等,這些可以用於keyedStream從而獲得聚合。用法如下

KeyedStream.sum(0)或者KeyedStream.sum(“Key”)

7、unoin

union:可以將多個流合併到一個流中,以便對合並的流進行統一處理,有點類似於Storm中的將上一級的兩個Bolt數據匯聚到這一級同一個Bolt中。注意,合併的流類型需要一致

//1.獲取執行環境配置信息
 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
 //2.定義加載或創建數據源(source),監聽9000端口的socket消息
 DataStream<String> textStream9000 = env.socketTextStream("localhost", 9000, "\n");
 DataStream<String> textStream9001 = env.socketTextStream("localhost", 9001, "\n");
 DataStream<String> textStream9002 = env.socketTextStream("localhost", 9002, "\n");

 DataStream<String> mapStream9000=textStream9000.map(s->"來自9000端口:"+s);
 DataStream<String> mapStream9001=textStream9001.map(s->"來自9001端口:"+s);
 DataStream<String> mapStream9002=textStream9002.map(s->"來自9002端口:"+s);

//3.union用來合併兩個或者多個流的數據,統一到一個流中
 DataStream<String> result =  mapStream9000.union(mapStream9001,mapStream9002);

//4.打印輸出sink
 result.print();
//5.開始執行
 env.execute();

8、connect

connect:和union類似,但是隻能連接兩個流,兩個流的數據類型可以不同,會對兩個流中的數據應用不同的處理方法。

        //獲取Flink運行環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        //綁定數據源
        DataStreamSource<Long> text1 = env.addSource(new MyParalleSource()).setParallelism(1);
        DataStreamSource<Long> text2 = env.addSource(new MyParalleSource()).setParallelism(1);

        //爲了演示connect的不同,將第二個source的值轉換爲string
        SingleOutputStreamOperator<String> text2_str = text2.map(new MapFunction<Long, String>() {
            @Override
            public String map(Long value) throws Exception {
                return "str" + value;
            }
        });

        ConnectedStreams<Long, String> connectStream = text1.connect(text2_str);

        SingleOutputStreamOperator<Object> result = connectStream.map(new CoMapFunction<Long, String, Object>() {
            @Override
            public Object map1(Long value) throws Exception {
                return value;
            }

            @Override
            public Object map2(String value) throws Exception {
                return value;
            }
        });

        //打印到控制檯,並行度爲1
        result.print().setParallelism(1);
        env.execute( "StreamingDemoWithMyNoParalleSource");

9、split

split:根據規則吧一個數據流切分成多個流,可能在實際場景中,源數據流中混合了多種類似的數據,多種類型的數據處理規則不一樣,所以就可以根據一定的規則把一個數據流切分成多個數據流。

//獲取Flink運行環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        //綁定數據源
        DataStreamSource<Long> text = env.addSource(new MyParalleSource()).setParallelism(1);
        //對流進行切分 奇數偶數進行區分
        SplitStream<Long> splitString = text.split(new OutputSelector<Long>() {
            @Override
            public Iterable<String> select(Long value) {
                ArrayList<String> output = new ArrayList<>();
                if (value % 2 == 0) {
                    output.add("even");//偶數
                } else {
                    output.add("odd");//奇數
                }

                return output;
            }
        });

        //選擇一個或者多個切分後的流
        DataStream<Long> evenStream = splitString.select("even");//選擇偶數
        DataStream<Long> oddStream = splitString.select("odd");//選擇奇數

        DataStream<Long> moreStream = splitString.select("odd","even");//選擇多個流
        //打印到控制檯,並行度爲1
        evenStream.print().setParallelism(1);
        env.execute( "StreamingDemoWithMyNoParalleSource");

10、window以及windowAll

window:按時間進行聚合或者其他條件對KeyedStream進行分組,用法:inputStream.keyBy(0).window(Time.seconds(10));

windowAll: 函數允許對常規數據流進行分組。通常,這是非並行數據轉換,因爲它在非分區數據流上運行。用法:inputStream.keyBy(0).windowAll(Time.seconds(10));

關於時間窗口,這個我們後期會詳細說一下,敬請關注。

 

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