Flink13基础-DataStream API(转换算子 Transformation)

一、基本转换算子

1. 映射(map)

用于将数据流中的数据进行转换,形成新的数据流

  1. 自定义MapFunction

 // 自定义MapFunction
 public static class MyMapper implements MapFunction {

    @Override
    public String map(Event value) throws Exception {
         return value.user;
    }
}

加载Map方法

// 1. 使用自定义类,实现MapFunction接口
SingleOutputStreamOperator result1 = stream.map(new MyMapper());
  1. 使用匿名类实现MapFunction

// 2. 使用匿名类实现MapFunction接口
SingleOutputStreamOperator result2 = stream.map(new MapFunction() {

      @Override
      public String map(Event event) throws Exception {
           return event.user;
       }
});
  1. 传入lambda表达式

SingleOutputStreamOperator result3 = stream.map(data -> data.user);

完整代码

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;


public class TransformMapTest {

    public static void main(String[] args) throws Exception {
        // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L)
        );

        // 进行转换计算,提取user字段
        // 1. 使用自定义类,实现MapFunction接口
        SingleOutputStreamOperator result1 = stream.map(new MyMapper());

        // 2. 使用匿名类实现MapFunction接口
        SingleOutputStreamOperator result2 = stream.map(new MapFunction() {
            @Override
            public String map(Event event) throws Exception {
                return event.user;
            }
        });

        // 3. 传入lambda表达式
        SingleOutputStreamOperator result3 = stream.map(data -> data.user);

        result3.print();

        env.execute();

    }

    // 自定义MapFunction
    public static class MyMapper implements MapFunction {

        @Override
        public String map(Event value) throws Exception {
            return value.user;
        }
    }
}

2. 过滤(filter)

通过一个布尔条件表达式设置过滤条件,对于每一个流内元素进行判断,若为 true 则元素正常输出,若为 false 则元素被过滤掉

进行 filter 转换之后的新数据流的数据类型与原数据流是相同的。filter 转换需要传入的参

数需要实现 FilterFunction 接口,而 FilterFunction 内要实现 filter()方法,就相当于一个返回布

尔类型的条件表达式。

  1. 实现FilterFunction的类对象

 // 实现一个自定义的FilterFunction
 public static class MyFilter implements FilterFunction {

     @Override
     public boolean filter(Event event) throws Exception {

          return event.user.equals("Mary");
     }
}

加载filter方法

 // 1. 传入一个实现了FilterFunction的类的对象
 SingleOutputStreamOperator result1 = stream.filter(new MyFilter());
  1. 一个匿名类实现FilterFunction的接口

SingleOutputStreamOperator result2 = stream.filter(new FilterFunction() {
      @Override
      public boolean filter(Event event) throws Exception {
          return event.user.equals("Bob");
      }
});
  1. 传入lambda表达式

stream.filter(data -> data.user.equals("Alice")).print("lambda: Alice click");

完整代码

import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class TransformFilterTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L)
        );

        // 1. 传入一个实现了FilterFunction的类的对象
        SingleOutputStreamOperator result1 = stream.filter(new MyFilter());

        // 2. 传入一个匿名类实现FilterFunction接口
        SingleOutputStreamOperator result2 = stream.filter(new FilterFunction() {
            @Override
            public boolean filter(Event event) throws Exception {
                return event.user.equals("Bob");
            }
        });

        // 3. 传入lambda表达式
        stream.filter(data -> data.user.equals("Alice")).print("lambda: Alice click");

        result2.print();

        env.execute();
    }

    // 实现一个自定义的FilterFunction
    public static class MyFilter implements FilterFunction {

        @Override
        public boolean filter(Event event) throws Exception {

            return event.user.equals("Mary");
        }
    }
}

3. 扁平映射(flatMap)一对多

flatMap 操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个

一个的个体使用。消费一个元素,可以产生 0 到多个元素。flatMap 可以认为是“扁平化”(flatten)

和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,再对拆分

后的元素做转换处理

同 map 一样,flatMap 也可以使用 Lambda 表达式或者 FlatMapFunction 接口实现类的方式

来进行传参,返回值类型取决于所传参数的具体逻辑,可以与原数据流相同,也可以不同。

flatMap 操作会应用在每一个输入事件上面,FlatMapFunction 接口中定义了 flatMap 方法,用户可以重写这个方法,在这个方法中对输入数据进行处理,并决定是返回 0 个、1 个或多个结果数据。因此 flatMap 并没有直接定义返回值类型,而是通过一个“收集器”(Collector)来指定输出。希望输出结果时,只要调用收集器的.collect()方法就可以了;这个方法可以多次调用,也可以不调用。所以 flatMap 方法也可以实现 map 方法和 filter 方法的功能,当返回结果是 0 个的时候,就相当于对数据进行了过滤,当返回结果是 1 个的时候,相当于对数据进行了简单的转换操作。

  1. 实现一个自定义的FlatMapFunction

public static class MyFlatMap implements FlatMapFunction {

    @Override
    public void flatMap(Event event, Collector collector) throws Exception {
        collector.collect(event.user);
        collector.collect(event.url);
        collector.collect(event.timestamp.toString());
     }
}

加载flatMap方法

// 1. 实现FlatMapFunction
stream.flatMap(new MyFlatMap()).print();
  1. 传入一个Lambda表达式

stream.flatMap((Event value,Collector out) -> {
            if (value.user.equals("Mary"))
                out.collect(value.url);
            else if (value.user.equals("Bob")) {
                out.collect(value.user);
                out.collect(value.url);
                out.collect(value.timestamp.toString());
            }
        }).returns(new TypeHint(){}).print();

完整代码

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

public class TransformFlatMapTest {
    public static void main(String[] args) throws Exception {
        // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L)
        );

        // 1. 实现FlatMapFunction
        stream.flatMap(new MyFlatMap()).print();

        stream.flatMap(new FlatMapFunction() {
            @Override
            public void flatMap(Event value, Collector out) throws Exception {
                if (value.user.equals("Mary"))
                    out.collect(value.url);
                else if (value.user.equals("Bob")) {
                    out.collect(value.user);
                    out.collect(value.url);
                    out.collect(value.timestamp.toString());
                }
            }
        });

        // 2. 传入一个Lambda表达式
        stream.flatMap((Event value,Collector out) -> {
            if (value.user.equals("Mary"))
                out.collect(value.url);
            else if (value.user.equals("Bob")) {
                out.collect(value.user);
                out.collect(value.url);
                out.collect(value.timestamp.toString());
            }
        }).returns(new TypeHint(){}).print();

        env.execute();
    }

    // 实现一个自定义的FlatMapFunction
    public static class MyFlatMap implements FlatMapFunction {

        @Override
        public void flatMap(Event event, Collector collector) throws Exception {
            collector.collect(event.user);
            collector.collect(event.url);
            collector.collect(event.timestamp.toString());
        }
    }
}

3. 聚合算子(Aggregation)

当计算的结果不仅依赖当前数据,还跟之前的数据有关,相当于要把所有数据聚在一起进行汇总合并

——这就是所谓的“聚合”(Aggregation),也对应着MapReduce中的reduce操作。

1. 按键分区(keyBy)

在Flink中,要做聚合,需要先进行分区;这个操作就是通过keyBy来完成的。keyBy是聚合前必须要用到的一个算子。keyBy通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务,也就对应着任务槽(task slot)。keyby算子后,具有相同的key的数据,都将被发往同一个分区,那么下一步算子操作就将会在同一个slot中进行处理了。

在内部,是通过计算key的哈希值(hash code),对分区数进行取模运算来实现的。所以这里key如果是POJO的话,必须要重写hashCode()方法。

keyBy()方法需要传入一个参数,这个参数指定了一个或一组key。有很多不同的方法来指

定key:比如对于Tuple数据类型,可以指定字段的位置或者多个位置的组合;对于POJO类

型,可以指定字段的名称(String);另外,还可以传入Lambda表达式或者实现一个键选择器

(KeySelector),用于说明从数据中提取key的逻辑。

我们可以以id作为key做一个分区操作,代码实现如下:

1.使用Lambda表达式

KeyedStream keyedStream = stream.keyBy(e -> e.user);

2.使用匿名类实现KeySelector

KeyedStream keyedStream1 = stream.keyBy(new KeySelector() {
    @Override
    public String getKey(Event e) throws Exception {
        return e.user;
    }
});

需要注意的是,keyBy得到的结果将不再是DataStream,而是会将DataStream转换为

KeyedStream。KeyedStream可以认为是“分区流”或者“键控流”,它是对DataStream按照

key的一个逻辑分区,所以泛型有两个类型:除去当前流中的元素类型外,还需要指定key的

类型。

KeyedStream也继承自DataStream,所以基于它的操作也都归属于DataStream API。但它

跟之前的转换操作得到的SingleOutputStreamOperator不同,只是一个流的分区操作,并不是

一个转换算子。KeyedStream是一个非常重要的数据结构,只有基于它才可以做后续的聚合操

作(比如sum,reduce);而且它可以将当前算子任务的状态(state)也按照key进行划分、限

定为仅对当前key有效。

2. 简单聚合

有了按键分区的数据流KeyedStream,我们就可以基于它进行聚合操作了。Flink为我们

内置实现了一些最基本、最简单的聚合API,主要有以下几种:

sum():在输入流上,对指定的字段做叠加求和的操作。

min():在输入流上,对指定的字段求最小值。

max():在输入流上,对指定的字段求最大值。

minBy():与min()类似,在输入流上针对指定字段求最小值。不同的是,min()只计

算指定字段的最小值,其他字段会保留最初第一个数据的值;而minBy()则会返回包

含字段最小值的整条数据。

maxBy():与max()类似,在输入流上针对指定字段求最大值。两者区别与

min()/minBy()完全一致。

完整代码

import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class TransformSimpleAggTest {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Bob", "./prod?id=100", 3000L),
                new Event("Bob", "./home", 4000L)
        );

        // 按分组之后的进行聚合,提取当前用户最近一次访问数据
        // max: 是提取当前 max字段的值,其余字段保存上次的记录
        stream.keyBy(new KeySelector() {
            @Override
            public String getKey(Event event) throws Exception {
                return event.user;
            }
        }).max("timestamp").print("max:");

        // maxBy是完整的一条数据
        stream.keyBy(data -> data.user)
                .maxBy("timestamp")
                .print("maxBy:");

        env.execute();

    }
}

3. 归约聚合(reduce

我们对reduce操作就不陌生:它可以对已有的数据进行归约处理,把每一个新输入的数据和当前已经归约出来的值,再做一个聚合计算。

与简单聚合类似,reduce 操作也会将 KeyedStream 转换为 DataStream。它不会改变流的元 素数据类型,所以输出类型和输入类型是一样的。 调用 KeyedStream 的 reduce 方法时,需要传入一个参数,实现 ReduceFunction 接口。接 口在源码中的定义如下:


public interface ReduceFunction extends Function, Serializable {
    T reduce(T value1, T value2) throws Exception;
}

我们将数据流按照用户id进行分区,然后用一个reduce算子实现sum的功能,统计每个

用户访问的频次;进而将所有统计结果分到一组,用另一个reduce算子实现maxBy的功能,

记录所有用户中访问频次最高的那个,也就是当前访问量最大的用户是谁。

完整代码

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class TransformReduceTest {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Bob", "./prod?id=100", 3000L),
                new Event("Bob", "./home", 4000L)
        );

        // 1. 统计每个用户的访问频次
        SingleOutputStreamOperator> reduce = stream.map(new MapFunction>() {

            @Override
            public Tuple2 map(Event event) throws Exception {
                return Tuple2.of(event.user, 1L);
            }
        }).keyBy(new KeySelector, String>() {
            @Override
            public String getKey(Tuple2 stringLongTuple2) throws Exception {
                return stringLongTuple2.f0;
            }
        }).reduce(new ReduceFunction>() {
            @Override
            public Tuple2 reduce(Tuple2 stringLongTuple2, Tuple2 t1) throws Exception {
                return Tuple2.of(stringLongTuple2.f0, stringLongTuple2.f1 + t1.f1);
            }
        });

        // 2. 选取当前最活跃的用户
        SingleOutputStreamOperator> result = reduce.keyBy(data -> "key").reduce(new ReduceFunction>() {
            @Override
            public Tuple2 reduce(Tuple2 t1, Tuple2 t2) throws Exception {
                return t1.f1 > t2.f1 ? t1 : t2;
            }
        });

        result.print();

        env.execute();
    }
}

4. 富函数类(Rich Function Classes)

“富函数类”也是 DataStream API 提供的一个函数类的接口,所有的 Flink 函数类都有其 Rich 版本。富函数类一般是以抽象类的形式出现的。例如:RichMapFunction、RichFilterFunction、 RichReduceFunction 等。 Rich Function 有生命周期的概念。典型的生命周期方法有:

open()方法,是 Rich Function 的初始化方法,也就是会开启一个算子的生命周期。当 一个算子的实际工作方法例如 map()或者 filter()方法被调用之前,open()会首先被调 用。所以像文件 IO 的创建,数据库连接的创建,配置文件的读取等等这样一次性的 工作,都适合在 open()方法中完成。。 close()方法,是生命周期中的最后一个调用的方法,类似于解构方法。一般用来做一 些清理工作。 需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;而对应的, 实际工作方法,例如 RichMapFunction 中的 map(),在每条数据到来后都会触发一次调用。

完整代码

import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class TransformRichFunctionTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(1);

        DataStreamSource stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Bob", "./prod?id=100", 3000L),
                new Event("Bob", "./home", 4000L)
        );

        stream.map(new MyRichMapper()).setParallelism(2).print();

        env.execute();
    }

    // 实现一个自定义的富函数类
    public static class MyRichMapper extends RichMapFunction {

        @Override
        public void open(Configuration parameters) throws Exception {
            super.open(parameters);
            System.out.println("open生命周期被调用 " + getRuntimeContext().getIndexOfThisSubtask()+"号任务启动");
        }

        @Override
        public Integer map(Event event) throws Exception {
            return event.url.length();
        }

        @Override
        public void close() throws Exception {
            super.close();
            System.out.println("close生命周期被调用 " + getRuntimeContext().getIndexOfThisSubtask()+"号任务结束");
        }
    }
}

一个常见的应用场景就是,如果我们希望连接到一个外部数据库进行读写操作,那么将连 接操作放在 map()中显然不是个好选择——因为每来一条数据就会重新连接一次数据库;所以 我们可以在 open()中建立连接,在 map()中读写数据,而在 close()中关闭连接。所以我们推荐 的最佳实践如下:

public class MyFlatMap extends RichFlatMapFunction> {

     @Override
     public void open(Configuration configuration) {
         // 做一些初始化工作
         // 例如建立一个和 MySQL 的连接
     }

     @Override
     public void flatMap(IN in, Collector

你可能感兴趣的:(flink)