以上是基本流上的 Window 操作与 KeyedStream 上的 Window 操对比。
KeyedStream 上的 Window 操作使采用多个实例并发处理成为了可能。
可以这样理解,KeyBy 和 Window 操作都是对数据进行分组,但是 KeyBy 是在水平分向对流进行切分,而 Window 是在垂直方式对流进行切分。
使用 KeyBy 进行数据切分之后,后续算子的每一个实例可以只处理特定 Key 集合对应的数据。除了处理本身外,Flink 中算子维护一部分状态(State),在KeyedStream 算子的状态也是可以分布式存储的。
由于 KeyBy 是一种确定的数据分配方式(下文将介绍其它分配方式),因此即使发生 Failover 作业重启,甚至发生了并发度的改变,Flink 都可以重新分配 Key 分组并保证处理某个 Key 的分组一定包含该 Key 的状态,从而保证一致性。
一般情况下,KeyBy 操作只有当 Key 的数量超过算子的并发实例数才可以较好的工作。由于同一个 Key 对应的所有数据都会发送到同一个实例上,因此如果Key 的数量比实例数量少时,就会导致部分实例收不到数据,从而导致计算能力不能充分发挥。
使用 scala 进行开发时,很多 TypeInformation 的方法都是通过隐式的参数传递进行的。再使用 java 调用 Scala 的一些方法时,需要进行 TypeInformation 的传递
同时,由于 java 中存在泛型擦除的情况,也需要显示的指定DataStream 中数据的类型以及 TypeInformation。
public class GroupedProcessingTimeWindowSample {
// 模拟数据 商品类型 + 个数
private static class DataSource extends RichParallelSourceFunction<Tuple2<String, Integer>> {
private volatile boolean isRunning = true;
@Override
public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
Random random = new Random();
while (isRunning) {
Thread.sleep((getRuntimeContext().getIndexOfThisSubtask() + 1) * 1000 * 5);
String key = "类别" + (char) ('A' + random.nextInt(3));
int value = random.nextInt(10) + 1;
System.out.println(String.format("Emits\t(%s, %d)", key, value));
ctx.collect(new Tuple2<>(key, value));
}
}
@Override
public void cancel() {
isRunning = false;
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
DataStream<Tuple2<String, Integer>> ds = env.addSource(new DataSource());
KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = ds.keyBy(0);
keyedStream.sum(1).keyBy(new KeySelector<Tuple2<String, Integer>, Object>() {
@Override
public Object getKey(Tuple2<String, Integer> stringIntegerTuple2) throws Exception {
return "";
}
}).fold(new HashMap<String, Integer>(), new FoldFunction<Tuple2<String, Integer>, HashMap<String, Integer>>() {
@Override
public HashMap<String, Integer> fold(HashMap<String, Integer> accumulator, Tuple2<String, Integer> value) throws Exception {
accumulator.put(value.f0, value.f1);
return accumulator;
}
}).addSink(new SinkFunction<HashMap<String, Integer>>() {
@Override
public void invoke(HashMap<String, Integer> value, Context context) throws Exception {
// 每个类型的商品成交量
System.out.println(value);
// 商品成交总量
System.out.println(value.values().stream().mapToInt(v -> v).sum());
}
});
env.execute();
}
}
模拟的数据源,它继承自 RichParallelSourceFunction,它是可以有多个实例的 SourceFunction 的接口。
两个方法需要实现,一个是 Run 方法,Flink 在运行时对 Source 会直接调用该方法,该方法需要不断的输出数据,从而形成初始的流。在 Run 方法的实现中,会随机的产生商品类别和交易量的记录,然后通过 ctx#collect 方法进行发送。另一个是 Cancel 方法,当 Flink 需要 Cancel Source Task 的时候会调用该方法,使用一个 Volatile 类型的变量来标记和控制执行的状态。
创建 StreamExecutioniEnviroment 对象。创建对象调用的 getExecutionEnvironment 方法会自动判断所处的环境,从而创建合适的对象。例如,如果在 IDE 中直接右键运行,则会创建 LocalStreamExecutionEnvironment 对象;如果是在一个实际的环境中,则会创建 RemoteStreamExecutionEnvironment 对象。
第一个需求是统计每一个类型的成交量。
基于 Environment 对象,创建一个 Source,从而得到初始的<商品类型,成交量>流。然后,为了统计每种类别的成交量,使用 KeyBy 按 Tuple 的第 1 个字段(即商品类型)对输入流进行分组,并对每一个 Key 对应的记录的第 2 个字段(即成交量)进行求合。在底层,Sum 算子内部会使用 State 来维护每个Key(即商品类型)对应的成交量之和。当有新记录到达时,Sum 算子内部会更新所维护的成交量之和,并输出一条<商品类型,更新后的成交量>记录。
在 Sum 后添加一个 Sink 算子对不断更新的各类型成交量进行输出。
第二个需求是对所有类型的总成交量进行统计。
需要将所有记录输出到同一个节点的实例上。
通过 KeyBy 并且对所有记录返回同一个 Key,将所有记录分到同一个组中,从而可以全部发送到同一个实例上。
使用 Fold 方法来在算子中维护每种类型商品的成交量。虽然目前 Fold 方法被标记为 Deprecated,但是在 DataStream API 中暂时还没有能替代它的其它操作,所以仍然使用 Fold 方法。
Fold 方法接收一个初始值,然后当后续流中每条记录到达的时候,算子会调用所传递的 FoldFunction 对初始值进行更新,并发送更新后的值。
Fold 方法中传入一个初始的 HashMap来对各个类别的当前成交量进行维护,当有一条新的<商品类别,成交量>到达时,更新该 HashMap。这样在 Sink 中,收到的就是最新的商品类别和成交量的 HashMap。
例子主要是用来演示 DataStream API 的用法,实际上还会有更高效的写法,此外,更上层的 Table / SQL 还支持 Retraction 机制,可以更好的处理这种情况。
调用 DataStream#map 算法时,Flink 在底层会创建一个 Transformation 对象,这一对象就代表计算逻辑图中的节点。其中记录了传入的 MapFunction,也就是 UDF(User Define Function)。随着调用更多的方法,创建更多的 DataStream 对象,每个对象在内部都有一个 Transformation 对象,这些对象根据计算依赖关系组成一个图结构。后续 Flink 将对这个图结构进行进一步的转换,从而最终生成提交作业所需要的 JobGraph。
以上内容来自对 https://www.bilibili.com/video/av47970985 的学习总结。