Flink第六章:flink中的时间和窗口


文章目录


前言

  • 在批处理系统中,我们可以等待一批数据全部到达后再统一处理,这种方式简单直接。然而,在实时流处理场景下**,数据是源源不断、无穷无尽的------这就是所谓的无界数据流。**

  • 面对无界流,我们无法像批处理那样"等所有数据到齐"。那么,如何对实时到达的每一条数据进行有意义的统计呢?答案就是窗口

  • 窗口本质上是对无限数据流进行的一种"切片"操作:我们将无限的数据按照时间或数量划分成有限大小的"数据块",每个数据块就是一个窗口。

  • 在 Flink 等流计算框架中,窗口可以被理解为一个"桶",每个到达的数据根据其时间戳或计数被分配到对应的桶中。

  • 当窗口的结束条件满足时(例如时间到达窗口边界,或收集到足够数量的数据),我们就对这个桶内的所有数据进行一次计算,输出结果,然后销毁窗口,准备迎接下一个窗口。

  • 窗口的引入,使得实时统计成为可能------无论是每分钟的 PV、最近 10 秒的温度滑动平均值,还是用户行为会话分析,都可以通过合适的窗口类型来优雅实现。

  • 本文将系统介绍 Flink 中窗口的核心概念、分类、API 使用,以及支撑窗口正确运行的关键机制------时间语义与水位线(Watermark),并探讨如何处理不可避免的延迟数据。掌握这些知识,是构建健壮实时数据应用的基础。


一、窗口(Window)是什么?

(一)窗口的概念

  • 1.Flink是一种流式计算引擎,主要是来处理无界数据流的 ,数据源源不断、无穷无尽。想要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的"数据块"进行处理,这就是所谓的"窗口"(Window)。
  • 2.具体实例理解:在Flink中 ,窗口其实并不是一个"框",应该把窗口理解成一个"桶" 。在Flink中,窗口可以把流切割成有限大小的多个"存储桶"(bucket) ;每个数据都会分发到对应的桶中,当到达窗口结束时间时,就对每个桶中收集的数据进行计算处理。
  • 注意: Flink中窗口并不是静态准备好的,而是动态创建------当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。另外,这里我们认为到达窗口结束时间时,窗口就触发计算并关闭,事实上"触发计算"和"窗口关闭"两个行为也可以分开,这部分内容我们会在后面详述。

(二)窗口的分类

1.按照驱动类型分类

窗口本身是截取有界数据的一种方式 ,所以窗口一个非常重要的信息其实就是"怎样截取数据"。换句话说,就是以什么标准来开始和结束数据的截取,我们把它叫作窗口的"驱动类型"。

  • (1)时间窗口(Time Window)
    时间窗口以时间点来定义窗口的开始(start)和结束(end) ,所以截取出的就是某一时间段的数据。到达结束时间时,窗口不再收集数据,触发计算输出结果,并将窗口关闭销毁。所以可以说基本思路就是=="定点发车"。==
  • 2)记数窗口(Count Window)
    计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口。每个窗口截取数据的个数,就是窗口的大小。基本思路是=="人齐发车"。==

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

(1)滚动窗口(Tumbling Windows)

滚动窗口有固定的大小 (如下图所示,window size固定),是一种对数据进行**"均匀切片"的划分方式。窗口之间没有重叠,也不会有间隔**,是"首尾相接"的状态。这是最简单的窗口形式,每个数据都会被分配到一个窗口,而且只会属于一个窗口。

滚动窗口应用非常广泛,它可以对每个时间段做聚合统计,很多BI分析指标都可以用它来实现。

滚动窗口可以基于时间定义 ,也可以基于数据个数定义 ;需要的参数只有一个,就是窗口的大小(window size)。

比如:我们可以定义一个长度为1小时的滚动时间窗口,那么每个小时就会进行一次统计;或者定义一个长度为10的滚动计数窗口,就会每10个数进行一次统计。

(2)滑动窗口(Sliding Windows)
  • 滑动窗口的大小也是固定 的。但是窗口之间并不是首尾相接的,而是可以"错开"一定的位置。
  • 定义滑动窗口的参数 有两个:除去窗口大小(window size)之外,还有一个"滑动步长"(window slide) ,它其实就代表了窗口计算的频率。窗口在结束时间触发计算输出结果,那么滑动步长就代表了计算频率。
  • 当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。而具体的个数,就由窗口大小和滑动步长的比值(size/slide)来决定。
  • 滚动窗口也可以看作是一种特殊的滑动窗口------窗口大小等于滑动步长(size = slide)。滑动窗口适合计算结果更新频率非常高的场景
滑动窗口 vs 滚动窗口(考点对比表)
对比维度 滚动窗口(Tumbling Window) 滑动窗口(Sliding Window) 考点重要性
核心区别 窗口之间严格不重叠,首尾相接 窗口之间可以重叠(当滑动步长 < 窗口长度时) ⭐⭐⭐⭐⭐
触发频率 等于窗口长度(例如 5s 触发一次) 等于滑动步长(例如每 5s 触发一次,但窗口长度 10s) ⭐⭐⭐⭐⭐
数据重复计算 每条数据只属于一个窗口 每条数据可能属于多个窗口(重叠部分重复计算) ⭐⭐⭐⭐
窗口数量 时间段长度 / 窗口长度 时间段长度 / 滑动步长(通常会更多) ⭐⭐⭐
适用场景 周期统计(每分钟 PV、每小时活跃用户) 实时趋势分析(每 5s 看最近 10s 的滑动平均) ⭐⭐⭐⭐
内存/状态开销 较小(每个数据只在一个窗口内) 较大(需保留滑动步长内的状态,可能重复存储) ⭐⭐⭐
数学关系 滑动窗口的特例(当窗口长度 = 滑动步长) 滚动窗口的泛化 ⭐⭐
典型 API .window(TumblingProcessingTimeWindows.of(Time.seconds(5))) .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5))) ⭐⭐⭐⭐
💥 重点考点总结(答辩/考试必背)
  1. 最核心区别 :滚动窗口不重叠,滑动窗口可以重叠(重叠程度由滑动步长决定)。
  2. 触发频率公式
    • 滚动窗口触发频率 = 窗口长度
    • 滑动窗口触发频率 = 滑动步长
  3. 数据重复性 :滑动窗口在重叠部分会重复计算数据,滚动窗口每条数据只计算一次。
  4. 实际例子
    • 滚动:每 5 秒统计一次这 5 秒内的数据(干净不重叠)。
    • 滑动:每 5 秒统计一次最近 10 秒的数据(窗口之间有 5 秒重叠)。
  5. 常见问题
    • 何时用滑动窗口? → 需要平滑趋势、避免窗口边界突变时(如股票滑动平均)。
    • 何时用滚动窗口? → 简单的周期性统计,对边界不敏感时。
    • 滑动步长大于窗口长度会发生什么? → 会出现数据间隙(部分数据不被任何窗口包含),一般不用。
  6. 记忆口诀
    "滚动不重叠,滑动常重叠;滚动触发等窗长,滑动触发看步长;重叠数据重复算,趋势平滑滑动强。"

💥 重点考点总结(答辩/考试必背)

  1. 最核心区别 :滚动窗口不重叠,滑动窗口可以重叠(重叠程度由滑动步长决定)。
  2. 触发频率公式
    • 滚动窗口触发频率 = 窗口长度
    • 滑动窗口触发频率 = 滑动步长
  3. 数据重复性 :滑动窗口在重叠部分会重复计算数据,滚动窗口每条数据只计算一次。
  4. 实际例子
    • 滚动:每 5 秒统计一次这 5 秒内的数据(干净不重叠)。
    • 滑动:每 5 秒统计一次最近 10 秒的数据(窗口之间有 5 秒重叠)。
  5. 老师最爱问
    • 何时用滑动窗口? → 需要平滑趋势、避免窗口边界突变时(如股票滑动平均)。
    • 何时用滚动窗口? → 简单的周期性统计,对边界不敏感时。
    • 滑动步长大于窗口长度会发生什么? → 会出现数据间隙(部分数据不被任何窗口包含),一般不用。
  6. 记忆口诀
    "滚动不重叠,滑动常重叠;滚动触发等窗长,滑动触发看步长;重叠数据重复算,趋势平滑滑动强。"
(3)会话窗口(Session Windows)
  • 会话窗口,是基于"会话"(session)对数据进行分组的。会话窗口只能基于时间来定义
  • 会话窗口中,最重要的参数就是会话的超时时间 ,也就是两个会话窗口之间的最小距离。如果相邻两个数据到来的时间间隔(Gap)小于指定的大小(size),那说明还在保持会话,它们就属于同一个窗口 ;如果gap大于size,那么新来的数据就应该属于新的会话窗口,而前一个窗口就应该关闭了。
  • 会话窗口的长度不固定,起始和结束时间也是不确定的,各个分区之间窗口没有任何关联。会话窗口之间一定是不会重叠的,而且会留有至少为size的间隔(session gap)。
  • 在一些类似保持会话 的场景下,可以使用会话窗口来进行数据的处理统计。
(4)全局窗口(Global Windows)
  • "全局窗口",这种窗口全局有效 ,会把相同key的所有数据都分配到同一个窗口中。这种窗口没有结束的时候,默认是不会做触发计算的。如果希望它能对数据进行计算处理,还需要自定义"触发器"(Trigger)。
  • 全局窗口没有结束的时间点,所以一般在希望做更加灵活的窗口处理时自定义使用。Flink中的计数窗口(Count Window),底层就是用全局窗口实现的。

二、Windows窗口API

(一)按键分区和非按键分区

  • 在定义窗口操作之前,首先需要确定,到底是基于按键分区(Keyed)的数据流KeyedStream来开窗,还是直接在没有按键分区的DataStream上开窗。也就是说,在调用窗口算子之前,是否有keyBy操作。

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

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

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

  • 如果没有进行keyBy,那么原始的DataStream就不会分成多条逻辑流。这时窗口逻辑只能在一个任务(task)上执行,就相当于并行度变成了1。
  • 在代码中,直接基于DataStream调用.windowAll()定义窗口。
    stream.windowAll(...)
  • 注意:对于非按键分区的窗口操作,手动调大窗口算子的并行度也是无效的,windowAll本身就是一个非并行的操作。
c 复制代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

(二)代码中窗口API的调用

  • 窗口操作主要有两个部分:窗口分配器(Window Assigners)和窗口函数(Window Functions)。
java 复制代码
// Keyed Window 键控流
stream
.keyBy(<key selector>)		<-  按照一个Key进行分组        
	.window (<window assigner>) 	<-  将数据流中的元素分配到相应的窗口中       
	.reduce/aggregate/process/apply()	<-  窗口函数Window Function
// Non-Keyed Window 非键控流
stream        
	.windowAll(...)         <-  不分组,将数据流中的所有元素分配到相应的窗口中        
	.reduce/aggregate/process()      <-  窗口函数Window Function
  • 其中.window()方法需要传入一个窗口分配器,它指明了窗口的类型;窗口函数用来定义窗口具体的处理逻辑。

(三)窗口函数

窗口函数定义了要对窗口中收集的数据做的计算操作,根据处理的方式可以分为两类:增量聚合函数和全窗口函数。

1.增量聚合函数(ReduceFunction / AggregateFunction)

  • 窗口将数据收集起来,最基本的处理操作当然就是进行聚合。我们可以每来一个数据就在之前结果上聚合一次,这就是"增量聚合"。
  • 常见的增量聚合函数如下:
    reduce(reduceFunction)/sum()/min()/max()
    aggregate(aggregateFunction)

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

  • 有些场景下,我们要做的计算必须基于全部的数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一些信息(比如窗口的起始时间),这是增量聚合函数做不到的。
  • 所以,我们还需要有更丰富的窗口计算方式。窗口操作中的另一大类就是全窗口函数。与增量聚合函数不同,全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。
  • 常见的全窗口函数如下:
    apply(windowFunction)
    process(processWindowFunction)
java 复制代码
package chapter06.watermarker;

import bean.OrderInfo;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
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.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

import java.time.Duration;

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

        env.setParallelism(1);

        KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
                .setBootstrapServers("niit01:9092")
                .setTopics("watermark")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setValueOnlyDeserializer(new SimpleStringSchema()).build();

        DataStreamSource<String> dataStreamSource =
                env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "Watermark Strategy");

        // 定义测流标签
        OutputTag<OrderInfo> sideOutput = new OutputTag<>("SideOutput", TypeInformation.of(OrderInfo.class));

        SingleOutputStreamOperator<OrderInfo> map = dataStreamSource.map(new MapFunction<String, OrderInfo>() {
            @Override
            public OrderInfo map(String json) throws Exception {
                return JSON.parseObject(json, OrderInfo.class);
            }
        });

        SingleOutputStreamOperator<String> result = map
                .assignTimestampsAndWatermarks(WatermarkStrategy    // 设置水印
                        .<OrderInfo>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<OrderInfo>() {
                            @Override
                            public long extractTimestamp(OrderInfo orderInfo, long l) {
                                return orderInfo.getTimestamp();
                            }
                        }))
                .keyBy(orderInfo -> orderInfo.getUid())
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                .allowedLateness(Time.seconds(10))  // 设置长期延迟
                .sideOutputLateData(sideOutput)     // 测流输出
                .apply(new WindowFunction<OrderInfo, String, Integer, TimeWindow>() {
                    @Override
                    public void apply(Integer key, TimeWindow timeWindow, Iterable<OrderInfo> iterable, Collector<String> collector) throws Exception {
                        long start = timeWindow.getStart();
                        long end = timeWindow.getEnd();

                        String startStr = DateFormatUtils.format(start, "yyyy-MM-dd HH:mm:ss");
                        String endStr = DateFormatUtils.format(end, "yyyy-MM-dd HH:mm:ss");

                        int sumMoney = 0;
                        for (OrderInfo orderInfo : iterable) {
                            sumMoney += orderInfo.getMoney();
                        }

                        collector.collect(startStr + "->" + endStr + "->" + key + "->" + sumMoney);
                    }
                });
        result.print();
        result.getSideOutput(sideOutput).print("严重迟到的数据");

        env.execute();
    }
}

三、时间语义

(一)有关概念

1.EventTime(事件时间)

事件(数据)时间:是事件/数据真真正正发生时/产生时的时间

2.ProcessingTime(处理时间)

处理时间:是事件/数据被处理/计算时的系统的时间

对比维度 事件时间(Event Time) 处理时间(Processing Time) 考点重要性
定义 数据实际发生的时间(业务时间) 数据到达 Flink 的系统时间 ⭐⭐⭐
窗口触发依据 Watermark 推进 系统时钟周期性触发 ⭐⭐⭐⭐⭐
能否处理乱序 ✅ 通过 Watermark 容忍乱序 ❌ 无法处理 ⭐⭐⭐⭐⭐
结果确定性 ✅ 确定性 ❌ 依赖机器负载,不稳定 ⭐⭐⭐⭐
数据丢失风险 低(可配合侧输出兜底) ⭐⭐⭐⭐
实现复杂度 ⭐⭐⭐
典型场景 金融、日志、传感器 实时监控、简单告警 ⭐⭐⭐⭐
面试必问 Watermark 原理、迟到策略 为什么不稳定 ⭐⭐⭐⭐⭐

3.数据处理系统中的时间语义

  • 在实际应用中,事件时间语义会更为常见 。一般情况下,业务日志数据中都会记录数据生成的时间戳(timestamp),它就可以作为事件时间的判断基础。
  • 在Flink中,由于处理时间比较简单,早期版本默认的时间语义是处理时间;而考虑到事件时间在实际应用中更为广泛,从Flink1.12版本开始,Flink已经将事件时间作为默认的时间语义了。

(二)水印、水位线(Watermark)

1.为什么需要WaterMark?

  • 当flink 以 EventTime 模式处理流数据时,它会根据数据里的时间戳来处理基于时间的算子。但是由于网络、分布式等原因,会导致数据乱序的情况。
  • 所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
  • 只要使用event time,就必须使用watermark。在上游算子指定,比如:source、map算子后。
  • Watermark的核心本质可以理解成一个延迟触发机制

2.WaterMark是什么?

  • Watermark就是给数据额外添加的一列时间戳!
  • Watermark = 当前最大的事件时间 - 最大允许的延迟时间(或最大允许的乱序时间)
  • 比如,事件时间是10分30秒, 最大允许的延迟时间是2秒,那么水印就是10分28秒

3.Watermark能解决什么问题?

4.水位线生成策略

  • 在Flink的DataStream API中,有一个单独用于生成水位线的方法:.assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间
  • 具体使用如下:
java 复制代码
DataStream<Event> withTimestampsAndWatermarks = 
stream.assignTimestampsAndWatermarks(<watermark strategy>);
  • 说明:WatermarkStrategy作为参数,这就是所谓的"水位线生成策略" 。WatermarkStrategy是一个接口,该接口中包含了一个"时间戳分配器"TimestampAssigner和一个"水位线生成器"WatermarkGenerator

四、Flink对于延迟数据的处理

  • 水印(水位线、watermark)机制可以帮助我们在短期延迟下,允许乱序数据的到来。这个机制很好的处理了那些因为网络等情况短期延迟的数据。
  • 但是水印机制无法长期的等待下去,因为水印机制简单说就是让窗口一直等在那里,等达到水印时间才会触发计算和关闭窗口。
  • 根据具体业务情况,一般水印也就是几秒钟最多几分钟而已。
  • 主要的解决办法是给定一个允许延迟的时间,在该时间范围内仍可以接受处理延迟数据
  • 设置允许延迟的时间是通过allowedLateness(lateness: Time)设置。

(一)allowedLateness(lateness: Time)

  • 当我们对流设置窗口后得到的WindowedSteam对象就可以使用allowedLateness方法。该方法传入一个Time值,设置允许的长期延迟(迟到)的时间。 给定允许延迟时间,处理延迟数据

1.情况一

  • 未设置allowedLateness(为0),当watermark满足条件,会触发窗口的 执行 + 关闭窗口。

2.情况二

  • 当设置了allowedLateness,当watermark满足条件后,只会触发窗口的执行,不会触发窗口关闭。也就是,watermark满足条件后会正常触发窗口计算,将已有的数据完成计算。但是,不会关闭窗口。

3.情况三

  • 如果在allowedLateness允许的时间内仍有这个窗口的数据进来,那么每进来一条,会和已经计算过的(被watermark触发的)数据一起再计算一次。

4.总结

  • 水印:短期延迟,达到条件后触发计算并且关闭窗口(触发+关闭同时进行)。
  • 水印+allowedLateness : 短期延迟+ 等待长期延迟效果, 达到水印条件后,会触发窗口计算,但是不关闭窗口。事件时间延迟达到水印+allowedLateness之和后会关闭窗口。

(二)侧输出-SideOutput

  • Flink 通过watermark在短时间内允许了乱序到来的数据。通过延迟数据处理机制,可以处理长期迟到的数据。
  • Flink的这两个延迟机制尽量确保了数据不会错过了属于他们的窗口,但是如果真的迟到太久,Flink还有一个机制将这些数据收集起来保存成为一个DataStream,然后,交由开发人员自行处理。这个机制就叫做侧输出机制(Side Output)。
  • 侧输出机制:可以将错过水印又错过allowedLateness允许的时间的数据,单独的存放到一个DataStream中,然后开发人员可以自定逻辑对这些超级迟到数据进行处理。

五、总结

本文围绕 Flink 流处理中的窗口机制展开,从基本概念到实际应用进行了较为全面的梳理。回顾全文,核心要点可以概括为以下几个方面:

(一)窗口的本质

窗口是切割无界数据流的手段,通过将无限数据划分为有限的数据块,使得实时聚合、统计成为可能。窗口可以理解为"桶",数据按规则落入桶中,桶满或时间到即触发计算。

(二)窗口的分类

1.按驱动类型分

时间窗口(定点发车)和计数窗口(人齐发车)。

2.按分配规则分

  • 滚动窗口(无重叠、首尾相接)、滑动窗口(可有重叠、支持高频更新)、会话窗口(基于活动间隔动态划分)和全局窗口(需配合自定义触发器)。
  • 其中滑动窗口与滚动窗口的对比是常见考点,核心区别在于数据是否被重复计算以及触发频率的不同。
3.窗口 API 与函数:
  • 按键分区(keyBy)后调用 .window(),非按键分区则使用 .windowAll()。

  • 窗口函数分为增量聚合函数(ReduceFunction / AggregateFunction,每来一条数据就更新一次状态,效率高)和全窗口函数(WindowFunction / ProcessWindowFunction,缓存全部数据后再计算,能获取窗口元信息)。

4.时间语义与水位线:
  • 事件时间(Event Time)是数据实际发生的时间,能正确处理乱序和延迟,是生产环境的默认选择;处理时间(Processing Time)依赖系统时钟,简单但不稳定。

  • 水位线(Watermark) 是事件时间进度的衡量标准,公式为:Watermark = 当前最大事件时间 - 允许的最大乱序时间。它解决了乱序数据导致窗口无法正确触发的问题,是实现事件时间窗口的核心机制。

5.延迟数据的处理策略:
  • 短期延迟通过水位线本身的容忍度解决。

  • 长期延迟则使用 allowedLateness 让窗口在触发计算后继续保持一段时间,接收迟到的数据并触发多次更新。

  • 对于严重超时的数据(既错过了水位线,又超过了 allowedLateness),可以通过侧输出(SideOutput) 将其收集到单独的 DataStream 中,由开发者自定义处理(如存入死信队列或人工干预)。

  • 总之,窗口是流处理中连接"无限数据"与"有限计算"的桥梁,而事件时间、水位线和延迟处理机制共同保证了窗口计算的正确性与健壮性。在实际项目中,应根据业务对实时性、准确性、容错性的不同要求,合理选择窗口类型、时间语义和延迟容忍策略。希望本文能帮助读者建立起清晰的 Flink 窗口知识体系,为后续深入流计算开发打下坚实基础。

相关推荐
xingyuzhisuan2 小时前
算力租赁平台 GPU 资源隔离方案:显存抢占问题深度排查与解决
大数据·云计算·gpu算力
天天讯通3 小时前
OKCC 呼叫中心安全性能全解析:技术防护与管理措施指南
大数据·开发语言·网络·人工智能·安全·语音识别
名不经传的养虾人4 小时前
从0到1:企业级AI项目迭代日记 Vol.47|从“能说”到“能上手”
大数据·人工智能·ai编程·企业ai·多agent协作
MicroTech20255 小时前
业绩披露|微算法科技(MLGO)2025年净利润1.27亿元
大数据·人工智能·科技
AGIPlayer5 小时前
没有生态的大模型不算前沿
大数据·人工智能·物联网
weilaieqi15 小时前
际连集团:印尼公司注册代办一站式服务
大数据
林间码客5 小时前
04 ROC曲线与AUC:从零开始手动计算
大数据·人工智能·算法
穆利堂-movno16 小时前
住宅、写字楼、高校、医院物业后勤数字化升级:“收费+巡检+工单”全链路落地思路
大数据
makise-6 小时前
破译大数据底层密码:从 HDFS 存储基石到现代分布式计算引擎的架构演进
大数据·hdfs·架构