Flink Transformation API

(二)DataStream API

DataStream是Flink编写流处理作业的API。我们前面说过一个完整的Flink处理程序应该包含三部分:数据源(Source)、转换操作(Transformation)、结果接收(Sink)。下面我们从这三部分来看DataStream API。

(四)数据转换(Transformation)

数据处理的核心就是对数据进行各种转化操作,在Flink上就是通过转换将一个或多个DataStream转换成新的DataStream。

为了更好的理解transformation函数,下面给出匿名类的方式来实现各个函数。

所有转换函数都是依赖以下基础:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> dataStream = env.readTextFile("/opt/yjz/flink/flink-1.7.2/README.txt");

Aggregations

1.介绍

Aggregations 为聚合函数的总称,常见的聚合函数包括但不限于 sum、max、min 等。

事实上,对于 Aggregations 函数,Flink 帮助我们封装了状态数据,这些状态数据不会被清理,所以在实际生产环境中应该尽量避免在一个无限流上使用 Aggregations。而且,对于同一个 keyedStream ,只能调用一次 Aggregation 函数。

不建议的是那些状态无限增长的聚合,实际应用中一般会配合窗口使用。使得状态不会无限制扩张。

2.例子

滚动聚合具有相同key的数据流元素,我们可以指定需要聚合的字段(field)。DataStream<T>中的T为聚合之后的结果。

//对KeyedStream中元素的第一个Filed求和
DataStream<String> dataStream1 = keyedStream.sum(0);
//对KeyedStream中元素的“count”字段求和
keyedStream.sum("count");
//获取keyedStream中第一个字段的最小值
keyedStream.min(0);
//获取keyedStream中count字段的最小值的元素
keyedStream.minBy("count");
keyedStream.max("count");
keyedStream.maxBy(0);

min和minBy的区别是:min返回指定字段的最小值,而minBy返回最小值所在的元素。

AggregateOperator<Tuple2<String, Long>> sum = wordAndOneUG.sum(1);
SingleOutputStreamOperator<Tuple2<String, Integer>> sumDS = wordAndOneKS.sum(1);

转换前后数据类型:KeyedStream->DataStream

broadcast

使用broadcast可以向每个分区广播元素。

dataStream.broadcast();

转换前后的数据类型:DataStream->DataStream

CoFlatMap/CoMap

可以对连接流执行类似map和flatMap操作。

connectedStreams.map(new CoMapFunction<String, String, String>() {
            @Override
            public String map1(String value) throws Exception {
                return value.toUpperCase();
            }
            @Override
            public String map2(String value) throws Exception {
                return value.toLowerCase();
            }
        });

转换前后的数据类型:ConnectedDataStreams->DataStream

Connect

连接(connect)两个流,并且保留其类型。两个数据流之间可以共享状态。ConnectedStreams<IN1,IN2>中IN1代表第一个数据流中的数据类型,IN2代表第二个数据流中的数据类型。

ConnectedStreams<String,String> connectedStreams = dataStream.connect(dataStream);

转换前后的数据类型:DataStream,DataStream->ConnectedDataStreams

Extract Timestamps(Deprecated)

从记录中提取时间戳,以便使用事件时间语义窗口。之后会专门来看Flink的Event Time。

dataStream.assignTimestamps(new TimestampExtractor<String>() {
            @Override
            public long extractTimestamp(String element, long currentTimestamp) {
                return 0;
            }
            @Override
            public long extractWatermark(String element, long currentTimestamp) {
                return 0;
            }
            @Override
            public long getCurrentWatermark() {
                return 0;
            }
        });

该方法以被标记为废弃!

转换前后的数据类型:DataStream->DataStream。

Filter

1.介绍

顾名思义,Fliter 的意思就是过滤掉不需要的数据,每个元素都会被 filter 函数处理,如果 filter 函数返回 true 则保留,否则丢弃。

2.例子

过滤指定元素数据,如果返回true则该元素继续向下传递,如果为false则将该元素过滤掉。FilterFunction<T>中T代表输入元素的数据类型。

dataStream.filter(new FilterFunction<String>() {
    @Override
    public boolean filter(String line) throws Exception {
        if(line.contains("flink"))
            return true;
        else
            return false;
    }
});

转换前后数据类型:DataStream->DataStream

FlatMap

1.介绍

FlatMap 接受一个元素,返回零到多个元素。接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出,但是当返回值是列表的时候,FlatMap 会将列表“平铺”,也就是以单个元素的形式进行输出

2.例子

输入一个元素,输出0个、1个或多个元素。FlatMapFunction<T,V>中T代表输入元素数据类型(flatMap方法的第一个参数类型),V代表输出集合中元素类型(flatMap中的Collector类型参数)

        SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOneDS = lineDS
                .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                        // 按照 空格 切分
                        String[] words = value.split(" ");
                        for (String word : words) {
                            // 转换成 二元组 (word,1)
                            Tuple2<String, Integer> wordsAndOne = Tuple2.of(word, 1);
                            // 通过 采集器 向下游发送数据
                            out.collect(wordsAndOne);
                        }
                    }
                });

转换前后数据类型:DataStream->DataStream

3.return

...
.flatMap(
    (String value, Collector<String> out) -> {
        String[] words = value.split(" ");
        for (String word : words) {
            out.collect(word);
         }
     }
)
.returns(Types.STRING)
...

Fold(Deprecated)

Fold功能和Reduce类似,但是Fold提供了初始值,从初始值开始滚动对相同的key记录进行滚动合并。FoldFunction<T,V>中的T为KeyStream中元素数据类型,V为初始值类型和fold方法返回值类型。

keyedStream.fold(0, new FoldFunction<String, Integer>() {
    @Override
    public Integer fold(Integer accumulator, String value) throws Exception {
        return accumulator + value.split(" ").length;
    }
});

该方法已经标记为废弃!

转换前后数据类型:KeyedStream->DataStream

Interval Join

对指定的时间间隔内使用公共key来连接两个KeyedStream。ProcessJoinFunction<IN1,IN2,OUT>中IN1为第一个DataStream中元素数据类型,IN2为第二个DataStream中的元素数据类型,OUT为结果输出类型。

 keyedStream
            .intervalJoin(keyedStream)
            .between(Time.milliseconds(-2),Time.milliseconds(2))//间隔时间
            .lowerBoundExclusive()//并不包含下限时间
            .upperBoundExclusive()
            .process(new ProcessJoinFunction<String, String, String>() {
                @Override
                public void processElement(String left, String right, Context ctx, Collector<String> out) throws Exception {
                    //...
                }
            });

Iterate

可以使用iterate方法来获取IterativeStream。

IterativeStream<String> iterativeStream = dataStream.iterate();

转换前后的数据类型:DataStream->IterativeStream

KeyBy

1.介绍

在介绍 KeyBy 函数之前,需要你理解一个概念:KeyedStream。 在实际业务中,我们经常会需要根据数据的某种属性或者单纯某个字段进行分组,然后对不同的组进行不同的处理。举个例子,当我们需要描述一个用户画像时,则需要根据用户的不同行为事件进行加权;再比如,我们在监控双十一的交易大盘时,则需要按照商品的品类进行分组,分别计算销售额。

我们在使用 KeyBy 函数时会把 DataStream 转换成为 KeyedStream,事实上 KeyedStream 继承了 DataStream,KeyedStream 中的元素会根据用户传入的参数进行分组。

在生产环境中使用 KeyBy 函数时要十分注意!该函数会把数据按照用户指定的 key 进行分组,那么相同分组的数据会被分发到一个 subtask 上进行处理,在大数据量和 key 分布不均匀的时非常容易出现数据倾斜和反压,导致任务失败。

常见的解决方式是把所有数据加上随机前后缀,这个需要进行深入讲解。

groupBy用于批计算(DataSet),keyBy用于流计算(DataStream)

2.例子

逻辑上将数据流元素进行分区,具有相同key的记录将被划分到同一分区。指定Key的方式有多种,这个我们在之前说过了。返回类型KeyedStream<T,KEY>中T代表KeyedStream中元素数据类型,KEY代表虚拟KEY的数据类型。

KeyedStream<String,Tuple> keyedStream = dataStream.keyBy(0);

以下情况的元素不能作为key使用:

  1. POJO类型,但没有重写hashCode(),而是依赖Object.hashCode()。
  2. 该元素是数组类型。
        KeyedStream<Tuple2<String, Integer>, String> wordAndOneKS = wordAndOneDS.keyBy(
                new KeySelector<Tuple2<String, Integer>, String>() {
                    @Override
                    public String getKey(Tuple2<String, Integer> value) throws Exception {
                        return value.f0;
                    }
                }
        );

keyBy内部使用散列来实现的。
转换前后数据类型:DataStream->KeyedStream

Map

1.介绍

Map 接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出。

2.例子

接受一个元素,输出一个元素。MapFunction<T,V>中T代表输入数据类型(map方法的参数类型),V代表操作结果输出类型(map方法返回数据类型)。

dataStream.map(new MapFunction<String, String>() {
    @Override
    public String map(String line) throws Exception {
        return line.toUpperCase();
    }
});

3.return

在一般情况下,Java会擦除泛型类型信息。 Flink尝试使用Java保留的少量位(主要是函数签名和子类信息)通过反射重建尽可能多的类型信息。对于函数的返回类型取决于其输入类型的情况,此逻辑还包含一些简单类型推断:

在Flink无法重建已擦除的泛型类型信息的情况下,Java API提供所谓的类型提示。类型提示告诉系统函数生成的数据流或数据集的类型:

...
.map(word -> Tuple2.of(word, 1))
.returns(Types.TUPLE(Types.STRING,Types.INT))
...

min & minBy

min和minBy之间的区别是min返回最小值,而minBy返回在此字段中具有最小值的元素(与max和maxBy相同)。

但是事实上,min与max 也会返回整个元素。

不同的是min会根据指定的字段取最小值,并且把这个值保存在对应的位置上,对于其他的字段取了最先获取的值,不能保证每个元素的数值正确,max同理。

minBy会返回指定字段取最小值的元素,并且会覆盖指定字段小于当前已找到的最小值元素。maxBy同理。

        DataStreamSource<Event> stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Mary", "./cart", 2000L)
        );

        stream.keyBy(e -> e.user).max("timestamp").print("max");

        stream.keyBy(e -> e.user).maxBy("timestamp").print("maxBy");

max> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:01.0}
maxBy> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:01.0}
max> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:02.0}
maxBy> Event{user='Mary', url='./cart', timestamp=1970-01-01 08:00:02.0}

partitionCustom

自定义分区。使用用户自定义的分区函数对指定key进行分区,partitionCustom只支持单分区。

dataStream.partitionCustom(new Partitioner<String>() {
            
            @Override
            public int partition(String key, int numPartitions) {
                return key.hashCode() % numPartitions;
            }
        },1);

转换前后的数据类型:DataStream->DataStream

Project

对元组类型的DataStream可以使用Project选取子元组。

DataStream<Tuple2<String,Integer>> dataStream2 = dataStream.project(0,2);

转换前后的数据类型:DataStream->DataStream

rebalance

以轮询的方式为每个分区均衡分配元素,对于优化数据倾斜该方法非常有效。

dataStream.rebalance();

转换前后的数据类型:DataStream->DataStream

Reduce

1.介绍

Reduce 函数的原理是,会在每一个分组的 keyedStream 上生效,它会按照用户自定义的聚合逻辑进行分组聚合。

2.例子

对指定的“虚拟”key相同的记录进行滚动合并,也就是最后一次的reduce结果当前元素进行reduce操作。ReduceFunction<T>中的T代表KeyStream中元素的数据类型。

对第一个参数,即最后一次的reduce结果,进行改变;同时返回值也是最后一次的reduce结果,即第一个参数。

...
.keyBy(r -> r.f0) // 使用用户名来进行分流 .reduce(new ReduceFunction<Tuple2<String, Long>>() {   @Override   public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {     return Tuple2.of(value1.f0, value1.f1 + value2.f1);   } })
...

rescale

根据上下游task数进行分区。

dataStream.rescale();

转换前后的数据类型:DataStream->DataStream

Select

我们可以对SplitStream分开的流进行选择,可以将其转换成一个或多个DataStream。

splitStream.select("flink");
splitStream.select("other");

shuffle

随机分区。均匀随机将元素进行分区。

dataStream.shuffle();

Split(Deprecated)

我们可以根据某些规则将数据流切分成两个或多个数据流。

dataStream.split(new OutputSelector<String>() {
            @Override
            public Iterable<String> select(String value) {
                List<String> outList = new ArrayList<>();
                if(value.contains("flink"))
                    outList.add("flink");
                else 
                    outList.add("other");
                return outList;
            }
        });

该方法底层引用以被标记为废弃!

转换前后的数据类型:DataStream->SplitStream

Union

联合(Union)两个或多个DataStream,所有DataStream中的元素都会组合成一个新的DataStream。如果联合自身,则每个元素在新的DataStream出现两次。

dataStream.union(dataStream1);

转换前后的数据类型:DataStream*->DataStream

window

对已经分区的KeyedStream上定义窗口,Window会根据某些规则(比如在最后5s到达的数据)对虚拟“key”相同的记录进行分组。WindowedStream<T, K, W extends Window>中的T为KeyedStream中元素数据类型,K为指定Key的数据类型,W为我们所使用的窗口类型

WindowedStream<String,Tuple,TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));

转换前后的数据类型:KeyedStream->WindowedStream

Window Apply

将整个窗口应用在指定函数上,可以对WindowedStream和AllWindowedStream使用。WindowFunction<IN, OUT, KEY, W extends Window>中的IN为输入元素类型,OUT为输出类型元素,KEY为指定的key类型,W为所使用的窗口类型。

windowedStream.apply(new WindowFunction<String, String, Tuple, TimeWindow>() {
    @Override
    public void apply(Tuple tuple, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
        int sumCount = 0;
        for(String line : input){
            sumCount += line.split(" ").length;
        }
        out.collect(String.valueOf(sumCount));
    }
});

AllWindowedStream使用与WindowedStream类似。

转换前后的数据类型:WindowedStream->DataStreamAllWindowedStream->DataStream

windowAll

我们也可以在常规DataStream上使用窗口,Window根据某些条件(比如最后5s到达的数据)对所有流事件进行分组。AllWindowedStream<T,W extends Window>中的T为DataStream中元素的数据类型,W为我们所使用的窗口类型。

AllWindowedStream<String,TimeWindow> allWindowedStream = dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));

注意:该方法在许多时候并不是并行执行的,所有记录都会收集到一个task中

转换前后的数据类型:DataStream->AllWindowedStream

Window CoGroup

对两个指定的key的DataStream在公共窗口上执行CoGroup,和Join功能类似,但是更加灵活。CoGroupFunction<IN1,IN2,OUT>,IN1代表第一个DataStream中元素类型,IN2代表第二个DataStream中元素类型,OUT为结果输出集合类型。

dataStream
          .coGroup(dataStream1)
          .where(new MyKeySelector()).equalTo(new MyKeySelector())
          .window(TumblingEventTimeWindows.of(Time.seconds(5)))
          .apply(new CoGroupFunction<String, String, String>() {
              @Override
              public void coGroup(Iterable<String> first, Iterable<String> second, Collector<String> out) throws Exception {
 
              }
          });

转换前后的数据类型:DataStream,DataStream->DataStream

Window Join

在指定key的公共窗口上连接两个数据流。JoinFunction<IN1,IN2,OUT>中的IN1为第一个DataStream中元素数据类型,IN2为第二个DataStream中元素数据类型,OUT为Join结果数据类型。

dataStream
          .join(dataStream1)
          .where(new MyKeySelector()).equalTo(new MyKeySelector())
          .window(TumblingEventTimeWindows.of(Time.seconds(5)))
          .apply(new JoinFunction<String, String, String>() {
              @Override
              public String join(String first, String second) throws Exception {
                  return first + second;
              }
          });

转换前后的数据类型:DataStream,DataStream->DataStream

Window Reduce/Fold/Aggregation/

对于WindowedStream数据流我们同样也可以应用Reduce、Fold、Aggregation函数。

windowedStream.reduce(new ReduceFunction<String>() {
    @Override
    public String reduce(String value1, String value2) throws Exception {
        return value1 + value2;
    }
});
windowedStream.fold(0, new FoldFunction<String, Integer>() {
    @Override
    public Integer fold(Integer accumulator, String value) throws Exception {
        return accumulator + value.split(" ").length;
    }
});
windowedStream.sum(0);
windowedStream.max("count");

转换前后的数据类型:WindowedStream->DataStream

 

posted @ 2023-07-03 10:19  ImreW  阅读(14)  评论(0编辑  收藏  举报