【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题

目录

一、窗口

1、窗口的概念

2、窗口的分类

(1)按照驱动类型分类——时间窗口和计数窗口

(2)按照窗口分配数据的规则分类

 3、窗口 API

(1)按键分区窗口(Keyed Windows)

(2)非按键分区(Non-Keyed Windows)

(3)窗口 API 的调用

4、窗口分配器

(1)滚动处理时间窗口

(2)滑动处理时间窗口

(3)处理时间会话窗口

(4)滚动事件时间窗口

(5)滑动事件时间窗口

(6)事件时间会话窗口

(7)滚动计数窗口

(8)滑动计数窗口

5、窗口函数

(1)增量聚合函数(incremental aggregation functions)

(2)全窗口函数(full window functions)

(3)增量聚合和全窗口函数的结合使用

6、其他 API

(1)触发器(Trigger)

(2)移除器(Evictor)

(3)允许延迟(Allowed Lateness)

(4)将迟到的数据放入侧输出流

7、窗口的生命周期

(1) 窗口的创建

(2)窗口计算的触发

(3)窗口的销毁

二、迟到数据的处理

1、设置水位线延迟时间

2、允许窗口处理迟到数据

3、将迟到数据放入窗口侧输出流


一、窗口

1、窗口的概念

        在流处理中,我们往往需要面对的是连续不断、无休无止的无界流,不可能等到所有所有数据都到齐了才开始处理。更加高效的做法是,把无界流进行切分,每一段数据分别进行聚合,结果只输出一次。这就相当于将无界流的聚合转化为了有界数据集的聚合,这就是所谓的“ 窗口”(Window)聚合操作。 窗口聚合其实是对实时性和处理效率的一个权衡。
        在 Flink 中 窗口就是用来处理无界流的核心。我们很容易把窗口想象成一个固定位置的“框”,数据源源不断地流过来,到某个时间点窗口该关闭了,就停止收集数据、触发计算并输出结果。

【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题_第1张图片

【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题_第2张图片

        我们为了正确处理迟到数据,结果把早到的数据划分到了错误的窗口——最终结果都是错误的。所以在 Flink 中, 窗口其实并不是一个“框”,流进来的数据被框住了就只能进这一个窗口。相比之下,我们应该 把窗口理解成一个“桶”,如图 6-15 所示。在 Flink 中,窗口可以把流切割成有限大小的多个“存储桶”(bucket);每个数据都会分发到对应的桶中,当到达窗口结束时间时,就对每个桶中收集的数据进行计算处理。
        Flink 中窗口并不是静态准备好的,而是动态创建——当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。

【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题_第3张图片

2、窗口的分类

(1)按照驱动类型分类——时间窗口和计数窗口

按照时间段去截取数据,这种窗口就叫作“时间窗口”( TimeWindow)。
按照固定的个数,来截取一段数据集,这种窗口叫作“计数窗口”(Count Window)

【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题_第4张图片

 Flink 中有一个专门的类来表示时间窗口,名称就叫作 TimeWindow。这个类只有两个私有属性:start end,表示窗口的开始和结束的时间戳,单位为毫秒。

在 Flink 内部也并没有对应的类来表示计数窗口,底层是通过“全局窗口”(Global Window)来实现的。

(2)按照窗口分配数据的规则分类

  • 滚动窗口(Tumbling Windows
滚动窗口有固定的大小,是一种对数据进行“均匀切片”的划分方式。窗口之间没有重叠,也不会有间隔,是“首尾相接”的状态。滚动窗口可以基于时间定义,也可以基于数据个数定义;需要的参数只有一个,就是窗口的大小(window size)。
  • 滑动窗口(Sliding Windows
与滚动窗口类似,滑动窗口的大小也是固定的。区别在于,窗口之间并不是首尾相接的,而是可以“错开”一定的位置。如果看作一个窗口的运动,那么就像是向前小步“滑动”一样。 定义滑动窗口的参数有两个:除去 窗口大小(window size)之外,还有一个“ 滑动步长”(window slide)。 滑动窗口可以基于时间定义,也可以基于数据个数定义。
  •  会话窗口(Session Windows
        就是数据来了之后就开启一个会话窗口,如果接下来还有数据陆续到来,那么就一直保持会话;如果一段时间一直没收到数据,那就认为会话超时失效,窗口自动关闭。 会话窗口只能基于时间来定义,“会话”终止的标志就是“隔一段时间没有数据来”, 对于会话窗口而言,最重要的参数就是这段时间的长度(size ),它表示会话的超时时间,也就是两个会话窗口之间的最小距离。
        在 Flink 底层,对会话窗口的处理会比较特殊:每来一个新的数据,都会创建一个新的会话窗口;然后判断已有窗口之间的距离,如果小于给定的 size ,就对它们进行合并( merge ) 操作。在 Window 算子中,对会话窗口会有单独的处理逻辑。
  • 全局窗口(Global Windows
        这种窗口全局有效,会把相同 key 的所有数据都分配到同一个窗口中;说直白一点,就跟没分窗口一样。无界流的数据永无止尽,所以这种窗口也没有结束的时候,默认是不会做触发计算的。如果希望它能对数据进行计算处理,还需要自定义“触发器”(Trigger)。
        全局窗口没有结束的时间点,所以一般在希望做更加灵活的窗口处理时自定义使用。Flink 中的计数窗口( Count Window ),底层就是用全局窗口实现的。

 3、窗口 API

1)按键分区窗口(Keyed Windows

        经过按键分区 keyBy 操作后,数据流会按照 key 被分为多条逻辑流 logical streams ),这
就是 KeyedStream 。基于 KeyedStream 进行窗口操作时 , 窗口计算会在多个并行子任务上同时执行。相同 key 的数据会被发送到同一个并行子任务,而窗口操作会基于每个 key 进行单独的处理。所以可以认为,每个 key 上都定义了一组窗口,各自独立地进行统计计算。
stream.keyBy(...)
      .window(...)

(2)非按键分区(Non-Keyed Windows

        如果没有进行 keyBy ,那么原始的 DataStream 就不会分成多条逻辑流, 只有一条逻辑流 。这时窗口逻辑只能在一个任务(task )上执行,就相当于并行度变成了 1。所以在实际应用中一般不推荐使用这种方式。在代码中,直接基于 DataStream 调用 .windowAll() 定义窗口。
stream.windowAll(...)

(3)窗口 API 的调用

窗口操作主要有两个部分:窗口分配器(Window Assigners )和窗口函数 WindowFunctions )。
stream.keyBy()
     .window()
     .aggregate()

4、窗口分配器

窗口分配器其实就是在指定窗口的类型。窗口按照驱动类型可以分成时间窗口和计数窗口,而按照具体的分配规则,又有滚动窗口、滑动窗口、会话窗口、全局窗口四种。标准的声明方式就是直接调用.window(),在里面传入对应时间语义下的窗口分配器。我们不需要专门定义时间语义,默认就是事件时间;如果想用处理时间,那么在这里传入处理时间的窗口分配器就可以了。

(1)滚动处理时间窗口

stream.keyBy(...)
      .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
      .aggregate(...)

// 得到北京时间每天 0 点开启的滚动窗口
// 只要设置-8 小时的偏移量就可以了:
.window(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8)))

(2)滑动处理时间窗口

stream.keyBy(...)
      .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
      .aggregate(...)

(3)处理时间会话窗口

stream.keyBy(...)
      .window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
      .aggregate(...)

(4)滚动事件时间窗口

stream.keyBy(...)
      .window(TumblingEventTimeWindows.of(Time.seconds(5))) .aggregate(...)

(5)滑动事件时间窗口

stream.keyBy(...)
      .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
      .aggregate(...)

(6)事件时间会话窗口

stream.keyBy(...)
      .window(EventTimeSessionWindows.withGap(Time.seconds(10)))
      .aggregate(...)

(7)滚动计数窗口

        定义了一个长度为 10 的滚动计数窗口,当窗口中元素数量达到 10 的时候,就会触发计算执行并关闭窗口:
stream.keyBy(...)
      .countWindow(10)

(8)滑动计数窗口

        与滚动计数窗口类似,不过需要在.countWindow() 调用时传入两个参数: size slide ,前
者表示窗口大小,后者表示滑动步长:
stream.keyBy(...)
      .countWindow(10,3)

(9)全局窗口

        全局窗口是计数窗口的底层实现,一般在需要自定义窗口时使用。它的定义同样是直接调
.window() ,分配器由 GlobalWindows 类提供。
stream.keyBy(...)
      .window(GlobalWindows.create());

5、窗口函数

        经窗口分配器处理之后,数据可以分配到对应的窗口中,而数据流经过转换得到的数据类型是 WindowedStream。这个类型并不是 DataStream,所以并不能直接进行其他转换,而必须进一步调用窗口函数,对收集到的数据进行处理计算之后,才能最终再次得到 DataStream。窗口函数定义了要对窗口中收集的数据做的计算操作,根据处理的方式可以分为两类:增量聚合函数和全窗口函数。

【Flink】Flink中的窗口API、窗口函数以及迟到数据处理问题_第5张图片

(1)增量聚合函数(incremental aggregation functions

就像 DataStream 的简单聚合一样,每来一条数据就立即进行计算,中间只要保持一个简单的聚合状态就可以了;区别只是在于不立即输出结果,而是要等到窗口结束时间。等到窗口到了结束时间需要输出计算结果的时候,我们只需要拿出之前聚合的状态直接输出,这无疑就大大提高了程序运行的效率和实时性。 典型的增量聚合函数有两个:ReduceFunction AggregateFunction
  • 归约函数(ReduceFunction
就是将窗口中收集到的数据两两进行归约窗口函数中也提供了 ReduceFunction:只要基于 WindowedStream 调用 .reduce()方法,然后传入 ReduceFunction 作为参数,就可以指定以归约两个元素的方式去对窗口中数据进行聚合了。
stream.keyBy(r -> r.f0)
        // 设置滚动事件时间窗口
        .window(TumblingEventTimeWindows.of(Time.seconds(5)))
        .reduce(new ReduceFunction>() {
           @Override
           public Tuple2 reduce(Tuple2 value1, Tuple2 value2) throws Exception {
              // 定义累加规则,窗口闭合时,向下游发送累加结果
               return Tuple2.of(value1.f0,value1.f1+value2.f1);
           }
        }).print();
  • 聚合函数(AggregateFunction
ReduceFunction 可以解决大多数归约聚合的问题,但是这个接口有一个限制,就是聚合状
态的类型、输出结果的类型都必须和输入数据类型一样。这就迫使我们必须在聚合前,先将数
据转换( map)成预期结果类型;直接基于 WindowedStream 调 用 .aggregate()方法,就可以定义更加灵活的窗口聚合操作。这个方法需要传入一个 AggregateFunction 的实现类作为参数。
AggregateFunction 的工作原理是:首先调用 createAccumulator() 为任务初始化一个状态( 累加器 ) ;而后每来一个数据就调用一次 add() 方法,对数据进行聚合,得到的结果保存在状态中;等到了窗口需要输出时,再调用 getResult() 方法得到计算结果。
// 使用AggregateFunction实现PV/UV

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 确定时间戳和水位线,取出字段中的时间戳
        SingleOutputStreamOperator stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        // 所有数据设置相同的key,发送到同一个分区内统计PV和UV,再相除
        stream.keyBy(r -> "chris")
                .window(SlidingEventTimeWindows.of(Time.seconds(10),Time.seconds(2)))
                .aggregate(new AvgPV())
                .print();

        env.execute();

    }

    // 自定义 aggregate 方法
    public static class AvgPV implements AggregateFunction,Long>,Double> {
        @Override
        public Tuple2, Long> createAccumulator() {
            // 创建累加器
            return Tuple2.of(new HashSet<>(),0L);
        }

        @Override
        public Tuple2, Long> add(Event event, Tuple2, Long> accumulator) {
            // 属于本窗口的数据,来一条累加一条,并返回累加器
            accumulator.f0.add(event.user);
            return Tuple2.of(accumulator.f0,accumulator.f1+1L);
        }

        @Override
        public Double getResult(Tuple2, Long> accumulator) {
            // 窗口闭合时,增量聚合结束,将计算结果发送到下游
            return (double) accumulator.f1 / accumulator.f0.size();
        }

        @Override
        public Tuple2, Long> merge(Tuple2, Long> hashSetLongTuple2, Tuple2, Long> acc1) {
            return null;
        }
    }

(2)全窗口函数(full window functions

全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。很明显,这就是典型的批处理思路了——先攒数据,等一批都到齐了再正式启动处理流程这样做毫无疑问是低效的。

那为什么还需要有全窗口函数呢?这是因为有些场景下,我们要做的计算必须基于全部的数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一些信息(比如窗口的起始时间),这是增量聚合函数做不到的。
  • 窗口函数(WindowFunction
我们可以基于 WindowedStream 调用 .apply() 方法,传入一个 WindowFunction 的实现类。
stream
 .keyBy()
 .window()
 .apply(new MyWindowFunction());
不过我们也看到了, WindowFunction 能提供的上下文信息较少,也没有更高级的功能。事实上,它的作用可以被 ProcessWindowFunction 全覆盖,所以之后可能会逐渐弃用。一般在实际应用,直接使用 ProcessWindowFunction 就可以了。
  • 处理窗口函数(ProcessWindowFunction
ProcessWindowFunction Window API 最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个 “上下文对象”(Context )。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。 作 为 一 个 全 窗 口 函 数 , ProcessWindowFunction 同样需要将所有数据缓存下来、等到窗口触发计算时才使用。
// 自定义窗口处理函数
    public static class UvCountByWindow extends ProcessWindowFunction{
        @Override
        public void process(Boolean aBoolean, Context context, Iterable elements, Collector out) throws Exception {
            HashSet userSet = new HashSet<>();
            // 遍历所有数据,放到Set里去重
            for (Event event: elements){
                userSet.add(event.user);
            }
            // 结合窗口信息,包装输出内容
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect("窗口: " + new Timestamp(start) + " ~ " + new Timestamp(end)
                    + " 的独立访客数量是:" + userSet.size());
        }
    }

(3)增量聚合和全窗口函数的结合使用

如果我们采用增量聚合的方式,那么只需要保存一个当前和的状态,每个数据到来时就会做一次加法,更新状态;到了要输出结果的时候,只要将当前状态直接拿出来就可以了。增量聚合相当于把计算量“均摊”到了窗口收集数据的过程中,自然就会比全窗口聚合更加高效、输出更加实时。
全窗口函数的优势在于提供了更多的信息,可以认为是更加“通用”的窗口操作。它只负责收集数据、提供上下文相关信息,把所有的原材料都准备好,至于拿来做什么我们完全可以任意发挥。这就使得窗口计算更加灵活,功能更加强大。所以在实际应用中,我们往往希望兼具这两者的优点,把它们结合在一起使用。Flink 的Window API 就给我们实现了这样的用法。
我们之前在调用 WindowedStream .reduce() .aggregate() 方法时,只是简单地直接传入了一个 ReduceFunction AggregateFunction 进行增量聚合。除此之外,其实还可以传入第二个参数:一个全窗口函数,可以是 WindowFunction 或者 ProcessWindowFunction
这样调用的处理机制是:基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数的结果拿来当作了 Iterable 类型的输入。一般情况下,这时的可迭代集合中就只有一个元素了。
    // 按照url分组,开滑动窗口统计
        stream.keyBy(data -> data.url)
                .window(SlidingEventTimeWindows.of(Time.seconds(10),Time.seconds(5)))
                // 同时传入 增量聚合函数 和 全窗口函数
                .aggregate(new urlCountAgg(),new urlCountResult())
                .print();

    // 自定义增量聚合函数,来一条数据就加一条
    public static class urlCountAgg implements AggregateFunction {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event event, Long accumulator) {
            return accumulator+1;
        }

        @Override
        public Long getResult(Long aLong) {
            return aLong;
        }

        @Override
        public Long merge(Long aLong, Long acc1) {
            return null;
        }
    }

    // 自定义窗口处理函数,只需要包装窗口信息
    public static class urlCountResult extends ProcessWindowFunction {
        @Override
        public void process(String url, ProcessWindowFunction.Context context, Iterable iterable, Collector collector) throws Exception {
            // 结合窗口信息,包装输出内容
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            // 迭代器中只有一个元素,就是增量聚合函数的计算结果
            collector.collect(new UrlViewCount(url,iterable.iterator().next(),start,end));
        }
    }
窗口处理的主体还是增量聚合,而引入全窗口函数又可以获取到更多的信息包装输出,这 样的结合兼具了两种窗口函数的优势,在保证处理性能和实时性的同时支持了更加丰富的应用场景。

6、其他 API

(1)触发器(Trigger

触发器主要是用来控制窗口什么时候触发计算。所谓的“触发计算”,本质上就是执行窗口函数,所以可以认为是计算得到结果并输出的过程。
Trigger 是窗口算子的内部属性,每个窗口分配器( WindowAssigner )都会对应一个默认的触发器;对于 Flink 内置的窗口类型,它们的触发器都已经做了实现。例如,所有事件时间窗口,默认的触发器都是 EventTimeTrigger ;类似还有 ProcessingTimeTrigger CountTrigger 。所以一般情况下是不需要自定义触发器的。
stream.keyBy(...)
 .window(...)
 .trigger(new MyTrigger())

(2)移除器(Evictor

移除器主要用来定义移除某些数据的逻辑。基于 WindowedStream 调用 .evictor() 方法,就可以传入一个自定义的移除器(Evictor )。
stream.keyBy(...)
 .window(...)
 .evictor(new MyEvictor())
Evictor 接口定义了两个方法:
  • evictBefore():定义执行窗口函数之前的移除数据操作
  • evictAfter():定义执行窗口函数之后的以处数据操作
默认情况下,预实现的移除器都是在执行窗口函数( window fucntions )之前移除数据的。

(3)允许延迟(Allowed Lateness

为了解决迟到数据的问题, Flink 提供了一个特殊的接口,可以为窗口算子设置一个 “允许的最大延迟”(Allowed Lateness )。也就是说,我们可以设定允许延迟一段时间,在这段时间内,窗口不会销毁,继续到来的数据依然可以进入窗口中并触发计算。直到水位线推进到了 窗口结束时间 + 延迟时间,才真正将窗口的内容清空,正式关闭窗口。
stream.keyBy(...)
 .window(TumblingEventTimeWindows.of(Time.hours(1)))
 .allowedLateness(Time.minutes(1))

(4)将迟到的数据放入侧输出流

即使可以设置窗口的延迟时间,终归还是有限的,后续的数据还是会被丢弃。如果不想丢弃任何一个数据,又该怎么做呢?
Flink 还提供了另外一种方式处理迟到数据。我们可以将未收入窗口的迟到数据,放入“侧输出流”(side output )进行另外的处理。所谓的侧输出流,相当于是数据流的一个“分支”,这个流中单独放置那些错过了该上的车、本该被丢弃的数据。基于 WindowedStream .sideOutputLateData() 方法,就可以实现这个功能。方法需要传入一个“输出标签”(OutputTag ,用来标记分支的迟到数据流。因为保存的就是流中的原始数据,所以 OutputTag 的类型与流中数据类型相同。
DataStream stream = env.addSource(...);
OutputTag outputTag = new OutputTag("late") {};
winAggStream = stream.keyBy(...)
                     .window(TumblingEventTimeWindows.of(Time.hours(1)))
                     .sideOutputLateData(outputTag)
DataStream lateStream = winAggStream.getSideOutput(outputTag);

7、窗口的生命周期

(1) 窗口的创建

窗口的类型和基本信息由窗口分配器( window assigners )指定,但窗口不会预先创建好,
而是由数据驱动创建。当第一个应该属于这个窗口的数据元素到达时,就会创建对应的窗口。

(2)窗口计算的触发

除了窗口分配器,每个窗口还会有自己的窗口函数( window functions )和触发器( trigger )。
窗口函数可以分为增量聚合函数和全窗口函数,主要定义了窗口中计算的逻辑;而触发器则是
指定调用窗口函数的条件。

(3)窗口的销毁

一般情况下,当时间达到了结束点,就会直接触发计算输出结果、进而清除状态销毁窗口。这时窗口的销毁可以认为和触发计算是同一时刻。这里需要注意,Flink 中只对时间窗口 有销毁机制;由于计数窗口 是基于全局窗口实现的,而全局窗口不会清除状态,所以就不会被销毁。

二、迟到数据的处理

所谓的“迟到数据” late data ),是指某个水位线之后到来的数据,它的时间戳其实是在水位线之前的。所以只有在事件时间语义下,讨论迟到数据的处理才是有意义的。

1、设置水位线延迟时间

水位线是事件时间的进展,它是我们整个应用的全局逻辑时钟水位线生成之后,会随着数据在任务间流动,从而给每个任务指明当前的事件时间。
那一般情况就不应该把它的延迟设置得太大,否则流处理的实时性就会大大降低。因为水位线的延迟主要是用来对付分布式网络传输导致的数据乱序,而网络传输的乱序程度一般并不会很大,大多集中在几毫秒至几百毫秒。所以实际应用中,我们往往会给水位线设置一个“能够处理大多数乱序数据的小延迟”,视需求一般设在毫秒~ 秒级。
当我们设置了水位线延迟时间后,所有定时器就都会按照延迟后的水位线来触发。如果一个数据所包含的时间戳,小于当前的水位线,那么它就是所谓的“迟到数据”。

2、允许窗口处理迟到数据

水位线延迟设置的比较小,那之后如果仍有数据迟到该怎么办?对于窗口计算而言,如果水位线已经到了窗口结束时间,默认窗口就会关闭,那么之后再来的数据就要被丢弃了。
在水位线到达窗口结束时间时,先快速地输出一个近似正确的计算结果;然后保持窗口继续等到延迟数据,每来一条数据,窗口就会再次计算,并将更新后的结果输出。这样就可以逐步修正计算结果,最终得到准确的统计值了。
将水位线的延迟和窗口的允许延迟数据结合起来,最后的效果就是先快速实时地输出一个近似的结果,而后再不断调整,最终得到正确的计算结果。回想流处理的发展过程,这不就是著名的Lambda 架构吗?原先需要两套独立的系统来同时保证实时性和结果的最终正确性,如今 Flink 一套系统就全部搞定了。

3、将迟到数据放入窗口侧输出流

即使我们有了前面的双重保证,可窗口不能一直等下去,最后总要真正关闭。窗口一旦关 闭,后续的数据就都要被丢弃了。那如果真的还有漏网之鱼又该怎么办呢?
那就要用到最后一招了:用窗口的侧输出流来收集关窗以后的迟到数据。这种方式是最后“兜底”的方法,只能保证数据不丢失;因为窗口已经真正关闭,所以是无法基于之前窗口的结果直接做更新的。我们只能将之前的窗口计算结果保存下来,然后获取侧输出流中的迟到数据,判断数据所属的窗口,手动对结果进行合并更新。尽管有些烦琐,实时性也不够强,但能够保证最终结果一定是正确的。
所以总结起来, Flink 处理迟到数据,对于结果的正确性有三重保障:水位线的延迟,窗口允许迟到数据,以及将迟到数据放入窗口侧输出流。
    // 处理迟到数据的综合案例
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 读取socket文本流
        SingleOutputStreamOperator stream = env.socketTextStream("localhost", 7777)
                .map(new MapFunction() {
                    @Override
                    public Event map(String value) throws Exception {
                        String[] fields = value.split(" ");
                        return new Event(fields[0].trim(), fields[1].trim(), Long.valueOf(fields[2].trim()));
                    }
                })
                // 方式一:设置watermark延迟时间,2秒钟
                .assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(2))
                        .withTimestampAssigner(new SerializableTimestampAssigner() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        }));

        // 定义侧输出流标签
        OutputTag outputTag = new OutputTag("late"){};

        SingleOutputStreamOperator result = stream.keyBy(data -> data.url)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                // 方式二:允许窗口处理迟到数据,设置1分钟的等待时间
                .allowedLateness(Time.minutes(1))
                // 方式三:将最后的迟到数据输出到侧输出流
                .sideOutputLateData(outputTag)
                .aggregate(new UrlViewCountAgg(), new UrlViewCountResult());

        result.print("result");
        result.getSideOutput(outputTag).print("late");

        // 为方便观察,可以将原始数据也输出
        stream.print("input");

        env.execute();
    }

    public static class UrlViewCountAgg implements AggregateFunction {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event value, Long accumulator) {
            return accumulator + 1;
        }

        @Override
        public Long getResult(Long accumulator) {
            return accumulator;
        }

        @Override
        public Long merge(Long a, Long b) {
            return null;
        }
    }

    public static class UrlViewCountResult extends ProcessWindowFunction {

        @Override
        public void process(String url, Context context, Iterable elements, Collector out) throws Exception {
            // 结合窗口信息,包装输出内容
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect(new UrlViewCount(url, elements.iterator().next(), start, end));
        }
    }

你可能感兴趣的:(Flink,分布式,大数据,flink,big,data)