DataStream API(二)
书接上回,在上一篇DataStream API介绍中我们了解到Flink程序一般由五部分组成。分别是:
- 获得执行环境(Environment),
- 加载/创建初始数据(Source),
- 指定此数据的转换(Transform),
- 指定计算结果的存放位置(Sink),
- 触发程序执行(Execut)
Environment和Source已经介绍过了,接下来我们来了解一下Transform以及Flink支持的数据类型。话不多说,Let`s go!
Transform
map
DataStream—>DataStream:这个算子是接收一个输出一个,这么干说可能有点单薄,上图:
由上图我们可以看出来map是接收一条数据处理之后输出一条数据,处理前后数据的量是不变的。
DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
});
这是官网给出的例子。从中我们可以看出map中接收一个MapFunction对象。MapFunction两个泛型分别是输入和输出数据类型。
flatMap
DataStream—>DataStream:这个算子可以接收一条数据,输出一条或多条数据甚至不输出数据。flatMap与map的不同点在于flatMap支持拆分数据。
dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out)
throws Exception {
for(String word: value.split(" ")){
out.collect(word);
}
}
});
与map类似,flatMap中接收一个FlatMapFunction对象。FlatMapFunction中的两个泛型分别是输入和输出数据类型。
filter
DataStream—>DataStream:是专门用于筛选数据的。它会将不满足条件的数据过滤掉。如下图所示:
dataStream.filter(new FilterFunction<Integer>() {
@Override
public boolean filter(Integer value) throws Exception {
return value != 0;
}
});
filter接收一个FilterFunction对象,该对象的filter方法会返回一个boolean类型的数据。如果返回的是false,这条数据就会被过滤掉。
keyBy
DataStream—>KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现的逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现的。如下图所示:
keyBy有4种调用方式
//dataStream实例化为POJO对象后可以按字段分区
dataStream.keyBy("someKey");
//按指定下标的字段分区
dataStream.keyBy(0);
//以上两种方法已经不建议使用了
//通过实现一个KeySelector的接口来返回分组字段
dataStream.keyBy(new MyKey());
private static class MyKey implements KeySelector<Sensor, String> {
public String getKey(Sensor sensor) throws Exception {
return sensor.getId();
}
}
//第四种调用方式还未搞明白,做个记录,后续补上
滚动聚合算子(Rolling Aggregation)
- min()
- max()
- sum()
- minBy()
- maxBy()
这些算子可以针对KeyedStream的每一个支流做聚合。
reduce
KeyedStream → DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
DataStream<Sensor> dataStream = inputData.flatMap(new MySpliter())
.keyBy(new GetMyKey())
.reduce(new ReduceFunction<Sensor>() {
public Sensor reduce(Sensor sensor, Sensor t1) throws Exception {
return new Sensor(sensor.getId(),sensor.getTimeStamp()+1,t1.getTemperature());
}
});
我们可以看到ReduceFunction的reduce方法包括两个参数,分别是上次聚合的结果和本次需要聚合的数据。
split和select
split与select是成对使用的。
split:
DataStream → SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream。
select:
SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream。
举个栗子:传感器温度按照温度高低(以30度为界),拆分成两个数据流
SplitStream<Sensor> dataStream = inputData.flatMap(new MySpliter())
.keyBy(new GetMyKey())
.split(new OutputSelector<Sensor>() {
public Iterable<String> select(Sensor sensor) {
List<String> strList = new ArrayList<String>();
if(sensor.getTemperature()>30){
strList.add("heigh");
}else{
strList.add("low");
}
return strList;
}
});
dataStream.select("heigh");
dataStream.select("low");
dataStream.select("heigh","low");
值得注意的是:这两个算子是过时的。那么现在我们要实现上边提到的需求,怎么办呢?用SideOutput(侧输出流),这个咱们后边再讲。
connect和coMap、coFlatMap
connect和coMap、coFlatMap与split和select类似,也是成对出现的。
connect:
DataStream,DataStream → ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
coMap/coFlatMap
ConnectedStreams → DataStream:作用于ConnectedStreams上,功能与map和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap处理。
DataStream<Tuple2<String, Double>> worning = heigh.map(new MapFunction<Sensor, Tuple2<String, Double>>() {
public Tuple2<String, Double> map(Sensor sensor) throws Exception {
return new Tuple2<String, Double>(sensor.getId(), sensor.getTemperature());
}
});
ConnectedStreams<Tuple2<String, Double>, Sensor> connect = worning.connect(low);
DataStream<Tuple3<String, Double, String>> map = connect.map(new CoMapFunction<Tuple2<String, Double>, Sensor, Tuple3<String, Double, String>>() {
public Tuple3<String, Double, String> map1(Tuple2<String, Double> value) throws Exception {
return new Tuple3<String, Double, String>(value.f0, value.f1, "worning");
}
public Tuple3<String, Double, String> map2(Sensor sensor) throws Exception {
return new Tuple3<String, Double, String>(sensor.getId(), sensor.getTemperature(), "low");
}
});
union
DataStream → DataStream:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream。
dataStream.union(otherStream1, otherStream2, ...);
union与connect的区别
- union之前的流类型必须是一样的;connect之前的流类型可以不一样,在coMap中再调整为统一的流类型输出。
- connect只能合并两条数据流;union可以合并多条数据流。
支持的数据类型
Flink流应用程序处理的是以数据对象表示的事件流。所以在Flink内部,我们需要能够处理这些对象。它们需要被序列化和反序列化,以便通过网络传送它们;或者从状态后端、检查点和保存点读取它们。为了有效地做到这一点,Flink需要明确知道应用程序所处理的数据类型。Flink使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。
Flink还具有一个类型提取系统,该系统分析函数的输入和返回类型,以自动获取类型信息,从而获得序列化器和反序列化器。但是,在某些情况下,例如lambda函数或泛型类型,需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。
Flink支持Java和Scala中所有常见数据类型。使用最广泛的类型有以下几种:
- 基础数据类型
- Java和Scala元组(Tuples)
- Scala样例类(case)
- Java 简单对象(POJOs)
- 其他(Arrays,Lists,Enums,Maps等)
- Hadoop的Writable数据类型
注意:
-
要使用Hadoop的Writable数据类型需要实现org.apache.hadoop.Writable接口。
-
Flink认为的Java POJOs需要满足以下条件:
1. 该类是公有的 (public) 和独立的(没有非静态内部类)
2. 该类拥有公有的无参构造器
3. 类(以及所有超类)中的所有非静态、非 transient 字段都是公有的(非 final 的), 或者具有遵循 Java 4. bean 对于 getter 和 setter 命名规则的公有 getter 和 setter 方法。
Flink 的 TypeInformation 类
//创建TypeInformation类
//非泛型
TypeInformation<String> info = TypeInformation.of(String.class);
//泛型
TypeInformation<Tuple2<String, Double>> info = TypeInformation.of(new TypeHint<Tuple2<String, Double>>(){});
POJO类的序列化
PojoTypeInfo 为 POJO 中的所有字段创建序列化器。Flink 标准类型如 int、long、String 等由 Flink 序列化器处理。 对于所有其他类型,我们回退到 Kryo。
对于 Kryo 不能处理的类型,你可以要求 PojoTypeInfo 使用 Avro 对 POJO 进行序列化。
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceAvro();
类似的我们可以指定用Kryo进行POJO类的序列化
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceKryo();
使用工厂类方法定义类型信息
//带注解的自定义类型
@TypeInfo(MyTupleTypeInfoFactory.class)
public class MyTuple<T0, T1> {
public T0 myfield0;
public T1 myfield1;
}
支持自定义类型信息的工厂:
public class MyTupleTypeInfoFactory extends TypeInfoFactory<MyTuple> {
@Override
public TypeInformation<MyTuple> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParameters) {
return new MyTupleTypeInfo(genericParameters.get("T0"), genericParameters.get("T1"));
}
}