本次学习 Flink 中关于 DataStream 中的源数据进行转化操作的 API.
基本操作
map
将函数作用在集合中的每一个元素上,并返回作用后的结果。
flatMap
将集合中的每个元素变成一个或多个元素,并返回扁平化之后的结果。
keyBy
按照指定的 key 来对流中的数据进行分组。注意:流处理中没有 groupBy,而是 keyBy.
filter
按照指定的条件对集合中的元素进行过滤,过滤出返回 true/符合条件的元素。
reduce
对集合中的元素进行聚合。
下面通过一个例子来应用下以上的几种 API。
需求:对流数据中的单词进行计数,并过滤掉敏感词汇 TMD。
package com.learn.flink.transformations;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import java.util.Arrays;
public class TransformationDemo1 {
public static void main(String[] args) throws Exception {
//0: env
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//1: source
DataStreamSource<String> ds = env.socketTextStream("node01", 999);
//2. transformation
// 切割
DataStream<String> words = ds.flatMap((String lines, Collector<String> out) -> Arrays.stream(lines.split(" ")).forEach(out::collect))
.returns(Types.STRING);
// 过滤
DataStream<String> filterWords = words.filter((String word) -> !word.equalsIgnoreCase("TMD")).returns(Types.STRING);
// 将每个单词转化为单词和数量记为 1 的格式
DataStream<Tuple2<String, Integer>> wordAndOne = filterWords.map((String word) -> Tuple2.of(word, 1))
.returns(Types.TUPLE(Types.STRING, Types.INT));
// 分组
KeyedStream<Tuple2<String, Integer>, String> grouped = wordAndOne.keyBy(value -> value.f0);
// 聚合
SingleOutputStreamOperator<Tuple2<String, Integer>> sum = grouped.sum(1);
SingleOutputStreamOperator<Tuple2<String, Integer>> reduce = grouped.reduce((Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) -> Tuple2.of(value1.f0, value1.f1 + value2.f1));
//3: sink
sum.print();
reduce.print();
//4: execute
env.execute();
}
}
合并与连接
union
算子可以合并多个 同类型的数据流,并生成同类型的数据流,即可以将多个 DataStream[T] 合并为一个新的DataStream[T]。数据将按照先进先出(first in first out)的模式合并。
connect
connect 提供了和 union 类似的功能,用来连接两个数据流。
connect 与 union 的区别在于:
- connect 只能连接两个数据流,union 可以连接多个数据流。
- connect 连接的数据流的数据类型可以不一致, union 所连接的两个数据流的类型必须相同。
- 多个数据流经过 union 之后,仅仅是数据进行先进先出的合并。而两个 DataStream 数据流经过 connect 之后会被转化为 ConnectedStreams ,ConnectedStreams 会对两个数据应用不同的处理方法,且双流之间可以共享状态。
package com.learn.flink.transformations;
import org.apache.flink.streaming.api.datastream.ConnectedStreams;
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;
/**
* transformation - 合并与连接
*/
public class TransformationDemo2 {
public static void main(String[] args) throws Exception {
//0: env
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//1: source
final DataStream<String> ds1 = env.fromElements("hello1", "apache1", "flink1");
final DataStream<String> ds2 = env.fromElements("hello2", "apache2", "flink2");
final DataStream<Long> ds3 = env.fromSequence(1, 10);
//2. transformation
// union 可以合并同类型的数据流
final DataStream<String> ds1Uds2 = ds1.union(ds2);
final DataStream<String> ds2Uds1 = ds2.union(ds1);
//Cannot resolve method 'union(org.apache.flink.streaming.api.datastream.DataStream<java.lang.Long>)'
// final DataStream<String> union = ds1.union(ds3);
// connect 可以合并 2 个同类型的或者不同类型的数据源
final ConnectedStreams<String, String> ds1Cds2 = ds1.connect(ds2);
final ConnectedStreams<String, Long> ds1Cds3 = ds1.connect(ds3);
/**
* OUT map1(IN1 var1) throws Exception;
* OUT map2(IN2 var1) throws Exception;
*/
final DataStream<String> ds1Cds3Map = ds1Cds3.map(new CoMapFunction<String, Long, String>() {
// 用于处理 ConnectedStreams 的第一个参数
@Override
public String map1(String s) throws Exception {
return "String:" + s;
}
// 用于处理 ConnectedStreams 的第二个参数
@Override
public String map2(Long aLong) throws Exception {
return "Long:" + aLong;
}
});
//3: sink
ds1Uds2.print();
// ds2Uds1.print();
// connect 合并后的数据源,需要经过后续的处理
// ds1Cds3.print();
ds1Cds3Map.print();
//4: execute
env.execute();
}
}
结果:
拆分和选择
split:将一个流分成多个流。
Select:获取分流后对应的数据。
以上 2 个方法在 flink 1.12 之后已经过期并移除,所以实现拆分的方法使用下面的 OutputTag 和 process 来实现。
Side Outputs:可以使用 process 方法对流中的数据进行处理,并且针对不同的处理结果将数据收集到不同的 OutputTag 中。
package com.learn.flink.transformations;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
/**
* transformation - 拆分和选择
*/
public class TransformationDemo3 {
public static void main(String[] args) throws Exception {
//0: env
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//1: source
final DataStream<Integer> ds = env.fromElements( 1, 2, 3,4,5,6,7,8,9,10);
//2. transformation
final OutputTag<Integer> oddTag = new OutputTag<>("奇数", TypeInformation.of(Integer.class));
final OutputTag<Integer> evenTag = new OutputTag<>("偶数", TypeInformation.of(Integer.class));
final SingleOutputStreamOperator<Integer> processDS = ds.process(new ProcessFunction<Integer, Integer>() {
/**
* @param value : 数据源中的每个数据
* @param context
* @param collector: 收集器可以收集每个处理后的数据
* @throws Exception
*/
@Override
public void processElement(Integer value, Context context, Collector<Integer> collector) throws Exception {
if (value % 2 == 0) {
// 偶数
context.output(evenTag, value);
} else {
context.output(oddTag, value);
}
}
});
final DataStream<Integer> oddOutput = processDS.getSideOutput(oddTag);
final DataStream<Integer> evenOutput = processDS.getSideOutput(evenTag);
//3: sink
oddOutput.print("奇数:");
evenOutput.print("偶数:");
//4: execute
env.execute();
}
}
结果:
重新平衡 rebalance
在数据的处理过程中有可能会出现如图下的数据倾斜,每个分区分配的处理的数据很不均衡,其他 3 台机器即使执行完毕也要等待机器 1 执行完毕后才算整体将任务完成。
Flink 中提供的 rebalance 方法可以直接解决这种数据倾斜的情况。
package com.learn.flink.transformations;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
/**
* transformation - 重平衡分区 rebalance
*/
public class TransformationDemo4 {
public static void main(String[] args) throws Exception {
//0: env
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
//1: source
DataStream<Long> ds = env.fromSequence(1, 100);
//2. transformation
DataStream<Long> filterDS = ds.filter((Long value) -> value > 10);
final DataStream<Tuple2<Integer, Integer>> result1 = filterDS
.map(new RichMapFunction<Long, Tuple2<Integer, Integer>>() {
@Override
public Tuple2<Integer, Integer> map(Long value) throws Exception {
// 获取子任务的 ID 即分区的编号
final int subtaskId = getRuntimeContext().getIndexOfThisSubtask();
return Tuple2.of(subtaskId, 1);
}
})
.keyBy(t -> t.f0)
.sum(1); // 按照分区编号分组并聚合
final DataStream<Tuple2<Integer, Integer>> result2 = filterDS
.rebalance()
.map(new RichMapFunction<Long, Tuple2<Integer, Integer>>() {
@Override
public Tuple2<Integer, Integer> map(Long value) throws Exception {
// 获取子任务的 ID 即分区的编号
final int subtaskId = getRuntimeContext().getIndexOfThisSubtask();
return Tuple2.of(subtaskId, 1);
}
}).keyBy(t -> t.f0).sum(1); // 按照分区编号分组并聚合
//3: sink
result1.print("result1:");
result2.print("result2:");
//4: execute
env.execute();
}
}
结果: