文章目录

  • 键控流算子 - keyBy
  • 基本滚动聚合算子
  • 滚动聚合算子 - Reduce



很多流处理程序的一个基本要求就是要能对数据进行分组,分组后的数据共享某一个相同的属性。DataStream API 提供了一个叫做 KeyedStream 的抽象,此抽象会从逻辑上对 DataStream进行分区,分区后的数据拥有同样的 Key 值,分区后的流互不相关。


针对 KeyedStream 的状态转换操作可以读取数据或者写入数据到当前事件 Key 所对应的状态中。这表明拥有同样 Key 的所有事件都可以访问同样的状态,也就是说所以这些事件可以一起处理。


要小心使用状态转换操作和基于 Key 的聚合操作。如果 Key 的值越来越多,例如:Key 是订单ID,我们必须及时清空 Key 所对应的状态,以免引起内存方面的问题。


KeyedStream 可以使用 map,flatMap 和 filter 子来处理。接下来我们会使用 keyBy 算子来将DataStream 转换成 KeyedStream,并讲解基于 key 的转换操作:滚动聚合和 reduce 算子。

键控流算子 - keyBy

keyBy 通过指定 key 来将 DataStream 转换成 KeyedStream。基于不同的 key,流中的事件将被分配到不同的分区中去。所有具有相同 key 的事件将会在接下来的操作符的同一个子任务槽中进行处理。拥有不同 key 的事件可以在同一个任务中处理。但是算子只能访问当前事件的 key 所对应的状态。

相同的 key 一定发送到同一任务槽,不同的 key 可能发送到同一任务槽处理

如图所示,把输入事件的颜色作为 key,黑色的事件输出到了一个分区,其他颜色输出到了另一个分区。

java flink keyby之后如何聚合Java Flink 读取kafka数据后分组之后如何统计个数 flink keyedstream_flink


keyBy() 方法接收一个参数,这个参数指定了 key 或者 keys,有很多不同的方法来指定 key。

下面例子展示如何使用 keyBy 算子将时间按 user 字段分组:

public class KeyByOperator {

    // 按 user 分组

    public static void main(String[] args) throws Exception{

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // keyBy:将相同 key 的 数据分发到同一逻辑分区

        env
                .addSource(new ClickSource())
                // 第一个泛型:流数据类型
                // 第二个泛型:key 类型
                .keyBy(new KeySelector<Event, String>() {
                    @Override
                    public String getKey(Event value) throws Exception {
                        return value.user;
                    }
                })
                .print();

        env.execute();

    }

}

基本滚动聚合算子

滚动聚合算子由 KeyedStream 调用,并生成一个聚合以后的 DataStream,例如:sum,min,max。一个滚动聚合算子会为每一个观察到的 key 保存一个聚合的值。针对每一个输入事件,算子将会更新保存的聚合结果,并发送一个带有更新后的值的事件到下游算子。滚动聚合不需要用户自定义函数,但需要接受一个参数,这个参数指定了在哪一个字段上面做聚合操作。DataStream API 提供了以下滚动聚合方法。
滚动聚合算子只能用在滚动窗口,不能用在滑动窗口。

  • sum():在输入流上对指定的字段做滚动相加操作。
  • min():在输入流上对指定的字段求最小值。
  • max():在输入流上对指定的字段求最大值。
  • minBy():在输入流上针对指定字段求最小值,并返回包含当前观察到的最小值的事件。
  • maxBy():在输入流上针对指定字段求最大值,并返回包含当前观察到的最大值的事件。

滚动聚合算子无法组合起来使用,每次计算只能使用一个单独的滚动聚合算子。
滚动聚合操作会对每一个 key 都保存一个状态。因为状态从来不会被清空,所以在使用滚动聚合算子时只能使用在含有有限个 key 的流上面。
下面例子通过对 Tuple2 格式的数据的第一个元素进行 keyBy 然后进行聚合统计:

public class AGGReduceOperator {

    public static void main(String[] args) throws Exception{

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Tuple2<String, Integer>> stream = env.fromElements(
                Tuple2.of("a", 1),
                Tuple2.of("a", 3),
                Tuple2.of("b", 3),
                Tuple2.of("b", 4)
        );

        stream.keyBy(r -> r.f0).sum(1).print("sum:");
        stream.keyBy(r -> r.f0).sum("f1").print("sum -> f1:");
        stream.keyBy(r -> r.f0).max(1).print("max:");
        stream.keyBy(r -> r.f0).max("f1").print("max -> f1:");
        stream.keyBy(r -> r.f0).min(1).print("min:");
        stream.keyBy(r -> r.f0).min("f1").print("min -> f1:");
        stream.keyBy(r -> r.f0).maxBy(1).print("maxBy:");
        stream.keyBy(r -> r.f0).maxBy("f1").print("maxBy -> f1:");
        stream.keyBy(r -> r.f0).minBy(1).print("minBy:");
        stream.keyBy(r -> r.f0).minBy("f1").print("minBy -> f1:");

        env.execute();
    }
}

res:
minBy::11> (a,1)
minBy -> f1::3> (b,3)
minBy -> f1::3> (b,3)
sum -> f1::11> (a,1)
min::3> (b,3)
min::3> (b,3)
maxBy::3> (b,3)
max -> f1::3> (b,3)
sum -> f1::11> (a,4)
maxBy::11> (a,1)
maxBy::3> (b,4)
maxBy::11> (a,3)
maxBy -> f1::11> (a,1)
max::3> (b,3)
max -> f1::11> (a,1)
minBy -> f1::11> (a,1)
min::11> (a,1)
minBy -> f1::11> (a,1)
min::11> (a,1)
minBy::11> (a,1)
max -> f1::3> (b,4)
min -> f1::3> (b,3)
min -> f1::11> (a,1)
min -> f1::3> (b,3)
maxBy -> f1::3> (b,3)
min -> f1::11> (a,1)
minBy::3> (b,3)
max -> f1::11> (a,3)
max::3> (b,4)
maxBy -> f1::11> (a,3)
max::11> (a,1)
sum -> f1::3> (b,3)
max::11> (a,3)
sum::11> (a,1)
sum -> f1::3> (b,7)
minBy::3> (b,3)
maxBy -> f1::3> (b,4)
sum::3> (b,3)
sum::11> (a,4)
sum::3> (b,7)

滚动聚合算子 - Reduce

reduce 算子是滚动聚合的泛化实现。它将一个 ReduceFunction 应用到了一个 KeyedStream 上面去。reduce 算子将会把每一个输入事件和当前已经 reduce 出来的值做聚合计算。reduce 操作不会改变流的事件类型。输出流数据类型和输入流数据类型是一样的。
reduce 函数可以通过实现接口 ReduceFunction 来创建一个类。ReduceFunction 接口定义了reduce() 方法,此方法接收两个输入事件,输出一个相同类型的事件。
下面的例子对相同用户的分数进行累加操作:

public class ReduceOperator {

    public static void main(String[] args) throws Exception{

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env
                .fromElements(
                        Tuple2.of("Alick",98),
                        Tuple2.of("Alick",69),
                        Tuple2.of("Bob",91),
                        Tuple2.of("Tom",87),
                        Tuple2.of("Bob",69)
                )
                .keyBy(elem -> elem.f1)
                .reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                        return Tuple2.of(value1.f0,value1.f1 + value2.f1);
                    }
                })
                .print();

        env.execute();
    }
}

res:
16> (Bob,91)
3> (Alick,98)
16> (Tom,87)
3> (Alick,69)
3> (Alick,138)