文章目录
前言和准备工作
数据源读入数据之后,我们就可以使用各种转换算子,将一个或多个DataStream转换为新的DataStream。
我们为了后续的练习,我们定义一个类:
package flink_transfrom;
import java.util.Objects;
public class WaterSensor {
public String id;
public Long ts;
public Integer vc;
public WaterSensor() {
}
public WaterSensor(String id, Long ts, Integer vc) {
this.id = id;
this.ts = ts;
this.vc = vc;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getTs() {
return ts;
}
public void setTs(Long ts) {
this.ts = ts;
}
public Integer getVc() {
return vc;
}
public void setVc(Integer vc) {
this.vc = vc;
}
@Override
public String toString() {
return "WaterSensor{" +
"id='" + id + '\'' +
", ts=" + ts +
", vc=" + vc +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WaterSensor that = (WaterSensor) o;
return Objects.equals(id, that.id) &&
Objects.equals(ts, that.ts) &&
Objects.equals(vc, that.vc);
}
@Override
public int hashCode() {
return Objects.hash(id, ts, vc);
}
}
我们发现这个类有几个特点:
- 类是公有(public)的
- 有一个无参的构造方法
- 所有属性都是公有(public)的
- 所有属性的类型都是可以序列化的
Flink会把这样的类作为一种特殊的POJO(Plain Ordinary Java Object简单的Java对象,实际就是普通JavaBeans)数据类型来对待,方便数据的解析和序列化。另外我们在类中还重写了toString方法,主要是为了测试输出显示更清晰。
我们这里自定义的POJO类会在后面的代码中频繁使用,所以在后面的代码中碰到,把这里的POJO类导入就好了。
基本转换算子
1.映射(map)
map是大家非常熟悉的大数据操作算子,主要用于将数据流中的数据进行转换,形成新的数据流。简单来说,就是一个“一一映射”,消费一个元素就产出一个元素。
我们只需要基于DataStream调用map()方法就可以进行转换处理。方法需要传入的参数是接口MapFunction的实现;返回值类型还是DataStream。
我们用map算子实现提取WaterSensor中的id字段的功能:
package flink_transfrom;
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 mapDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> sensorsDS = env.fromElements(
new WaterSensor("s1", 1L, 1),
new WaterSensor("s2", 2L, 2),
new WaterSensor("s3", 3L, 3)
);
/* //方式一:匿名实现类
SingleOutputStreamOperator<String> map = sensorsDS.map(new MapFunction<WaterSensor, String>() {
@Override
public String map(WaterSensor waterSensor) throws Exception {
return waterSensor.getId();
}
});*/
//方式二:lambda表达式
SingleOutputStreamOperator<String> map = sensorsDS.map(waterSensor -> waterSensor.getId());
map.print();
env.execute();
}
}
2.过滤(filter)
filter转换操作,顾名思义是对数据流执行一个过滤,通过一个布尔条件表达式设置过滤条件,对于每一个流内元素进行判断,若为true则元素正常输出,若为false则元素被过滤掉。
进行filter转换之后的新数据流的数据类型与原数据流是相同的。filter转换需要传入的参数需要实现FilterFunction接口,而FilterFunction内要实现filter()方法,就相当于一个返回布尔类型的条件表达式。
我们用filter算子将数据流中传感器id为sensor_1的数据过滤出来:
package flink_transfrom;
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 filterDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> sensorsDS = env.fromElements(
new WaterSensor("s1", 1L, 1),
new WaterSensor("s1", 4L, 4),
new WaterSensor("s2", 2L, 2),
new WaterSensor("s3", 3L, 3)
);
SingleOutputStreamOperator<WaterSensor> filter = sensorsDS.filter(new FilterFunction<WaterSensor>() {
@Override
public boolean filter(WaterSensor waterSensor) throws Exception {
return "s1".equals(waterSensor.getId());
}
});
filter.print();
env.execute();
}
}
3.扁平映射(flatMap)
flatMap操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个一个的个体使用。消费一个元素,可以产生0到多个元素。flatMap可以认为是“扁平化”(flatten)和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,再对拆分后的元素做转换处理。
这个算子比较重要的一个特点是一进多出。
我们用flatMap算子实现这么一个功能:如果输入的数据是sensor_1,只打印vc;如果输入的数据是sensor_2,既打印ts又打印vc。
package flink_transfrom;
import org.apache.flink.api.common.functions.FlatMapFunction;
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 org.apache.flink.util.Collector;
public class flatMapDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> sensorsDS = env.fromElements(
new WaterSensor("s1", 1L, 1),
new WaterSensor("s1", 4L, 4),
new WaterSensor("s2", 2L, 2),
new WaterSensor("s3", 3L, 3)
);
SingleOutputStreamOperator<String> flatMap = sensorsDS.flatMap(new FlatMapFunction<WaterSensor, String>() {
@Override
public void flatMap(WaterSensor value, Collector<String> out) throws Exception {
if ("s1".equals(value.getId())) {
out.collect(value.getVc().toString());
} else if ("s2".equals(value.getId())) {
out.collect(value.getTs().toString());
out.collect(value.getVc().toString());
}
}
});
flatMap.print();
env.execute();
}
}
思考:map怎么控制的一进一出,flatmap怎么控制的一进多出
看一下map方法,用的是return,必须返回一个东西,那肯定是一出
再来看一下flatmap方法,它没有返回值,它的输出取决于采集器,我们可以调用多次采集器,通过Collector来输出,调用几次就输出几条。
聚合算子(Aggregation)
1.按键分区(keyBy)
我们是不能直接对数据流进行聚合的,对于Flink而言,DataStream是没有直接进行聚合的API的。
因为我们对海量数据做聚合肯定要进行分区并行处理,这样才能提高效率。
所以在Flink中,要做聚合,需要先进行分区;这个操作就是通过keyBy来完成的。
keyBy是聚合前必须要用到的一个算子。keyBy通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务。
基于不同的key,流中的数据将被分配到不同的分区中去;这样一来,所有具有相同的key的数据,都将被发往同一个分区。
我们可以以id作为key做一个分区操作:
package flink_Aggregation;
import flink_transfrom.WaterSensor;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class keyByDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
DataStreamSource<WaterSensor> sensorsDS = env.fromElements(
new WaterSensor("s1", 1L, 1),
new WaterSensor("s1", 4L, 4),
new WaterSensor("s2", 2L, 2),
new WaterSensor("s3", 3L, 3)
);
KeyedStream<WaterSensor, String> Sensor = sensorsDS.keyBy(new KeySelector<WaterSensor, String>() {
@Override
public String getKey(WaterSensor value) throws Exception {
return value.getId();
}
});
Sensor.print();
env.execute();
}
}
需要注意的是,keyBy得到的结果将不再是DataStream,而是会将DataStream转换为KeyedStream。KeyedStream可以认为是“分区流”或者“键控流”,它是对DataStream按照key的一个逻辑分区,所以泛型有两个类型:除去当前流中的元素类型外,还需要指定key的类型。
KeyedStream也继承自DataStream,所以基于它的操作也都归属于DataStream API。但它跟之前的转换操作得到的SingleOutputStreamOperator不同,只是一个流的分区操作,并不是一个转换算子。
KeyedStream是一个非常重要的数据结构,只有基于它才可以做后续的聚合操作(比如sum,reduce)。
我们看一下运行结果:
一个子任务可以理解为一个分区,我们设置的并行度是2,所以有两个子任务,那为什么s1和s3在同一个子任务中呢?其实这并不矛盾,一个分区(子任务)中可以存在多个分组(key),只要保证相同的key在同一个分区中就可以(比如两个id为s1的都在2号分区)。
2.简单聚合(sum/min/max/minBy/maxBy)
有了按键分区的数据流KeyedStream,我们就可以基于它进行聚合操作了。Flink为我们内置实现了一些最基本、最简单的聚合API,主要有以下几种:
- sum():在输入流上,对指定的字段做叠加求和的操作。
- min():在输入流上,对指定的字段求最小值。
- max():在输入流上,对指定的字段求最大值。
- minBy():与min()类似,在输入流上针对指定字段求最小值。不同的是,min()只计算指定字段的最小值,其他字段会保留最初第一个数据的值;而minBy()则会返回包含字段最小值的整条数据。
- maxBy():与max()类似,在输入流上针对指定字段求最大值。两者区别与min()/minBy()完全一致。
简单聚合算子使用非常方便,语义也非常明确。这些聚合方法调用时,也需要传入参数;但并不像基本转换算子那样需要实现自定义函数,只要说明聚合指定的字段就可以了。指定字段的方式有两种:指定位置,和指定名称。
对于元组类型的数据,可以使用这两种方式来指定字段。需要注意的是,元组中字段的名称,是以f0、f1、f2、…来命名的。
如果数据流的类型是POJO类,那么就只能通过字段名称来指定,不能通过位置来指定了。
为了方便观察,我们把并行度设为1,看一个sum算子:
KeyedStream<WaterSensor, String> Sensor = sensorsDS.keyBy(new KeySelector<WaterSensor, String>() {
@Override
public String getKey(WaterSensor value) throws Exception {
return value.getId();
}
});
SingleOutputStreamOperator<WaterSensor> vc = Sensor.sum("vc");
vc.print();
注意:我们的聚合操作是对组内的聚合,对同一个key的数据进行聚合,两个s1属于同一个分组,vc先是1,后来4到来之后进行sum操作相加变成5。
简单聚合算子返回的,同样是一个SingleOutputStreamOperator,也就是从KeyedStream又转换成了常规的DataStream。所以可以这样理解:keyBy和聚合是成对出现的,先分区、后聚合,得到的依然是一个DataStream。而且经过简单聚合之后的数据流,元素的数据类型保持不变。
max()和maxBy()的区别:
区别在ts字段,max()只计算指定字段的最大值,其他字段会保留最初第一个数据的值;而maxBy()则会返回包含字段最大值的整条数据。(min和minBy()同理)
3.归约聚合(reduce)
reduce操作也会将KeyedStream转换为DataStream。它不会改变流的元素数据类型,所以输入类型和输出类型必须是一样的。
我们实现一个把他们的vc相加这么一个功能:
SingleOutputStreamOperator<WaterSensor> reduce = Sensor.reduce(new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
System.out.println("value1="+value1);
System.out.println("value2="+value2);
return new WaterSensor(value1.id,value2.ts,value1.vc+value2.vc);
}
});
reduce.print();
我们来看一下运行结果:
我们可以看到第一条数据来的时候并没有进入到reduce方法,而是直接打印了出来。
所以每个key的第一条数据来的时候并不会执行reduce方法,而是把它存起来然后直接输出。
所以reduce方法的两个参数:
value1:之前的计算结果,存的状态;value2:现在来的数据。