Flink DataStream API 总结

流程如下 Environment(执行环境) -> Source(数据源) -> Transformation(转换操作) -> Sink(输出)

1.Environment(执行环境)

package cn.coreqi.env;

import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.RestOptions;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class EnvDemo {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();   //conf对象可以修改一些配置参数
        conf.set(RestOptions.BIND_PORT,"8082");     // 默认端口8081
        StreamExecutionEnvironment env = StreamExecutionEnvironment
                //.getExecutionEnvironment(); // 自动识别 是远程集群 还是本地环境
                //.createLocalEnvironment(); // 本地环境
                //.createRemoteEnvironment("192.168.58.130",8081,"/home/FlinkTutorial-1.0-SNAPSHOT.jar"); // 远程集群环境
                .getExecutionEnvironment(conf);

        // 废除了DataSet API - ExecutionEnvironment 同一套代码通过配置来决定是以批还是流的方式处理,默认是以流的方式处理
        // 可以在提交时通过参数 -Dexecution.runtime-mode=BATCH | STREAMING 来定义是以批还是流的方式处理
        env.setRuntimeMode(RuntimeExecutionMode.BATCH); //批的方式处理

        JobExecutionResult execute = env.execute(); //触发程序执行

        // 1.默认 env.execute() 触发一个flink job,一个main方法中可以调用多个execute(),但是没意义,在第一个execute()会阻塞程序的运行
        // 2.env.executeAsync(),异步触发,不阻塞,每个 executeAsync()都会生成一个flink job
        //env.executeAsync();
    }
}

2.Source(数据源)

Flink 从各种来源获取数据,然后构建DataStream进行转换处理。

1.分类

1.Version < Flink1.12

在 Flink1.12 以前,旧的添加 source 的方式,是调用执行环境的 addSource()方法:

DataStream<String> stream = env.addSource(...);

方法传入的参数是一个“源函数”(source function),需要实现 SourceFunction 接口。

2.Version >= Flink1.12

从 Flink1.12 开始,主要使用流批统一的新 Source 架构:

DataStreamSource<String> stream = env.fromSource(…);

Flink 直接提供了很多预实现的接口,此外还有很多外部连接工具也帮我们实现了对应的Source,通常情况下足以应对我们的实际需求。

2.实操

1.从集合中读取数据

最简单的读取数据的方式,就是在代码中直接创建一个 Java 集合,然后调用执行环境的fromCollection 方法进行读取。这相当于将数据临时存储到内存中,形成特殊的数据结构后,作为数据源使用,一般用于测试。

        List<Integer> data = Arrays.asList(1, 3, 5, 7, 9);
        DataStreamSource<Integer> ds = env.fromCollection(data);
        ds.print();

        DataStreamSource<Integer> ds = env.fromElements(1,3,5,7,9);
        ds.print();

2.从文件中读取

从存储介质中获取数据,一个比较常见的方式就是读取日志文件。这也是批处理中最常见的读取方式。

1.旧的添加 source 的方式 - addSource()
        // 读取数据
        DataSource<String> lineDS = env.readTextFile("input/word.txt");
2.新的添加方式
1.POM添加依赖
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-files</artifactId>
            <version>1.18.0</version>
        </dependency>
2.代码
        FileSource<String> fileSource = FileSource.forRecordStreamFormat(
                new TextLineInputFormat(), 
                new Path("input/word.txt"))
                .build();

        DataStreamSource<String> source = env.fromSource(fileSource, WatermarkStrategy.noWatermarks(), "fileSource");
        
        source.print();

3.从Socket中读取

集合还是文件,读取的其实都是有界数据。在流处理的场景中,数据往往是
无界的。
读取 socket 文本流,就是流处理场景。但是这种方式由于吞吐量小、稳定性较差,一般也是用于测试。

        // 读取socket数据
        DataStreamSource<String> socketDS = env.socketTextStream("localhost", 7878);

4.从 Kafka 读取数据

Flink 官方提供了连接工具 flink-connector-kafka,直接帮我们实现了一个消费者 FlinkKafkaConsumer,它就是用来读取 Kafka 数据的 SourceFunction。
想要以 Kafka 作为数据源获取数据,只需要引入 Kafka 连接器的依赖。Flink 官方提供的是一个通用的 Kafka 连接器,它会自动跟踪最新版本的 Kafka 客户端。目前最新版本只支持 0.10.0 版本以上的 Kafka。

1.POM添加依赖
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>3.0.2-1.18</version>
        </dependency>
2.代码
        KafkaSource<String> kafkaSource =
                KafkaSource.<String>builder()
                        .setBootstrapServers("192.168.58.130:9092,192.168.58.131:9092,192.168.58.132:9092") //指定kafka节点的地址和端口
                        .setGroupId("coreqi")   //指定消费者组的ID
                        .setTopics("topic_1")   // 指定消费者的Topic
                        .setValueOnlyDeserializer(new SimpleStringSchema()) //指定value的反序列化器

                        //kafka消费者的参数
                            // auto.reset.offsets
                                // earliest: 如果有offset,从offset继续消费;如果没有offset,从最早消费
                                // latest: 如果有offset,从offset继续消费;如果没有offset, 从最新消费
                        // flink的kafkasource,offset消费策略;offsetInitalize,默认是earliest
                                //ealiest: 一定从 最早 消费
                                // latest: 一定从 最新 消费
                        .setStartingOffsets(OffsetsInitializer.latest())    //offset初始化器,flink消费kafka的策略
                        .build();

        DataStreamSource<String> stream = env.fromSource(kafkaSource,
                WatermarkStrategy.noWatermarks(), "kafkaSource");

        stream.print("Kafka");

5. 从数据生成器读取数据

Flink 从 1.11 开始提供了一个内置的 DataGen 连接器,主要是用于生成一些随机数,用于在没有数据源的时候,进行流任务的测试以及性能测试等。1.17 提供了新的 Source 写法

1.POM添加依赖
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-datagen</artifactId>
            <version>1.18.0</version>
        </dependency>
2.代码
        // 如果有n个并行度,最大值设为a
        // 将数值 均分为 n份,a / n,比如,最大值100,并行度2,每个并行度生成50个
        // 其中一个是 0 - 49,另一个 50 - 99
        env.setParallelism(1);

        /**
         * 数据生成器source,四个参数
         *  第一个:GeneratorFunction接口,需要实现,重写map方法,输入类型固定是Long
         *  第二个:Long类型,自动生成的数字序列(从0自增)的最大值(小于),达到这个值就停止了
         *  第三个:限速策略,比如 每秒生成几条数据
         *  第四个:返回的类型
         */
        DataGeneratorSource<String> dataGeneratorSource =
                new DataGeneratorSource<>(
                        new GeneratorFunction<Long, String>() {
                            @Override
                            public String map(Long value) throws Exception {
                                return "Number:" + value;
                            }
                        },
                        Long.MAX_VALUE, //生成数据的最大值
                        RateLimiterStrategy.perSecond(10),  // 每秒钟生成多少条数据
                        Types.STRING    // 返回的数据类型
                );
        DataStreamSource<String> source = env.fromSource(
                dataGeneratorSource,
                WatermarkStrategy.noWatermarks(),
                "datagenerator"
        );

        source.print();

6. Flink支持的数据类型

Flink 使用“类型信息”(TypeInformation)来统一表示数据类型。TypeInformation 类是Flink 中所有类型描述符的基类。它涵盖了类型的一些基本属性,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

1.类型提示(Type Hints)

Flink 还具有一个类型提取系统,可以分析函数的输入和返回类型,自动获取类型信息,从而获得对应的序列化器和反序列化器。但是,由于 Java 中泛型擦除的存在,在某些特殊情况下(比如 Lambda 表达式中),自动提取的信息是不够精细的——只告诉 Flink 当前的元素由“船头、船身、船尾”构成,根本无法重建出“大船”的模样;这时就需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。
为了解决这类问题,Java API 提供了专门的“类型提示”(type hints)。
传入 Lambda 表达式,系统只能推断出返回的是 Tuple2 类型,而无法得到 Tuple2<String, Long>。只有显式地告诉系统当前的返回类型,才能正确地解析出完整数据。

                .returns(Types.TUPLE(Types.STRING, Types.INT));

Flink 还专门提供了 TypeHint 类,它可以捕获泛型的类型信息,并且一直记录下来,为运行时提供足够的信息。我们同样可以通过.returns()方法,明确地指定转换之后的 DataStream里元素的类型。

                .returns(new TypeHint<Tuple2<String, Integer>>(){});

3.Transformation(转换操作)

数据源读入数据之后,可以使用各种转换算子,将一个或多个 DataStream 转换为新的 DataStream。

1.基本转换算子

1.映射(map)

用于将数据流中的数据进行转换,形成新的数据流。简单来说,就是一个“一一映射”,一进一出,读取一条处理后输出一条数据。
我们只需要基于 DataStream 调用 map()方法就可以进行转换处理。方法需要传入的参数是接口 MapFunction 的实现;返回值类型还是 DataStream,不过泛型(流中的元素类型)可能改变。

        DataStreamSource<Integer> dataDS = env.fromElements(1, 3, 5, 7, 9);

        // 方式一 匿名实现类
/*        SingleOutputStreamOperator<String> map = dataDS.map(new MapFunction<Integer, String>() {
            @Override
            public String map(Integer integer) throws Exception {
                return String.valueOf(integer + 1);
            }
        });*/

        //方式二 lamada
        SingleOutputStreamOperator<String> map = dataDS.map(item -> String.valueOf(item + 1)).returns(Types.STRING);

        //方式三 定义一个类实现 MapFunction[略]
        
        map.print();

2.过滤(filter)

对数据流执行一个过滤,通过一个布尔条件表达式设置过滤条件,对于每一个流内元素进行判断,若为 true 则元素正常输出,若为 false 则元素被过滤掉。进行 filter 转换之后的新数据流的数据类型与原数据流是相同的。

        DataStreamSource<Integer> dataDS = env.fromElements(1, 2, 3, 5, 6, 7, 9);

        // 方式一 匿名实现类
/*        SingleOutputStreamOperator<Integer> filterDS = dataDS.filter(new FilterFunction<Integer>() {
            @Override
            public boolean filter(Integer integer) throws Exception {
                return integer % 2 == 0;
            }
        });*/

        //方式二 lamada
        SingleOutputStreamOperator<Integer> filterDS = dataDS.filter(item -> item % 2 == 0);

        // 方式三 定义一个类实现 MapFunction[略]

        filterDS.print();

3.扁平操作(flatMap)

flatMap 操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个一个的个体使用。一进多出,消费一个元素,可以产生 0 到多个元素。flatMap 可以认为是“扁平化”(flatten)和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,再对拆分后的元素做转换处理。

       DataStreamSource<String> sourceDS = env.fromElements("Hello fanqi", "Hello flink");

        // 方式一 匿名实现类
//        SingleOutputStreamOperator<Tuple2<String, Integer>> flatMap = sourceDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
//            @Override
//            public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
//                // 按照空格切分单词
//                String[] words = s.split(" ");
//                for (String word : words) {
//                    Tuple2<String, Integer> wordTuple2 = Tuple2.of(word, 1);
//                    // 使用 Collector 向下游发送数据
//                    collector.collect(wordTuple2);
//                }
//            }
//        });

        //方式二 lamada
        SingleOutputStreamOperator<Tuple2<String, Integer>> flatMap = sourceDS.flatMap((String s, Collector<Tuple2<String, Integer>> collector) -> {
            // 按照空格切分单词
            String[] words = s.split(" ");
            for (String word : words) {
                Tuple2<String, Integer> wordTuple2 = Tuple2.of(word, 1);
                // 使用 Collector 向下游发送数据
                collector.collect(wordTuple2);
            }
        }).returns(Types.TUPLE(Types.STRING, Types.INT));

        // 方式三 定义一个类实现 MapFunction[略]
        
        flatMap.print();

2.聚合算子(Aggregation - keyBy | sum/min/max/minBy/maxBy | reduce)

计算的结果不仅依赖当前数据,还跟之前的数据有关,相当于要把所有数据聚在一起进行汇总合并——这就是所谓的“聚合”(Aggregation),类似于 MapReduce 中的 reduce 操作。

1.按键分区(keyBy)

在Flink中,DataStream 是没有直接进行聚合的 API 的,因此我们对海量数据做聚合肯定要先进行分区并行处理,这样才能提高效率。所以在 Flink中,要先进行分区,再做聚合;这个操作就是通过 keyBy 来完成的。

keyBy 是聚合前必须要用到的一个算子。keyBy通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务。

基于不同的 key,流中的数据将被分配到不同的分区中去;这样一来,所有具有相同的key 的数据,都将被发往同一个分区。

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

keyBy()方法需要传入一个参数,这个参数指定了一个或一组 key。有很多不同的方法来指定 key:比如对于 Tuple 数据类型,可以指定字段的位置或者多个位置的组合;对于 POJO类型,可以指定字段的名称(String);另外,还可以传入Lambda表达式或者实现一个键选择器(KeySelector),用于说明从数据中提取 key 的逻辑。

需要注意的是,keyBy 得到的结果将不再是 DataStream,而是会将 DataStream 转换为KeyedStream。KeyedStream 可以认为是“分区流”或者“键控流”,它是对 DataStream 按照key 的一个逻辑分区,所以泛型有两个类型:除去当前流中的元素类型外,还需要指定 key 的类型。

KeyedStream 也继承自 DataStream,所以基于它的操作也都归属于 DataStream API。但它跟之前的转换操作得到的SingleOutputStreamOperator 不同,只是一个流的分区操作,并不是一个转换算子。KeyedStream 是一个非常重要的数据结构,只有基于它才可以做后续的聚合操作(比如 sum,reduce)。

        DataStreamSource<Integer> dataDS = env.fromElements(1, 3, 5, 7, 7, 7, 8, 9);

        /**
         * 使用匿名实现类的方式 按照key分组
         * 要点:
         *  1.返回的是一个KeyedStream键控流
         *  2.keyBy不是转换算子,只是对数据进行重分区,不能设置并行度
         *  3.keyBy分组与分区的关系:
         *      1.keyBy 是对数据分组,保证相同 key的数据再同一个分区
         *      2.分区:一个子任务,可以理解为一个分区,一个分区(子任务)中可以存在多个分组(key)
         */
        KeyedStream<Integer, String> sensorKS = dataDS.keyBy(new KeySelector<Integer, String>() {
            @Override
            public String getKey(Integer integer) throws Exception {
                return integer.toString();
            }
        });

        //方式二 lamada
        //KeyedStream<Integer, String> sensorKS = dataDS.keyBy(item -> item.toString());

        sensorKS.print();

2.简单聚合(sum/min/max/minBy/maxBy)

有了按键分区的数据流 KeyedStream,我们就可以基于它进行聚合操作了。Flink 为我们内置实现了一些最基本、最简单的聚合 API,主要有以下几种:

  • sum():在输入流上,对指定的字段做叠加求和的操作。
  • min():在输入流上,对指定的字段求最小值。
  • max():在输入流上,对指定的字段求最大值。
  • minBy():与 min()类似,在输入流上针对指定字段求最小值。不同的是,min()只计算指定字段的最小值,其他字段会保留最初第一个数据的值;而 minBy()则会返回包含字段最小值的整条数据。
  • maxBy():与 max()类似,在输入流上针对指定字段求最大值。两者区别与min()/minBy()完全一致。

简单聚合算子使用非常方便,语义也非常明确。这些聚合方法调用时,也需要传入参数;但并不像基本转换算子那样需要实现自定义函数,只要说明聚合指定的字段就可以了。指定字段的方式有两种:指定位置,和指定名称。

对于元组类型的数据,可以使用这两种方式来指定字段。需要注意的是,元组中字段的名称,是以 f0、f1、f2、…来命名的。

如果数据流的类型是 POJO 类,那么就只能通过字段名称来指定,不能通过位置来指定了。

简单聚合算子返回的,同样是一个 SingleOutputStreamOperator,也就是从 KeyedStream又转换成了常规的 DataStream。所以可以这样理解:keyBy 和聚合是成对出现的,先分区、后聚合,得到的依然是一个 DataStream。而且经过简单聚合之后的数据流,元素的数据类型保持不变。

一个聚合算子,会为每一个 key 保存一个聚合的值,在 Flink 中我们把它叫作“状态”(state)。所以每当有一个新的数据输入,算子就会更新保存的聚合结果,并发送一个带有更新后聚合值的事件到下游算子。对于无界流来说,这些状态是永远不会被清除的,所以我们使用聚合算子,应该只用在含有有限个 key 的数据流上。

        DataStreamSource<Tuple2<Integer, Integer>> dataDS = env.fromElements(Tuple2.of(1, 22), Tuple2.of(1, 24), Tuple2.of(2, 0), Tuple2.of(2, 7));
        KeyedStream<Tuple2<Integer, Integer>, Integer> sensorKS = dataDS.keyBy(new KeySelector<Tuple2<Integer, Integer>, Integer>() {
            @Override
            public Integer getKey(Tuple2<Integer, Integer> integerIntegerTuple2) throws Exception {
                return integerIntegerTuple2.f0;
            }
        });

        /**
         * 简单聚合算子在keyBy之后才能调用,分组内的聚合,对同一个key的数据进行聚合
         */
        sensorKS.sum(1).print();

3. 归约聚合(reduce)

reduce 可以对已有的数据进行归约处理,把每一个新输入的数据和当前已经归约出来的值,再做一个聚合计算。

reduce操作也会将 KeyedStream转换为 DataStream。它不会改变流的元素数据类型,所以输出类型和输入类型是一样的。

调用 KeyedStream 的 reduce 方法时,需要传入一个参数,实现 ReduceFunction 接口。ReduceFunction 接口里需要实现 reduce()方法,这个方法接收两个输入事件,经过转换处理之后输出一个相同类型的事件。在流处理的底层实现过程中,实际上是将中间“合并的结果”作为任务的一个状态保存起来的;之后每来一个新的数据,就和之前的聚合状态进一步做归约。

reduce 同简单聚合算子一样,也要针对每一个 key 保存状态。因为状态不会清空,所以
我们需要将 reduce 算子作用在一个有限 key 的流上。

        DataStreamSource<Tuple2<Integer, Integer>> dataDS = env.fromElements(Tuple2.of(1, 22), Tuple2.of(1, 24), Tuple2.of(2, 0), Tuple2.of(2, 7));
        KeyedStream<Tuple2<Integer, Integer>, Integer> sensorKS = dataDS.keyBy(new KeySelector<Tuple2<Integer, Integer>, Integer>() {
            @Override
            public Integer getKey(Tuple2<Integer, Integer> integerIntegerTuple2) throws Exception {
                return integerIntegerTuple2.f0;
            }
        });

        /**
         * keyBy之后调用
         * 输入类型 = 输出类型,类型不能变
         * 每个key的第一条数据来的时候,不会执行reduce方法,存起来,直接输出
         * reduce方法中的两个参数
         *  value1:之前的计算结果,存状态
         *  value2: 现在来的数据
         */
        SingleOutputStreamOperator<Tuple2<Integer, Integer>> reduce = sensorKS.reduce(new ReduceFunction<Tuple2<Integer, Integer>>() {
            @Override
            public Tuple2<Integer, Integer> reduce(Tuple2<Integer, Integer> value1, Tuple2<Integer, Integer> value2) throws Exception {
                return Tuple2.of(value1.f0, value1.f1 + value2.f1);
            }
        });

        reduce.print();

3.用户自定义函数(UDF)

用户自定义函数(user-defined function,UDF),即用户可以根据自身需求,重新实现算子的逻辑。
用户自定义函数分为:函数类、匿名函数、富函数类。

1.函数类(Function Classes)

Flink 暴露了所有 UDF 函数的接口,具体实现方式为接口或者抽象类,例如 MapFunction、FilterFunction、ReduceFunction 等。所以用户可以自定义一个函数类,实现对应的接口。
上面的代码中演示的三种方式是函数类的实现方式,此处不再赘述

2.富函数类(Rich Function Classes)

“富函数类”也是 DataStream API 提供的一个函数类的接口,所有的 Flink 函数类都有其Rich 版 本 。 富 函 数 类 一 般 是 以 抽 象 类 的 形 式 出 现 的 。 例 如 :RichMapFunction、RichFilterFunction、RichReduceFunction 等。

与常规函数类的不同主要在于,富函数类可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

Rich Function 有生命周期的概念。典型的生命周期方法有:

  • open()方法,是 Rich Function 的初始化方法,也就是会开启一个算子的生命周期。当一个算子的实际工作方法例如 map()或者 filter()方法被调用之前,open()会首先被调用。
  • close()方法,是生命周期中的最后一个调用的方法,类似于结束方法。一般用来做一些清理工作。

需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;而对应的,实际工作方法,例如 RichMapFunction 中的 map(),在每条数据到来后都会触发一次调用。

    DataStreamSource<Tuple2<Integer, Integer>> dataDS = env.fromElements(Tuple2.of(1, 22), Tuple2.of(1, 24), Tuple2.of(2, 0), Tuple2.of(2, 7));

        /**
         * RichXXXfunction: 复函数
         * 1.多了生命周期管理方法
         *  open(): 每个子任务,在启动时,调用一次
         *  close(): 每个子任务,在结束时,调用一次
         *   => 如果fink程序异常退出,不会调用close()
         *
         * 2.多了一个运行时上下文
         *  可以获取一些运行时的环境信息,比如:子任务编号、名称、其他信息等等.....
         */
        SingleOutputStreamOperator<String> map = dataDS.map(new RichMapFunction<Tuple2<Integer, Integer>, String>() {
            @Override
            public void open(Configuration parameters) throws Exception {
                super.open(parameters);
                RuntimeContext runtimeContext = getRuntimeContext();
                int indexOfThisSubtask = runtimeContext.getIndexOfThisSubtask();
                String taskNameWithSubtasks = runtimeContext.getTaskNameWithSubtasks();
            }

            @Override
            public void close() throws Exception {
                super.close();
            }

            @Override
            public String map(Tuple2<Integer, Integer> integerIntegerTuple2) throws Exception {
                return String.valueOf(integerIntegerTuple2.f1 + 1);
            }
        });

        map.print();

4.物理分区算子(Physical Partitioning)

常见的物理分区策略有:随机分配(Random)、轮询分配(Round-Robin)、重缩放(Rescale)和广播(Broadcast)。

1.随机分区(shuffle)

最简单的重分区方式就是直接“洗牌”。通过调用 DataStream 的.shuffle()方法,将数据随机地分配到下游算子的并行任务中去。

随机分区服从均匀分布(uniform distribution),所以可以把流中的数据随机打乱,均匀地传递到下游任务分区。因为是完全随机的,所以对于同样的输入数据, 每次执行得到的结果也不会相同。

经过随机分区之后,得到的依然是一个 DataStream。

        env.setParallelism(2);

        DataStreamSource<String> source = env.socketTextStream("localhost", 7878);

        // 随机分区:random.nextInt(下游算子并行度)
        DataStream<String> shuffle = source.shuffle();

        shuffle.print();

2.轮询分区(Round-Robin)

轮询,简单来说就是“发牌”,按照先后顺序将数据做依次分发。通过调用 DataStream的.rebalance()方法,就可以实现轮询重分区。rebalance使用的是 Round-Robin负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去。

        env.setParallelism(2);

        DataStreamSource<String> source = env.socketTextStream("localhost", 7878);

        // 轮询:nextChannelToSendTo = (nextChannelToSendTo + 1) % 下游算子并行度;
        // 如果是 数据倾斜的场景.source读进来之后,调用 rebalance, 就可以解决数据源的数据倾斜
        DataStream<String> rebalance = source.rebalance();

        rebalance.print();

3.重缩放分区(rescale)

重缩放分区和轮询分区非常相似。当调用 rescale()方法时,其实底层也是使用 Round-Robin算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中。rescale的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌。

        env.setParallelism(2);

        DataStreamSource<String> source = env.socketTextStream("localhost", 7878);
        
        // rescale缩放:实现轮询,局部组队,比rebalance更高效
        DataStream<String> rescale = source.rescale();

        rescale.print();

4.广播(broadcast)

这种方式其实不应该叫做“重分区”,因为经过广播之后,数据会在不同的分区都保留一份,可能进行重复处理。可以通过调用 DataStream 的 broadcast()方法,将输入数据复制并发送到下游算子的所有并行任务中去。

        env.setParallelism(2);

        DataStreamSource<String> source = env.socketTextStream("localhost", 7878);

        // broadcast广播:将每条数据都会重复发送到所有的子任务中
        DataStream<String> broadcast = source.broadcast();

        broadcast.print();

5.全局分区(global)

全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用.global()方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。

        env.setParallelism(2);

        DataStreamSource<String> source = env.socketTextStream("localhost", 7878);

        // global全局:全部数据只会发往第一个子任务
        DataStream<String> global = source.global();

        global.print();

6.Key分区(keyBy)

按指定Key去发送,相同key发往同一个子任务。

7.一对一分区(Forward)

one-to-one:Forward分区器

8.自定义分区(Custom)

当 Flink 提供的所有分区策略都不能满足用户的需求时,我们可以通过使用partitionCustom()方法来自定义分区策略。

1.自定义分区器
package cn.coreqi.partitioner;

import org.apache.flink.api.common.functions.Partitioner;

public class MyPartitioner implements Partitioner<String> {

    /**
     *
     * @param key 数据中提取的key
     * @param numPartitions 下游算子的并行度
     * @return
     */
    @Override
    public int partition(String key, int numPartitions) {
        return Integer.parseInt(key) % numPartitions;
    }
}

2.使用自定义分区器
        env.setParallelism(2);

        DataStreamSource<String> socketDS = env.socketTextStream("localhost", 7878);

        DataStream<String> stringDataStream = socketDS.partitionCustom(new MyPartitioner(), i -> i);

        stringDataStream.print();

5.分流

所谓“分流”,就是将一条数据流拆分成完全独立的两条、甚至多条流。也就是基于一个DataStream,定义一些筛选条件,将符合条件的数据拣选出来放到对应的流里。
可以使用filter来实现简单的分流效果,但是不推荐使用,因为相同的数据,每调用一次filter就会被处理一遍,底层中是将数据复制了多份,性能低效。

1.侧输出流分流

只需要调用上下文 ctx 的.output()方法,就可以输出任意类型的数据了。而侧输出流的标记和提取,都离不开一个“输出标签”(OutputTag),指定了侧输出流的 id 和类型。

        /**
         * 创建OutputTag对象
         * 第一个参数: 标签名
         * 第二个参数:放入侧输出流中的数据的类型。 TypeInformation
         */
        OutputTag<Tuple2<Integer, Integer>> s1Tag = new OutputTag<>("s1",Types.TUPLE(Types.INT,Types.INT));
        OutputTag<Tuple2<Integer, Integer>> s2Tag = new OutputTag<>("s2",Types.TUPLE(Types.INT,Types.INT));

        DataStreamSource<Tuple2<Integer, Integer>> dataDS = env.fromElements(Tuple2.of(1, 22), Tuple2.of(1, 24), Tuple2.of(2, 0), Tuple2.of(2, 7));
        /**
         * 使用测输出流实现分流
         */
        SingleOutputStreamOperator<Tuple2<Integer, Integer>> process = dataDS.process(new ProcessFunction<Tuple2<Integer, Integer>, Tuple2<Integer, Integer>>() {
            @Override
            public void processElement(Tuple2<Integer, Integer> value, ProcessFunction<Tuple2<Integer, Integer>, Tuple2<Integer, Integer>>.Context ctx, Collector<Tuple2<Integer, Integer>> out) throws Exception {
                if(value.f0 == 1){
                    //如果是1的数据,放到侧输出流s1中

                    /**
                     * 上下文调用output,将数据放入侧输出流
                     * 第一个参数:Tag对象
                     * 第二个参数:放入到侧输出流中的数据
                     */
                    ctx.output(s1Tag,value);
                } else if(value.f0 == 2){
                    //如果是2的数据,放到侧输出流s2中
                    ctx.output(s2Tag,value);
                } else{
                    // 非 1和2的数据,放到主流中
                    out.collect(value);
                }
            }
        });

        // 打印主流的数据
        process.print("main");

        // 打印侧输出流
        process.getSideOutput(s1Tag).print("s1");
        process.getSideOutput(s2Tag).print("s2");

6.合流

在实际应用中,我们经常会遇到来源不同的多条流,需要将它们的数据进行联合处理。所以 Flink 中合流的操作会更加普遍,对应的 API 也更加丰富。

1.联合(Union)

最简单的合流操作,就是直接将多条流合在一起,叫作流的“联合”(union)。联合操作要求必须流中的数据类型必须相同,合并之后的新流会包括所有流中的元素,数据类型不变。
代码中,只要基于 DataStream 直接调用.union()方法,传入其他 DataStream 作为参数,就可以实现流的联合了,得到的依然是一个 DataStream:union()的参数可以是多个 DataStream,所以联合操作可以实现多条流的合并。

        DataStreamSource<Integer> source1 = env.fromElements(1, 2, 3);
        DataStreamSource<Integer> source2 = env.fromElements(11, 22, 33);
        DataStreamSource<String> source3 = env.fromElements("111", "222", "333");

        // 合并数据流,流的数据类型必须一致,一次可以合并多条流
        // DataStream<Integer> union = source1.union(source2).union(source3.map(i -> Integer.valueOf(i)));
        DataStream<Integer> union = source1.union(source2, source3.map(i -> Integer.valueOf(i)));
        union.print();

2. 连接(Connect)

流的联合虽然简单,不过受限于数据类型不能改变,灵活性大打折扣,所以实际应用较少出现。除了联合(union),Flink 还提供了另外一种方便的合流操作——连接(connect)。

1.连接流(ConnectedStreams)

为了处理更加灵活,连接操作允许流的数据类型不同。但我们知道一个DataStream中的数据只能有唯一的类型,所以连接得到的并不是DataStream,而是一个“连接流”。连接流可以看成是两条流形式上的“统 一”,被放在了一个同一个流中;事实上内部仍保持各自的数据形式不变,彼此之间是相互独立的。要想得到新的DataStream,还需要进一步定义一个“同处理”(co-process)转换操作,用来说明对于不同来源、不同类型的数据,怎样分别进行处理转换、得到统一的输出类型。所以整体上来,两条流的连接就像是“一国两制”,两条流可以保持各自的数据类型、处理方式也可以不同,不过最终还是会统一到同一个DataStream中。

需要分为两步:首先基于一条 DataStream 调用.connect()方法,传入另外一条
DataStream作为参数,将两条流连接起来,得到一个ConnectedStreams;然后再调用同处理方法得到 DataStream。这里可以的调用的同处理方法有.map()/.flatMap(),以及.process()方法。

ConnectedStreams 有两个类型参数,分别表示内部包含的两条流各自的
数据类型;由于需要“一国两制”,因此调用.map()方法时传入的不再是一个简单的MapFunction,而是一个 CoMapFunction,表示分别对两条流中的数据执行 map 操作。这个接口有三个类型参数,依次表示第一条流、第二条流,以及合并后的流中的数据类型。需要实现的方法也非常直白:.map1()就是对第一条流中数据的 map 操作,.map2()则是针对第二条流。

        DataStreamSource<Integer> source1 = env.fromElements(1, 2, 3);
        DataStreamSource<String> source2 = env.fromElements("111", "222", "333");


        /**
         * 使用connect合流
         * 一次只能连接2条流
         * 流的数据类型可以不一样
         * 连接后可以调用map、flatmap、process来处理,但是每个流单独处理每个流的数据
         */
        ConnectedStreams<Integer, String> connect = source1.connect(source2);

        SingleOutputStreamOperator<String> map = connect.map(new CoMapFunction<Integer, String, String>() {
            @Override
            public String map1(Integer value) throws Exception {
                return value != null ? value.toString() : "";
            }

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

        map.print();
2.CoProcessFunction

与 CoMapFunction 类似,如果是调用.map()就需要传入一个 CoMapFunction,需要实现map1()、map2()两个方法;而调用.process()时,传入的则是一个 CoProcessFunction。它也是“处理函数”家族中的一员,用法非常相似。它需要实现的就是 processElement1()、processElement2()两个方法,在每个数据到来时,会根据来源的流调用其中的一个方法进行处理。

值得一提的是,ConnectedStreams 也可以直接调用.keyBy()进行按键分区的操作,得到的还是一个 ConnectedStreams:需要传入两个参数 keySelector1 和 keySelector2,是两条流中各自的键选择器;当然也可以直接传入键的位置值(keyPosition),或者键的字段名(field),这与普通的 keyBy用法完全一致。ConnectedStreams 进行 keyBy操作,其实就是把两条流中 key相同的数据放到了一起,然后针对来源的流再做各自处理,这在一些场景下非常有用。

        env.setParallelism(2);
        DataStreamSource<Tuple2<Integer, String>> source1 =
                env.fromElements(
                        Tuple2.of(1, "a1"),
                        Tuple2.of(1, "a2"),
                        Tuple2.of(2, "b"),
                        Tuple2.of(3, "c")
                );
        DataStreamSource<Tuple3<Integer, String, Integer>> source2 =
                env.fromElements(
                        Tuple3.of(1, "aa1", 1),
                        Tuple3.of(1, "aa2", 2),
                        Tuple3.of(2, "bb", 1),
                        Tuple3.of(3, "cc", 1)
                );

        ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer,
                String, Integer>> connect = source1.connect(source2);


        // 多并行度下,需要 根据关联条件进行 keyBy, 才能保证 相同key 的数据到一起去,才能匹配上
        ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>> keyBy = connect.keyBy(s1 -> s1.f0, s2 -> s2.f0);

        /**
         * 实现互相匹配的效果
         *  1.两条流,不一定谁的数据先来
         *  2.每条流,有数据来,存到一个变量中
         *   hashmap
         *    => key=id 第一个字段值
         *    => value=List<数据>
         *  3.每条流有数据来的时候,除了存变量中,不知道对方是否有匹配的数据,要去另一条流存的变量中 查找是否有匹配上的
         */
        SingleOutputStreamOperator<String> process = keyBy.process(new CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>() {
            // 定义hashmap,用来存数据
            Map<Integer, List<Tuple2<Integer, String>>> s1Cache = new HashMap();

            Map<Integer, List<Tuple3<Integer, String, Integer>>> s2Cache = new HashMap();

            /**
             * 第一条流的处理逻辑
             * @param value 第一条流的数据
             * @param ctx 上下文
             * @param out 采集器
             * @throws Exception
             */
            @Override
            public void processElement1(Tuple2<Integer, String> value, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
                Integer id = value.f0;
                // source1的数据来了,就存到变量中
                List<Tuple2<Integer, String>> temp = null;
                if (s1Cache.containsKey(id)) {
                    temp = s1Cache.get(id);
                } else {
                    temp = new ArrayList<>();
                }
                temp.add(value);
                s1Cache.put(id, temp);
                // 去source2的缓存中查找是否有id能够匹配上的数据
                if (s2Cache.containsKey(id)) {
                    List<Tuple3<Integer, String, Integer>> s2Item = s2Cache.get(id);
                    for (Tuple3<Integer, String, Integer> item : s2Item) {
                        out.collect("s1 " + value + "<=========>" + "s2:" + item);
                    }
                }
            }

            /**
             * 第二条流的处理逻辑
             * @param value 第二条流的数据
             * @param ctx 上下文
             * @param out 采集器
             * @throws Exception
             */
            @Override
            public void processElement2(Tuple3<Integer, String, Integer> value, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
                Integer id = value.f0;
                // source2的数据来了,就存到变量中
                List<Tuple3<Integer, String, Integer>> temp = null;
                if (s2Cache.containsKey(id)) {
                    temp = s2Cache.get(id);
                } else {
                    temp = new ArrayList<>();
                }
                temp.add(value);
                s2Cache.put(id, temp);
                // 去source1的缓存中查找是否有id能够匹配上的数据
                if (s1Cache.containsKey(id)) {
                    List<Tuple2<Integer, String>> s1Item = s1Cache.get(id);
                    for (Tuple2<Integer, String> item : s1Item) {
                        out.collect("s1 " + item + "<=========>" + "s2:" + value);
                    }
                }
            }
        });

        process.print();

4.输出算子(Sink)

Flink 作为数据处理框架,最终还是要把计算处理的结果写入外部存储,为外部应用提供支持。

Flink 的 DataStream API 专门提供了向外部写入数据的方法:addSink。与 addSource 类似,addSink 方法对应着一个“Sink”算子,主要就是用来实现与外部系统连接、并将数据提交写入的;Flink 程序中所有对外的输出操作,一般都是利用 Sink 算子完成的。

Flink1.12 以前,Sink 算子的创建是通过调用 DataStream 的.addSink()方法实现的。

stream.addSink(new SinkFunction(…));

addSink 方法同样需要传入一个参数,实现的是 SinkFunction 接口。在这个接口中只需要重写一个方法 invoke(),用来将指定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用。

Flink1.12 开始,同样重构了 Sink 架构,

stream.sinkTo(…)

Sink 多数情况下同样并不需要我们自己实现。之前我们一直在使用的 print 方法其实就是一种 Sink,它表示将数据流写入标准控制台打印输出。Flink 官方为我们提供了一部分的框架的 Sink 连接器。

像 Kafka 之类流式系统,Flink 提供了完美对接,source/sink 两端都能连接,可读可写;而对于 Elasticsearch、JDBC 等数据存储系统,则只提供了输出写入的 sink 连接器。

除 Flink 官方之外,Apache Bahir 框架,也实现了一些其他第三方系统与 Flink 的连接器。

除此以外,就需要用户自定义实现 sink 连接器了。

1.输出到文件

Flink 专门提供了一个流式文件系统的连接器:FileSink,为批处理和流处理提供了一个统一的 Sink,它可以将分区文件写入 Flink 支持的文件系统。

FileSink 支持行编码(Row-encoded)和批量编码(Bulk-encoded)格式。这两种不同的方式都有各自的构建器(builder),可以直接调用 FileSink 的静态方法:

  • 行编码: FileSink.forRowFormat(basePath,rowEncoder)
  • 批量编码: FileSink.forBulkFormat(basePath,bulkWriterFactory)。
// 每个目录中,都会有 并行度个数的 文件在写入
        env.setParallelism(2);
        // 必须开启Checkpoint,否则一直都是 .inprogress
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataGeneratorSource<String> dataGeneratorSource = new
                DataGeneratorSource<>(
                new GeneratorFunction<Long, String>() {
                    @Override
                    public String map(Long value) throws Exception {
                        return "Number:" + value;
                    }
                },
                Long.MAX_VALUE,
                RateLimiterStrategy.perSecond(1000),
                Types.STRING
        );

        DataStreamSource<String> dataGen = env.fromSource(
                dataGeneratorSource,
                WatermarkStrategy.noWatermarks(),
                "data-generator");

        FileSink<String> fileSink = FileSink
                // 输出行式存储的文件,指定路径、指定编码 [必须]
                .<String>forRowFormat(new Path("e:/tmp"), new SimpleStringEncoder<>("UTF-8"))
                // 输出文件的一些配置,文件名的前缀、后缀 [可选]
                .withOutputFileConfig(
                        OutputFileConfig.builder()
                                .withPartPrefix("coreqi-")
                                .withPartSuffix(".log")
                                .build()
                )
                // 按照目录分桶(根据路径或者根据时间),每个小时一个目录[可选]
                .withBucketAssigner(new DateTimeBucketAssigner<>("yyyy-MM-dd HH", ZoneId.systemDefault()))
                // 文件滚动策略 : 1分钟 或 1M [必须]
                .withRollingPolicy(
                        DefaultRollingPolicy.builder()
                                .withRolloverInterval(Duration.ofMinutes(1))
                                .withMaxPartSize(new MemorySize(1024 * 1024))
                                .build()
                )
                .build();


        dataGen.sinkTo(fileSink);

2.输出到 Kafka

1.添加POM依赖

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>3.0.2-1.18</version>
        </dependency>

2.启动 Kafka 集群[略]

3.代码

1.不关心Key
        env.setParallelism(1);
        // 如果是精准一次,必须开启 checkpoint
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> socketDS = env.socketTextStream("localhost", 7878);


        // kafka sink
        // 注意:如果要使用 精准一次 写入 Kafka,需要满足以下条件,缺一不可
        // 1、开启 checkpoint
        // 2、设置事务前缀
        // 3、设置事务超时时间: checkpoint 间隔 < 事务超时时间 < max 的 15 分钟
        KafkaSink<String> kafkaSink = KafkaSink.<String>builder()
                // 指定kafka的地址和端口
                .setBootstrapServers("192.168.58.130:9092,192.168.58.131:9092,192.168.58.132:9092")
                // 指定序列化器,指定Topic名称,具体的序列化
                .setRecordSerializer(
                        KafkaRecordSerializationSchema
                                .<String>builder()
                                .setTopic("ws")
                                .setValueSerializationSchema(new SimpleStringSchema())
                                .build())
                // 写到kafka的一致性级别,精准一次 | 至少一次
                .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
                // 如果是精准一次,必须设置 事务的前缀
                .setTransactionalIdPrefix("coreqi-")
                //如果是精准一次,必须设置 事务超时时间: 大于 checkpoint 间隔,小于 max 15 分钟
                .setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10*60*1000+"")
                .build();

        socketDS.sinkTo(kafkaSink);
2.关心Key

需要自定义序列化器,实现带 key 的 record

        env.setParallelism(1);
        // 如果是精准一次,必须开启 checkpoint
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> socketDS = env.socketTextStream("localhost", 7878);
        
        /**
         * 如果要指定写入 kafka 的 key,可以自定义序列化器:
         * 1、实现 一个接口,重写 序列化 方法
         * 2、指定 key,转成 字节数组
         * 3、指定 value,转成 字节数组
         * 4、返回一个 ProducerRecord 对象,把 key、value 放进去
         */
        KafkaSink<String> kafkaSink = KafkaSink.<String>builder()
                // 指定kafka的地址和端口
                .setBootstrapServers("192.168.58.130:9092,192.168.58.131:9092,192.168.58.132:9092")
                // 自定义序列化器
                .setRecordSerializer(new KafkaRecordSerializationSchema<String>() {
                    @Nullable
                    @Override
                    public ProducerRecord<byte[], byte[]> serialize(String s, KafkaSinkContext kafkaSinkContext, Long aLong) {
                        String[] datas = s.split(",");
                        byte[] key = datas[0].getBytes(StandardCharsets.UTF_8);
                        byte[] value = s.getBytes(StandardCharsets.UTF_8);
                        return new ProducerRecord<>("ws", key, value);
                    }
                })
                // 写到kafka的一致性级别,精准一次 | 至少一次
                .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
                // 如果是精准一次,必须设置 事务的前缀
                .setTransactionalIdPrefix("coreqi-")
                //如果是精准一次,必须设置 事务超时时间: 大于 checkpoint 间隔,小于 max 15 分钟
                .setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10*60*1000+"")
                .build();

        socketDS.sinkTo(kafkaSink);

3.输出到 MySQL(JDBC)

参考地址:https://nightlies.apache.org/flink/flink-docs-release-1.18/docs/connectors/table/jdbc/

1.添加POM依赖

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>

官方还未提供 flink-connector-jdbc 的 1.18.0 的正式依赖,暂时从 apache snapshot 仓库下载,pom 文件中指定仓库路径:

    <repositories>
        <repository>
            <id>apache-snapshots</id>
            <name>apache snapshots</name>
            <url>https://repository.apache.org/content/repositories/snapshots/</url>
        </repository>
    </repositories>

添加依赖:

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-jdbc</artifactId>
            <version>1.17-SNAPSHOT</version>
        </dependency>

如果不生效,还需要修改本地 maven 的配置文件settings.xml,mirrorOf 中添加如下标红内容:

    <mirror>
        <id>aliyunmaven</id>
        <mirrorOf>*,!apache-snapshots</mirrorOf>
        <name>阿里云公共仓库</name>
        <url>https://maven.aliyun.com/repository/public</url>
    </mirror>

因为我用的Flink版本是1.18,Maven仓库中也没有相关依赖,因此使用了最新的修改了下,完整的依赖列表如下

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-jdbc</artifactId>
            <version>3.1.1-1.17</version>
            <scope>provided</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.flink</groupId>
                    <artifactId>flink-table-api-java-bridge</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge</artifactId>
            <version>1.18.0</version>
            <scope>provided</scope>
        </dependency>

2.启动 MySQL,在 test 库下建表 ws

CREATE TABLE `ws` (
 `id` varchar(100) NOT NULL,
 `ts` bigint(20) DEFAULT NULL,
 `vc` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

3.代码

        env.setParallelism(1);

        SingleOutputStreamOperator<WaterSensor> socketDS = env.socketTextStream("localhost", 7878).map(new MapFunction<String, WaterSensor>() {
            @Override
            public WaterSensor map(String value) throws Exception {
                String[] split = value.split(",");
                return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            }
        });

        /**
         * TODO 写入 mysql
         * 1、只能用老的 sink 写法: addsink
         * 2、JDBCSink 的 4 个参数:
         * 第一个参数: 执行的 sql,一般就是 insert into
         * 第二个参数: 预编译 sql, 对占位符填充值
         * 第三个参数: 执行选项 ---》 攒批、重试
         * 第四个参数: 连接选项 ---》 url、用户名、密码
         */
        SinkFunction<WaterSensor> jdbcSink = JdbcSink.sink(
                "insert into ws values(?,?,?)",
                new JdbcStatementBuilder<WaterSensor>() {
                    @Override
                    public void accept(PreparedStatement preparedStatement, WaterSensor waterSensor) throws SQLException {
                        //每收到一条 WaterSensor,如何去填充占位符
                        preparedStatement.setString(1, waterSensor.getId());
                        preparedStatement.setLong(2, waterSensor.getTs());
                        preparedStatement.setInt(3, waterSensor.getVc());
                    }
                },
                JdbcExecutionOptions.builder()
                        .withMaxRetries(3) // 重试次数
                        .withBatchSize(100) // 批次的大小:条数
                        .withBatchIntervalMs(3000) // 批次的时间
                        .build(),
                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                        .withUrl("jdbc:mysql://192.168.58.130:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8")
                        .withUsername("root")
                        .withPassword("coreqi")
                        .withConnectionCheckTimeoutSeconds(60) // 重试的超时时间
                        .build()
        );

        socketDS.addSink(jdbcSink);

4.测试,使用Mysql客户端查看是否成功写入数据

4.自定义Sink输出

如果我们想将数据存储到我们自己的存储设备中,而 Flink 并没有提供可以直接使用的连接器,就只能自定义 Sink 进行输出了。与 Source 类似,Flink 为我们提供了通用的SinkFunction 接口和对应的 RichSinkDunction 抽象类,只要实现它,通过简单地调用DataStream 的.addSink()方法就可以自定义写入任何外部存储。

stream.addSink(new MySinkFunction<String>());

在实现 SinkFunction 的时候,需要重写的一个关键方法 invoke(),在这个方法中我们就可以实现将流里的数据发送出去的逻辑。

这种方式比较通用,对于任何外部存储系统都有效;不过自定义 Sink 想要实现状态一致性并不容易,所以一般只在没有其它选择时使用。实际项目中用到的外部连接器 Flink官方基本都已实现,而且在不断地扩充,因此自定义的场景并不常见。

    public static class MySink implements SinkFunction<String>{

        /**
         * sink的核心逻辑,写出的逻辑就写在这个方法里
         * @param value
         * @param context
         * @throws Exception
         */
        @Override
        public void invoke(String value, Context context) throws Exception {
            SinkFunction.super.invoke(value, context);
            //写出逻辑
            // 这个方法里,来一条数据,调用一次,所以不要在这里创建 连接对象
        }
    }

    public static class  MySink1 extends RichSinkFunction<String>{
        Connection conn = null;
        @Override
        public void open(Configuration parameters) throws Exception {
            // 在这里创建连接
            super.open(parameters);
            conn = new xxxx;
        }

        @Override
        public void close() throws Exception {
            super.close();
            // 做一些清理,销毁连接
            conn.close();
        }

        /**
         * sink的核心逻辑,写出的逻辑就写在这个方法里
         * @param value
         * @param context
         * @throws Exception
         */
        @Override
        public void invoke(String value, Context context) throws Exception {
            super.invoke(value, context);
            //写出逻辑
            // 这个方法里,来一条数据,调用一次,所以不要在这里创建 连接对象
            conn.xxxx
        }
    }
posted @ 2024-01-21 10:20  SpringCore  阅读(428)  评论(0编辑  收藏  举报