Flink(四):Flink流处理API:DataStreamAPI

一:Flink程序骨架

1. 设置执行环境

设置一个Flink流处理的执行环境Java代码:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

执行环境是FLink程序执行的上下文,提供了与作业与集群交互的方法,getExecutionEnvironment()会根据部署环境获取相应的上下文。
批处理执行环境Java代码如下:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

执行环境可以进行很多设置:

//设置执行环境整个作业并行度为4
        env.setParallelism(4);
        //关闭算子链功能
        env.disableOperatorChaining();
        //检查点设置
        env.enableCheckpointing(3600000, CheckpointingMode.EXACTLY_ONCE);
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE);
        env.getCheckpointConfig().setCheckpointTimeout(5000);
        env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);//上一个检查点没执行完,下一个可能开始了
        env.getCheckpointConfig().setMinPauseBetweenCheckpoints(100L);//第一次检查点完成时间到第二次检查点开始时间的时间间隔
        //重启策略设置
        env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 10000L));
        env.setRestartStrategy(RestartStrategies.failureRateRestart(5, Time.hours(1), Time.seconds(20)));

2. 读取一个到多个数据源

数据源一般是消息队列或文件,我们也可以重写数据源,如爬取网络数据。后端专门一个主题讲Flink的连接器,介绍使用各种数据源。

3. 根据业务逻辑对数据进行转换

此时我们可以在数据流上进行有状态的计算了。在TransFormation转换过程中初始的DataStream可能转换为KeyedStream、WindowedStream、JoinedStream等不同结构的数据流。

4. 将结果输出到sink

计算结果输出到外部系统,可输出到Kafka等消息队列,也可输出到文件系统中,也可是其他自定义的输出。输出结果部分统称为Sink操作。在Flink链接器专题中详细介绍如何输出。

5. 调用作业执行函数

Flink流处理是延迟执行,我们要调用执行环境的excute()方法去执行作业。

二:常见的Transformation使用方法

Flink的Transformation主要有四种:单数据流基本转换、基于key的分组转换、多数据流转换、数据重分布转换。
Flink数据转换,一般是执行XX转换需要实现XXFunction接口(也可以匿名类,lamda方式)或者继承RichXXFunction抽象。

1. 单数据流基本转换

主要对单个数据流上的每个元素进行处理。

1.1 Map

图1. map

map()对DataStream中的每个元素调用用户实现的Mapper函数,每个元素对应一个输出元素(类型可能改变),数据流变成一个新的DataStream。
用户自定义Mapper函数需要实现MapFunction接口或继承RichMapFunction类,举例如下:

    class MyMapFunction implements MapFunction<String, Tuple2<String, Integer>> {
        @Override
        public Tuple2<String, Integer> map(String s) throws Exception {
            return new Tuple2<>(s, s.length());
        }
    }

    class MyRichMapFunction extends RichMapFunction<String, String> {
        @Override
        public String map(String s) throws Exception {
            return "newString_" + s;
        }
    }

MapFunction是函数式接口,也可以通过匿名类、lambda表达式的方式实现

1.2 filter

图2. filter

filter对数据流进行过滤,我们需要实现FilterFunction接口或继承RichFilterFunction类,针对每个元素,FilterFunction返回true(保留该元素)或者false(过滤掉)。我们可以将参数传递给自己的实现类,达到传递参数给filter()的目的。

public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> dataStream = env.fromElements("filnk", "world count", "!");
        SingleOutputStreamOperator<String> filterStream = dataStream.filter(new MyFilterFunction(3));
    }
    static class MyFilterFunction implements FilterFunction<String> {
        private int limit;

        public MyFilterFunction(int limit) {
            this.limit = limit;
        }

        @Override
        public boolean filter(String s) throws Exception {
            return s.length() >= limit;
        }
    }

1.3 flatMap

与map类似,但flatMap的输出可以是0个、1个、多个。map与flatmap的区别:map会根据每个元素生成一个对应的输出元素;flatmap会根据每个元素生成一个集合,再将所有集合展平成一条数据流。flatmap可以同时实现filter和map的功能。

public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> dataStream = env.fromElements("filnk", "world count", "!");
        SingleOutputStreamOperator<String> newDataStream = dataStream.flatMap(new MyFlatMapFunction(0));
    }

    static class MyFlatMapFunction implements FlatMapFunction<String, String> {
        private int limit;

        public MyFlatMapFunction(int limit) {
            this.limit = limit;
        }

        @Override
        public void flatMap(String s, Collector<String> collector) throws Exception {
            if (s.length() <= limit) {
                return;
            }
            for (String world : s.split(" ")) {
                collector.collect(world);
            }
        }
    }

2. 基于key的分组转换

对数据分组主要是为了进行后续的聚合操作,对在同一组的数据进行分析。keyBy()会将一个DataStream转换为 一个KeyedStream,聚合操作会将KeyedStream转换为DataStream。聚合前后数据类型不变。
图3. keyBy数据类型转换

2.1 keyBy

图4. keyBy

多数情况下,我们需要根据数据的某个字段进行分组,然后对每个分组进行数据处理。我们需要想keyBy()传递一个参数,以告诉程序以什么作为key来进行分组,同一组数据会被分配到同一算子子任务中。指定key的方式主要有三种:使用数字位置来指定key,使用字段名来指定key,实现KeySelector;
分组后,我们可以对每组数据进行时间窗口的处理,状态的维护,key相同的数据可以访问和修改同一个状态。
推荐使用KeySelector的方式指定key。

public static void main(String[] args) {
        try {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            DataStreamSource<User> userDataStreamSource = env.fromElements(new User("luo", 1), new User("li", 2), new User("zhao", 3));
            //自定义selector
            KeyedStream<User, String> userStringKeyedStream = userDataStreamSource.keyBy(new MyKeySelector());
            //字段名
            userDataStreamSource.keyBy("id");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class MyKeySelector implements KeySelector<User, String> {
        @Override
        public String getKey(User user) throws Exception {
            return user.getName() + user.getId();
        }
    }

2.2 聚合函数sum(),max(),maxBy(),min(),minBy()

聚合函数也需要一个参数来指定按照哪个字段来进行聚合,我们可以使用数字位置、KeySelector来指定用哪个字段来进行聚合。
sum()对该字段进行加和,结果保存在该字段中,该函数无法确定其他字段的值。
max()对该字段求最大值,并将结果保存在该字段中,其他字段不能保证计算结果。maxBy()与max()的区别在于maxBy()可以保留其他字段的数值,既返回数据流中最大的整个元素,包括其他字段。min()和minBy()同max()相似。
这些聚合函数已经使用了状态数据,如sum()会记录当前的和,max()记录当前最大值,聚合函数的计算过程就是不断更新状态的过程。由于内部使用了状态数据,状态数据并不会清楚,因此对无线数据流使用聚合函数时要慎重。对于一个keyedStream,一次只能使用一个聚合函数,无法链式使用多个。

import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import scala.Tuple3;

public class MySumTest {
    public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Tuple3<Integer, Integer, Integer>> tuple3DataStreamSource = env.fromElements(new Tuple3<>(1, 1, 2), new Tuple3<>(1, 10, 18), new Tuple3<>(3, 100, 20));
        //对第二个字段求和,第三个字段不确定
        SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> sum = tuple3DataStreamSource.keyBy(0).sum(1);
        //对第三个字段求最大值,第二个字段的值不确定,(1,?,18),(3,100,20)
        SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> max = tuple3DataStreamSource.keyBy(0).max(2);
        //对第三个字段求最大值,第二个字段的值也一起保留,(1,10,18),(3,100,20)
        SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> maxBy = tuple3DataStreamSource.keyBy(0).maxBy(2);
        //使用reduce实现sumBy(1)
        SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> reduce = tuple3DataStreamSource.keyBy(0).reduce(new MyReduce());
    }

    static class MyReduce implements ReduceFunction<Tuple3<Integer, Integer, Integer>> {
        @Override
        public Tuple3 reduce(Tuple3 t1, Tuple3 t2) throws Exception {
            return new Tuple3(t1._1(), (int) t1._2() + (int) t2._2(), t1._3());
        }
    }
}

2.3 reduce

reduce在KeyedStream上生效,他接受两个输入元素,生成一个输出元素,及两两合一地进行汇总操作,生成一个类型不变的新元素。

3. 多数据流转换

对多个数据流进行转换。

3.1 union

图5. union

可以合并多个相同类型的数据流,也就是将多个DataStream合并成一个DataStream。

3.2 connect

图6. connect

connect只能链接两个数据流,两个数据流的元素类型可以不一致,两个数据流经过connect之后转化为ConnectedStreams,ConnectedStreams会对两个流的数据应用不同的处理方法,两个流之间可以共享状态。
connect经常用于使用一个控制流对另一个数据流进行控制的场景。对于ConnectedStreams进行数据处理,我们需要重写CoMapFunction或CoFlatMapFunction,这两个接口提供了三个泛型,分为对应第一个流的输入类型,第二个流的输入类型,和输出数据类型。重写函数时,对于CoMapFunction,map1()处理第一个流的数据,map2()处理第二个流的数据,CoFlatMapFunction类似。
两个数据流经过connect之后,可以继续使用FlatMapFunction或ProcessFunction继续处理,做到类似SQL中的join效果,Flink也提供了join,主要作用在时间窗口上,connect更广义一点。

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;

public class MyManySourceTest {
    public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> source1 = env.fromElements("1");
        DataStream<String> source2 = env.fromElements("2");
        DataStream<String> source3 = env.fromElements("3");
        //union
        source1.union(source2, source3);
        //connect
        DataStream<Integer> source4 = env.fromElements(1);
        source1.connect(source4).map(new MyCoMapFunction()).;

    }

    static class MyCoMapFunction implements CoMapFunction<String, Integer, String> {
        @Override
        public String map1(String s) throws Exception {
            return "new_" + s;
        }

        @Override
        public String map2(Integer integer) throws Exception {
            return "new_int_" + integer;
        }
    }
}

4. 数据重分布转换

Flink作业的并行度可以在执行环境层面进行同一设置,这样影响所有算子,也可以针对某个算子单独设置。默认情况一个作业的所有算子的并行度依赖执行环境,如在本地,则并行度为cpu核心数。
默认情况下,数据自动分配到多个子任务上,有时候我们需要手动在多个子任务上进行数据分配。

4.1 shuffle

基于正态分布,将数据随机分布到下游各算子子任务上。

4.2 rebalance与rescale

rebalance使用Round-Ribon方法将数据均匀分布到各子任务上。上游数据轮训的方式均匀分布到下游所有的子任务上。rescale也是均匀地分布到下游各子任务,但不是轮训的发送数据,而是就近发送给下游子任务。
rebalance轮训式地将数据分配到下游:
图6. rebalance
rescale,上游两个任务,下游四个任务:
图7. rescale2to4
rescale,上游四个子任务,下游2个子任务:
图8. rescale4to2

4.3 broadcast

数据复制,广播发送给下游的所有子任务。

4.4 global

将所有数据发送给下游算子的第一个子任务上。

4.5 partitionCustom

自定义数据重分布逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值