Flink - DataStream Transformations

571 阅读4分钟

本次学习 Flink 中关于 DataStream 中的源数据进行转化操作的 API.

基本操作

map

将函数作用在集合中的每一个元素上,并返回作用后的结果。

image.png

flatMap

将集合中的每个元素变成一个或多个元素,并返回扁平化之后的结果。

image.png

keyBy

按照指定的 key 来对流中的数据进行分组。注意:流处理中没有 groupBy,而是 keyBy.

image.png

filter

按照指定的条件对集合中的元素进行过滤,过滤出返回 true/符合条件的元素。

image.png

reduce

对集合中的元素进行聚合。

image.png

下面通过一个例子来应用下以上的几种 API。

需求:对流数据中的单词进行计数,并过滤掉敏感词汇 TMD。

package com.learn.flink.transformations;

import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.util.Arrays;

public class TransformationDemo1 {

    public static void main(String[] args) throws Exception {
        //0: env
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //1: source
        DataStreamSource<String> ds = env.socketTextStream("node01", 999);

        //2. transformation
        // 切割
        DataStream<String> words = ds.flatMap((String lines, Collector<String> out) -> Arrays.stream(lines.split(" ")).forEach(out::collect))
                .returns(Types.STRING);
        // 过滤
        DataStream<String> filterWords = words.filter((String word) -> !word.equalsIgnoreCase("TMD")).returns(Types.STRING);
        // 将每个单词转化为单词和数量记为 1 的格式
        DataStream<Tuple2<String, Integer>> wordAndOne = filterWords.map((String word) -> Tuple2.of(word, 1))
                .returns(Types.TUPLE(Types.STRING, Types.INT));
        // 分组
        KeyedStream<Tuple2<String, Integer>, String> grouped = wordAndOne.keyBy(value -> value.f0);
        // 聚合
        SingleOutputStreamOperator<Tuple2<String, Integer>> sum = grouped.sum(1);
        SingleOutputStreamOperator<Tuple2<String, Integer>> reduce = grouped.reduce((Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) -> Tuple2.of(value1.f0, value1.f1 + value2.f1));

        //3: sink
        sum.print();
        reduce.print();
        //4: execute
        env.execute();
    }
}

合并与连接

union
算子可以合并多个 同类型的数据流,并生成同类型的数据流,即可以将多个 DataStream[T] 合并为一个新的DataStream[T]。数据将按照先进先出(first in first out)的模式合并。

image.png connect
connect 提供了和 union 类似的功能,用来连接两个数据流。

image.png

connect 与 union 的区别在于:

  • connect 只能连接两个数据流,union 可以连接多个数据流。
  • connect 连接的数据流的数据类型可以不一致, union 所连接的两个数据流的类型必须相同。
  • 多个数据流经过 union 之后,仅仅是数据进行先进先出的合并。而两个 DataStream 数据流经过 connect 之后会被转化为 ConnectedStreams ,ConnectedStreams 会对两个数据应用不同的处理方法,且双流之间可以共享状态。
package com.learn.flink.transformations;

import org.apache.flink.streaming.api.datastream.ConnectedStreams;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.CoMapFunction;

/**
 * transformation - 合并与连接
 */
public class TransformationDemo2 {

    public static void main(String[] args) throws Exception {
        //0: env
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //1: source
        final DataStream<String> ds1 = env.fromElements("hello1", "apache1", "flink1");
        final DataStream<String> ds2 = env.fromElements("hello2", "apache2", "flink2");
        final DataStream<Long> ds3 = env.fromSequence(1, 10);
        //2. transformation
        // union 可以合并同类型的数据流
        final DataStream<String> ds1Uds2 = ds1.union(ds2);
        final DataStream<String> ds2Uds1 = ds2.union(ds1);
        //Cannot resolve method 'union(org.apache.flink.streaming.api.datastream.DataStream<java.lang.Long>)'
//        final DataStream<String> union = ds1.union(ds3);

        // connect 可以合并 2 个同类型的或者不同类型的数据源
        final ConnectedStreams<String, String> ds1Cds2 = ds1.connect(ds2);
        final ConnectedStreams<String, Long> ds1Cds3 = ds1.connect(ds3);
        /**
         *     OUT map1(IN1 var1) throws Exception;
         *     OUT map2(IN2 var1) throws Exception;
         */
        final DataStream<String> ds1Cds3Map = ds1Cds3.map(new CoMapFunction<String, Long, String>() {
            // 用于处理 ConnectedStreams 的第一个参数
            @Override
            public String map1(String s) throws Exception {
                return "String:" + s;
            }

            // 用于处理 ConnectedStreams 的第二个参数
            @Override
            public String map2(Long aLong) throws Exception {
                return "Long:" + aLong;
            }
        });
        //3: sink
        ds1Uds2.print();
//        ds2Uds1.print();
        // connect 合并后的数据源,需要经过后续的处理
//        ds1Cds3.print();
        ds1Cds3Map.print();
        //4: execute
        env.execute();
    }
}

结果:

image.png

拆分和选择

split:将一个流分成多个流。 Select:获取分流后对应的数据。
以上 2 个方法在 flink 1.12 之后已经过期并移除,所以实现拆分的方法使用下面的 OutputTag 和 process 来实现。

Side Outputs:可以使用 process 方法对流中的数据进行处理,并且针对不同的处理结果将数据收集到不同的 OutputTag 中。

package com.learn.flink.transformations;

import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

/**
 * transformation - 拆分和选择
 */
public class TransformationDemo3 {

    public static void main(String[] args) throws Exception {
        //0: env
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //1: source
        final DataStream<Integer> ds = env.fromElements( 1, 2, 3,4,5,6,7,8,9,10);
        //2. transformation
        final OutputTag<Integer> oddTag = new OutputTag<>("奇数", TypeInformation.of(Integer.class));
        final OutputTag<Integer> evenTag = new OutputTag<>("偶数", TypeInformation.of(Integer.class));

        final SingleOutputStreamOperator<Integer> processDS = ds.process(new ProcessFunction<Integer, Integer>() {
            /**
             * @param value : 数据源中的每个数据
             * @param context
             * @param collector: 收集器可以收集每个处理后的数据
             * @throws Exception
             */
            @Override
            public void processElement(Integer value, Context context, Collector<Integer> collector) throws Exception {
                if (value % 2 == 0) {
                    // 偶数
                    context.output(evenTag, value);
                } else {
                    context.output(oddTag, value);
                }
            }
        });

        final DataStream<Integer> oddOutput = processDS.getSideOutput(oddTag);
        final DataStream<Integer> evenOutput = processDS.getSideOutput(evenTag);
        //3: sink

        oddOutput.print("奇数:");
        evenOutput.print("偶数:");

        //4: execute
        env.execute();
    }
}

结果:

image.png

重新平衡 rebalance

在数据的处理过程中有可能会出现如图下的数据倾斜,每个分区分配的处理的数据很不均衡,其他 3 台机器即使执行完毕也要等待机器 1 执行完毕后才算整体将任务完成。

image.png

Flink 中提供的 rebalance 方法可以直接解决这种数据倾斜的情况。

package com.learn.flink.transformations;

import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

/**
 * transformation - 重平衡分区 rebalance
 */
public class TransformationDemo4 {

    public static void main(String[] args) throws Exception {
        //0: env
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.BATCH);
        //1: source
        DataStream<Long> ds = env.fromSequence(1, 100);
        //2. transformation
        DataStream<Long> filterDS = ds.filter((Long value) -> value > 10);

        final DataStream<Tuple2<Integer, Integer>> result1 = filterDS
                .map(new RichMapFunction<Long, Tuple2<Integer, Integer>>() {
                        @Override
                        public Tuple2<Integer, Integer> map(Long value) throws Exception {
                            // 获取子任务的 ID 即分区的编号
                            final int subtaskId = getRuntimeContext().getIndexOfThisSubtask();
                            return Tuple2.of(subtaskId, 1);
                        }
                })
                .keyBy(t -> t.f0)
                .sum(1); // 按照分区编号分组并聚合

        final DataStream<Tuple2<Integer, Integer>> result2 = filterDS
                .rebalance()
                .map(new RichMapFunction<Long, Tuple2<Integer, Integer>>() {
                    @Override
                    public Tuple2<Integer, Integer> map(Long value) throws Exception {
                        // 获取子任务的 ID 即分区的编号
                        final int subtaskId = getRuntimeContext().getIndexOfThisSubtask();
                        return Tuple2.of(subtaskId, 1);
                    }
        }).keyBy(t -> t.f0).sum(1); // 按照分区编号分组并聚合

        //3: sink
        result1.print("result1:");
        result2.print("result2:");

        //4: execute
        env.execute();
    }
}

结果:

image.png