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
}
}
作者:奇
出处:https://www.cnblogs.com/fanqisoft/p/17977571
版权:本作品采用「本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!