Flink 窗口 概述

一:窗口简述

        Flink是一种流式计算引擎,主要是来处理无界数据流的,数据源源不断、无穷无尽。想要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的“数据块”进行处理,这就是所谓的“窗口”(Window)。 【把窗口理解成一个“桶”,Flink则可以把流切割成大小有限的“储存桶”,把数据分发到不同的桶里,每一个窗口都是一个桶。当窗口结束,就对每一个桶的数据进行收集处理】

二: 窗口的分类

        1)按照驱动类型分

              (1) 时间窗口

                            原理:建立一个窗口,在固定的额时间段内不断收集数据,到达结束时间的时候窗口结束收集数据,生成结果,窗口销毁。【就像地铁一样,间隔一段时间发车,无论车上有多少乘客,地铁都会往前开】

              (2)  计数窗口

                               原理:计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口。每个窗口截取数据的个数,就是窗口的大小。基本思路是“人齐发车”

        2)按照窗口分配数据的规则分类(以下均为以时间驱动为例)

                        主要概念:窗口的大小,窗口的滑动步长【两个窗口重叠的部分】,会话间隔

                (1) 滚动窗口(Tumbling Windows)

                             滚动窗口有固定的大小,而且窗口之间不会重叠,每个数据都在一个窗口且只属于这个窗口。在一个固定时间内,接受数据的传入。到了截止时间,收集数据,输出结果。应用类型广泛,可以对每个时间做聚合统计。

                   c850e2e36a3f48bca15878796c5b9ec0.png

                (2) 滑动窗口  (Sliding Windows)

                                 当窗口大小大于窗口步长的时候就会出现滑动,滑动窗口会重叠,同时数据也会同时被分到多个窗口,滑动步长就代表了计算频率。适合计算结果更新较快的场景。

                              

                (3) 会话窗口   (Session Windows)

                                原理:基于“会话”来进行数据分组、如果相邻两个数据到来的时间间隔 小于指定大小,那么这两个数据在同一个窗口内。如果 大于则数据到了新的窗口,且前面的窗口关闭。  会话窗口长度,起始结束时间不确定。各个分区之间窗口没有任何关联。在规定的时间内没有数据到来触发一次计算。可以用于保持会话的场景下。

                (4) 全局窗口 (Global Windows)

                                把相同key的数据全部分配到一个窗口之中,窗口没有结束是不会触发计算的。如果希望对数据处理,需要定义一个触发器。

                     

三:窗口API

        1)按键分区(Keyed)和非按键分区(Non-Keyed

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

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

在代码实现上,我们需要先对DataStream调用.keyBy()进行按键分区,然后再调用.window()定义窗口。

stream.keyBy(...) .window(...)

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

如果没有进行keyBy,那么原始的DataStream就不会分成多条逻辑流。这时窗口逻辑只能在一个任务(task)上执行,就相当于并行度变成了1。setParallelism(1)

在代码中,直接基于DataStream调用.windowAll()定义窗口。

stream.windowAll(...)

注意:对于非按键分区的窗口操作手动调大窗口算子的并行度也是无效的

四:窗口分配器

        1)Flink中的时间语义

                                到底是以哪种时间作为衡量的标准,就被成为“时间语义”

        2)时间窗口分配器

                                                (1) 处理时间的时间语义窗口

特征:

  • 基于处理节点的当前系统时间。
  • 实现简单,延迟低。
  • 适用于数据实时性要求高且乱序不多的场景。
  • 在处理乱序数据时,结果可能不准确。

应用场景:        

  • 监控系统指标(如CPU使用率、内存使用情况)。
  • 实时数据分析,延迟比准确性更重要的场景。

     

使用参数:                 

TumblingProcessingTimeWindows.of(Time.)    滚动窗口
SlidingProcessingTimeWindows.of(Time.)    滑动窗口
ProcessingTimeSessionWindows.withGap()  ProcessingTimeSessionWindows.withDynamicGap()    会话窗口

                                                

                                                (2) 处理事件的时间语义窗口 

特征:      

  • 基于事件的时间戳。
  • 处理乱序数据,通过Watermark机制来处理延迟事件。
  • 更加准确,适用于要求严格时间语义的场景。

实时场景:    

  • 实时日志分析(如用户行为分析、点击流分析)。
  • 需要严格时间顺序和准确性的场景,如金融交易分析。

使用参数:

TumblingEventTimeWindows.of(Time.)    滚动窗口
SlidingEventTimeWindows.of(Time.)    滑动窗口

EventTimeSessionWindows.withGap() 

EventTimeSessionWindows.withDynamicGap()

    会话窗口

       

        3)计数窗口分配器

countWindow(5)计数窗口分配器  【滚动】  满足5条输出一次计算结果
countWindow(5,2)计数窗口分配器  【滑动】  满足5条输出一次计算结果 , 每经过一个步长都有一个窗口-触发输出【第一次输出在第二条数据来的时候】

        4)全局窗口分配器

GlobalWindows.create()    全局窗口

五:窗口函数

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

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

                                窗口将数据收集起来,最基本的处理操作当然就是进行聚合。我们可以每来一个数据就在之前结果上聚合一次,这就是“增量聚合”。

                       (1)归约函数(ReduceFunction)

                                

import com.guigu.function.WaterSensorMapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
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;


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

        SingleOutputStreamOperator<WaterSensor> map = env.socketTextStream("192.168.88.161", 9999)
                .map(new WaterSensorMapFunction());

        KeyedStream<WaterSensor, String> senorks = map.keyBy(sensor -> sensor.getId());

//        窗口分配器
        WindowedStream<WaterSensor, String, TimeWindow> senorWS = senorks.window(TumblingProcessingTimeWindows.of(Time.seconds(5)));

        /*
            1.相同key的第一条数据来的时候,不会调用reduce方法 【来的数据类型必须保持一致】
            2.增量聚合:来一条数据,就会计算一次,但是不会输出
            3.在窗口触发的时候,才会输出窗口的最终结果
         */
//        窗口函数,增量聚合reduce
//        TODO:返回一个DataStream
        SingleOutputStreamOperator<WaterSensor> reduce = senorWS.reduce(new ReduceFunction<WaterSensor>() {
//            TODO:只有第二条数据进来了才会调用reduce方法
            @Override
            public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
                System.out.println("调用reduce方法: value1" + value1 + ",value2" + value2);
                return new WaterSensor(value1.getId(), value2.getTs(), value1.getVc() + value2.getVc());
            }
        });

        reduce.print();


        env.execute();
    }
}

                       (2)聚合函数(AggregateFunction)

                                        来一个数据就调用add方法,进行数据聚合。结果保存在状态中。窗口需要输出时调用getresult()方法 得到计算结果。和ReduceFunction作用相同,而由于输入、中间状态、输出的类型可以不同,使得应用更加灵活方便。

import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
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;

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

        SingleOutputStreamOperator<WaterSensor> map = env.socketTextStream("192.168.88.161", 9999)
                .map(new WaterSensorMapFunction());

        KeyedStream<WaterSensor, String> senorks = map.keyBy(sensor -> sensor.getId());

//        窗口分配器
        WindowedStream<WaterSensor, String, TimeWindow> senorWS = senorks.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));


//        窗口函数,增量聚合 Aggregate
        /*
            第一个类型:输入数据的类型
            第二个类型: 累加器的类型,存储的中间结果的类型
            第三个类型: 输出的类型
         */


        /*

                *1、属于本窗口的第一条数据来,创建窗口,创建累加器
                *2、增量聚合:来一条计算一条,调用-次add方法
                *3、窗口输出时调用一次getresult方法
                *4、输入、中间累加器、输出 类型可以不一样,非常灵活

         */

        senorWS.aggregate(
                new AggregateFunction<WaterSensor, Integer, String>() {
//                    TODO:创建累加器初始化累加器
                    @Override
                    public Integer createAccumulator() {
                        System.out.println("创建累加器");
                        return 0;
                    }
//                    TODO: 聚合逻辑
                    @Override
                    public Integer add(WaterSensor value, Integer accumulator) {
                        System.out.println("调用add方法,value="+value);
                        return accumulator+ value.getVc();
                    }
//                    TODO:获得最后的结果,窗口触发时输出
                    @Override
                    public String getResult(Integer accumulator) {
                        System.out.println("调用getresult方法");
                        return accumulator.toString();
                    }

                    @Override
                    public Integer merge(Integer a, Integer b) {
//                        只有会话窗口才会用到
                        System.out.println("调用merge方法");
                        return null;
                    }
                }
        ).print();


        env.execute();
    }
}

另外,Flink也为窗口的聚合提供了一系列预定义的简单聚合方法,可以直接基于WindowedStream调用。主要包括.sum()/max()/maxBy()/min()/minBy(),与KeyedStream的简单聚合非常相似。它们的底层,其实都是通过AggregateFunction来实现的。

               2)全窗口函数(窗口函数,处理窗口函数)

                                        全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。    WindowFunction【apply方法】能提供的上下文信息较少,也没有更高级的功能。事实上,它的作用可以被ProcessWindowFunction全覆盖,所以之后可能会逐渐弃用【老方法】

import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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;

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

        SingleOutputStreamOperator<WaterSensor> map = env.socketTextStream("192.168.88.161", 9999)
                .map(new WaterSensorMapFunction());

        KeyedStream<WaterSensor, String> senorks = map.keyBy(sensor -> sensor.getId());

//        窗口分配器  【滚动】
        WindowedStream<WaterSensor, String, TimeWindow> senorWS = senorks.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));


        //        TODO:全窗口  不断存储数据到最后  窗口触发时只会输出一次


        //        TODO:老写法
//        senorWS
//                .apply(
//                new WindowFunction<WaterSensor, String, String, TimeWindow>() {
//                    /**
//                     *
//                     * @param s 分组的key
//                     * @param window 窗口对象
//                     * @param input 存储的数据
//                     * @param out 采集器
//                     * @throws Exception
//                     */
//                    @Override
//                    public void apply(String s, TimeWindow window, Iterable<WaterSensor> input, Collector<String> out) throws Exception {
//
//                    }
//                }
//        )

//        TODO:新写法
                senorWS
        .process(
                new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
                    /**
                     * @param s        分组的 key
                     * @param context  上下文
                     * @param elements 存的数据
                     * @param out      采集器
                     * @throws Exception
                     */
                    @Override
                    public void process(String s, ProcessWindowFunction<WaterSensor, String, String, TimeWindow>.Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
//                        TODO:毫秒
                        long start = context.window().getStart();
                        long end = context.window().getEnd();

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

//                        TODO:多少条数据
                        long data = elements.spliterator().estimateSize();

                        out.collect("key="+s+"的窗口【"+start_format+","+end_format+"]包含"+data+"条数据 ====》"+elements.toString());

                    }
                }
        ).print();


        env.execute();
    }
}

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

import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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;

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

        SingleOutputStreamOperator<WaterSensor> map = env.socketTextStream("192.168.88.161", 9999)
                .map(new WaterSensorMapFunction());

        KeyedStream<WaterSensor, String> senorks = map.keyBy(sensor -> sensor.getId());

//        窗口分配器
        WindowedStream<WaterSensor, String, TimeWindow> senorWS = senorks.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));


//        TODO:窗口函数,增量聚合 Aggregate  +  全窗口 Process
                /*
            *增量聚合 Aggregate+全窗日 process
                    * 1、增量聚合函数处理数据:来一条计算一条
                    *2、窗口触发时,增量聚合的结果(只有一条)传递给 全窗口函数
                    *3、经过全窗口函数的处理包装后,输出米指轺调坝党
            *结合两者的优点:
                *1、增量聚合: 来一条计算一条,存储中间的计算结果,占用的空间少
                *2、全窗口函数:可以通过 上下文 实现灵活的功能
                * */

        senorWS.aggregate(
                new MyAgg(),
                new MyProcess()
                ).print();
//


        env.execute();
    }

    public static class MyAgg implements  AggregateFunction<WaterSensor, Integer, String>{
//        TODO:创建累加器初始化累加器
        @Override
        public Integer createAccumulator() {
            System.out.println("创建累加器");
            return 0;
        }
        //                    TODO: 聚合逻辑
        @Override
        public Integer add(WaterSensor value, Integer accumulator) {
            System.out.println("调用add方法,value="+value);
            return accumulator+ value.getVc();
        }
        //                    TODO:获得最后的结果,窗口触发时输出
        @Override
        public String getResult(Integer accumulator) {
            System.out.println("调用getresult方法");
            return accumulator.toString();
        }

        @Override
        public Integer merge(Integer a, Integer b) {
//                        只有会话窗口才会用到
            System.out.println("调用merge方法");
            return null;
        }


    }



    public static class MyProcess extends ProcessWindowFunction<String,String,String,TimeWindow>{

        /**
         * @param s        分组的 key
         * @param context  上下文
         * @param elements 存的数据
         * @param out      采集器
         * @throws Exception
         */


        @Override
        public void process(String s, ProcessWindowFunction<String, String, String, TimeWindow>.Context context, Iterable<String> elements, Collector<String> out) throws Exception {
//                TODO:毫秒
            long start = context.window().getStart();
            long end = context.window().getEnd();

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

//                        TODO:多少条数据
            long data = elements.spliterator().estimateSize();

            out.collect("key="+s+"的窗口【"+start_format+","+end_format+"]包含"+data+"条数据 ====》"+elements.toString());

        }
    }
}

这样调用的处理机制是:基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数的结果拿来当作了Iterable类型的输入。

六:其他API

                1) 触发器(Trigger)

                                        触发器主要是用来控制窗口什么时候触发计算。所谓的“触发计算”,本质上就是执行窗口函数,所以可以认为是计算得到结果并输出的过程。基于WindowedStream调用.trigger()方法,就可以传入一个自定义的窗口触发器(Trigger)。

stream.keyBy(...) .window(...) .trigger(new MyTrigger())

                2)移除器(Evictor)

                                移除器主要用来定义移除某些数据的逻辑。基于WindowedStream调用.evictor()方法,就可以传入一个自定义的移除器(Evictor)。Evictor是一个接口,不同的窗口类型都有各自预实现的移除器。

stream.keyBy(...) .window(...)

七:窗口的原理 

以 时间类型的 滚动窗口 为例,分析原理:

1、窗口什么时候触发 输出?

时间进展>= 窗口的最大时间戳(end-1ms)


2、窗口是怎么划分的?     

start= 向下取整,取窗口长度的整数倍     

end =start+窗长度

窗口左闭右开        ==》        属于本窗口的  最大时间戳 =end - 1ms


3、窗口的生命周期?

创建:属于本窗口的第一条数据来的时候,现new的,放入一个singeton单例的集合中。

销毁(关窗):  时间进展>= 窗口的最大时间戳(end - 1ms) +允许迟到的时间【默认为0】

八:水位线 (Watermark)

        1)什么是水位线?【下图中均为虚线(w)表示】

                        在Flink中水位线被用来标记事件的进展时间。是在数据流里面的一个标记点,具体内容是时间戳。用来指示当前事件的处理时间。

        2) 有序数据的处理

                        为了提高速率,一般会每隔一段时间产生一个水位线

        3)乱序+数据迟到的水位线处理

                                通常在流式计算中,数据的传输收到网络IO等等因素的影响,数据无法准时到达计算的窗口。为了让窗口处理数据变得规整且正确。我们通常使用让水位线等待固定秒数【意思就是在时间戳的基础上增加一些延迟,以保证不丢失数据】

注意:乱序    和    迟到的区别:

乱序: 数据的顺序乱了   时间小的比时间大的晚来

迟到: 数据的时间戳  <   当前的  watermark

        4) 水位线的特征

  1. 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据

  2. 水位线主要的内容是一个时间戳,用来表示当前事件时间【数据传入的时间】的进展

  3. 水位线是基于数据的时间戳生成的

  4. 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进水位线可以通过设置延迟,来保证正确处理乱序数据

  5.  一个水位线Watermank(t),表示在当前流中事件时间已经达到了时间戳T,这代表t之前的所有数据都到齐了,之后流中不会出现   时间戳T<t的数据。

  6.  水位线是Flink流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理                                                                   

         

        5)水位线的使用【需要使用事件时间语义窗口才能触发】

                      (1)有序流的使用

                                直接调用WatermarkStrategy.forMonotonousTimestamps()方法,然后使用assignTimestampsAndWatermarks()接受 。就可以实现。

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.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

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

        env.setParallelism(1);

        SingleOutputStreamOperator<WaterSensor> map = env.socketTextStream("192.168.88.130", 9999)
                .map(new WaterSensorMapFunction());
        /*
            1.定义watermark策略
            2.使用assignTimestampsAndWatermarks 调用
         */

//        TODO:指定watermark策略
        WatermarkStrategy<WaterSensor> waterSensorWatermarkStrategy = WatermarkStrategy
                .<WaterSensor>forMonotonousTimestamps()    // 指定watermark的生成 单调递增  没有等待时间
//                指定时间戳分配器,从数据中提取
                .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                    @Override
                    public long extractTimestamp(WaterSensor element, long recordTimestamp) {
//                        返回的时间戳是毫秒
                        System.out.println("数据=" + element + ",recordTS = " + recordTimestamp);
                        return element.getTs() * 1000L;
                    }
                });

        map.assignTimestampsAndWatermarks(waterSensorWatermarkStrategy)

                .keyBy(sensor -> sensor.getId())
//                TODO:只能使用 事件时间语义窗口 才能使用水平线
                .window(TumblingEventTimeWindows.of(Time.seconds(2)))
                 .process(
                new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
                    /**
                     * @param s        分组的 key
                     * @param context  上下文
                     * @param elements 存的数据
                     * @param out      采集器
                     * @throws Exception
                     */
                    @Override
                    public void process(String s, ProcessWindowFunction<WaterSensor, String, String, TimeWindow>.Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
//                        TODO:毫秒
                        long start = context.window().getStart();
                        long end = context.window().getEnd();

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

//                        TODO:多少条数据
                        long data = elements.spliterator().estimateSize();

                        out.collect("key="+s+"的窗口【"+start_format+","+end_format+"]包含"+data+"条数据 ====》"+elements.toString());

                    }
                }
        ).print();


        env.execute();
    }
}

                      (2) 无序流的使用

                                        将.forMonotonousTimestamps()方法 修改为forBoundedOutOfOrderness() 参数Duration.ofSeconds(等待的时间)                          

//        TODO:指定watermark策略
        WatermarkStrategy
                //  TODO: 指定watermark的生成 乱序  有等待时间 3s
                .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
//                TODO: 指定时间戳分配器,从数据中提取
                .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                    @Override
                    public long extractTimestamp(WaterSensor element, long recordTimestamp) {
//                        返回的时间戳是毫秒
                        System.out.println("数据=" + element + ",recordTS = " + recordTimestamp);
                        return element.getTs() * 1000L;
                    }
                });

                      (3) 自定义水位线的使用 

                                                将.forBoundedOutOfOrderness(方法 修改为 forGenerator(重写类atermarkGeneratorSupplier)    最后 return 自己的方法<>(延迟的时间)                         

                                        

WatermarkStrategy
                //  TODO: 自定义指定watermark的生成 乱序  有等待时间 3s
                .<WaterSensor>forGenerator(new WatermarkGeneratorSupplier<WaterSensor>() {
                    @Override
                    public WatermarkGenerator<WaterSensor> createWatermarkGenerator(Context context) {
//                        TODO:延迟时间3s
                        return new MyGenerator<>(3000);
                    }
                })

                      (4) 在数据源发送水位线   

                                                我们也可以在自定义的数据源中抽取事件时间,然后发送水位线。这里要注意的是,在自定义数据源中发送了水位线以后,就不能再在程序中使用assignTimestampsAndWatermarks方法来生成水位线了。在自定义数据源中生成水位线和在程序中使用assignTimestampsAndWatermarks方法生成水位线二者只能取其一。

env.fromSource(
kafkaSource, WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)), "kafkasource"
)

                6)水位线的传递 

                                一个Task通常会设置多个并行度,而而当一个任务接收到多个上游并行任务传递来的水位线时,应该以最小的那个作为当前任务的事件时钟。每个任务都以“处理完之前所有数据”为标准来确定自己的时钟

九:处理乱序+迟到数据  常用三部曲               

                1)水位线空闲等待 

                                        在多个上游并行任务中,如果有其中一个没有数据,由于当前Task是以最小的那个作为当前任务的事件时钟,而没有数据的并行任务一直保持最小。就会导致当前Task的水位线无法推进,就可能导致窗口无法触发。这时候可以设置空闲等待(withIdleness)。

env.socketTextStream("192.168.88.130", 9999)
                .partitionCustom(new MyPartioner(), r -> r)
                .map(r -> Integer.parseInt(r))
                .assignTimestampsAndWatermarks(WatermarkStrategy
                        .<Integer>forMonotonousTimestamps()
                        .withTimestampAssigner((r, ts) -> r * 1000L)
//                        TODO: 空闲等待  5s
                        .withIdleness(Duration.ofSeconds(5))
                );

                2)  允许推迟时间关窗 

                                         Flink的窗口,也允许迟到数据。当触发了窗口计算后,会先计算当前的结果,但是此时并不会关闭窗口。以后每来一条迟到数据,就触发一次这条数据所在窗口计算(增量计算)。直到wartermark 超过了窗口结束时间+推迟时间,此时窗口会真正关闭,迟到来的数据不会被计算。

  
                .keyBy(sensor -> sensor.getId())
//                TODO:只能使用 事件时间语义窗口 才能使用水平线
                .window(TumblingEventTimeWindows.of(Time.seconds(2)))
//                TODO:使用窗口延迟   5s
                .allowedLateness(Time.seconds(5))

                3) 侧输出流 

.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))
// TODO:  【关窗后】   迟到的数据放入侧输出流
.sideOutputLateData(
// TODO:  参数1:侧输出流的名字    参数2:侧输出数据的类型
        new OutputTag<WaterSensor>("late_data", Types.POJO(WaterSensor.class))
)

                4) 设置经验 

1.watermark等待时间,设置一个不算特别大的,一般是秒级,在乱序和 延迟 取舍

2.设置一定的窗口允许迟到,只考虑大部分的迟到数据,极端小部分迟到很久的数据,不管

3.极端小部分迟到很久的数据,放到侧输出流。获取到之后可以做各种处理 

十:基于时间的合流-----双流联结(Join)

可以发现,根据某个key合并两条流,与关系型数据库中表的join操作非常相近。事实上,Flink中两条流的connect操作,就可以通过keyBy指定键进行分组后合并,实现了类似于SQL中的join操作;另外connect支持处理函数,可以使用自定义实现各种需求,其实已经能够处理双流join的大多数场景。

不过处理函数是底层接口,所以尽管connect能做的事情多,但在一些具体应用场景下还是显得太过抽象了。比如,如果我们希望统计固定时间内两条流数据的匹配情况,那就需要自定义来实现——其实这完全可以用窗口(window)来表示。为了更方便地实现基于时间的合流操作,Flink的DataStrema API提供了内置的join算子。

                1)窗口联结  (Window  Join)

                                Flink为基于一段时间的双流合并专门提供了一个窗口联结算子,可以定义时间窗口,并将两条流中共享一个公共键(key)的数据放在窗口中进行配对处理。

//        TODO: window  join 窗口联结
        
    /**
         * stream1.join(stream2)
         *  .where(<KeySelector>)
         * .equalTo(<KeySelector>)
         * .window(<WindowAssigner>)
         *  .apply(<JoinFunction>)
         *
         *   1. 落在同一个时间窗口范围内才能匹配
         *   2. 根据keyby的key,来进行匹配关联
         *   3. 只能拿到匹配上的数据,类似有固定时间范围的inner join
     */


        data1.join(data2)
//                TODO:data1的keyby
                .where(new KeySelector<Tuple2<String, Integer>, String>() {
                    @Override
                    public String getKey(Tuple2<String, Integer> value) throws Exception {
                        return value.f0;
                    }
                })
//                TODO:data2的keyby
                .equalTo(new KeySelector<Tuple3<String, Integer, Integer>, String>() {
                    @Override
                    public String getKey(Tuple3<String, Integer, Integer> value) throws Exception {
                        return value.f0;
                    }
                })

                .window(TumblingEventTimeWindows.of(Time.seconds(5)))

                .apply(new JoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>() {
                    /**
                     *关联上的数据调用join方法
                     * @param first The element from first input.  [data1]
                     * @param second The element from second input. [data2]
                     * @return
                     * @throws Exception
                     */
                    @Override
                    public String join(Tuple2<String, Integer> first, Tuple3<String, Integer, Integer> second) throws Exception {
                        return first + "<------------>" + second;
                    }
                });

                2)间隔联结 (Interval Join)

                                这里需要注意,做间隔联结的两条流A和B,也必须基于相同的key,所以要先进行KeyBy分组;下界lowerBound应该小于等于上界upperBound,两者都可正可负;间隔联结目前只支持事件时间语义

                     (1)正常使用                   

        data_afterKeyBy1.intervalJoin(data_afterKeyBy2)
//                TODO:设置下上界的偏移量  [先下后上的设置]
                .between(Time.seconds(-3), Time.seconds(3))
                .process(new ProcessJoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>() {
                    /**
                     *两条流的数据匹配上才会调用这个方法
                     * @param left ks1 的数据
                     * @param right ks2 的数据
                     * @param ctx 上下文
                     * @param out 采集器
                     * @throws Exception
                     */
                    @Override
                    public void processElement(Tuple2<String, Integer> left, Tuple3<String, Integer, Integer> right, ProcessJoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
//                                关联上的数据才能进入这个方法是
                        out.collect(left + "<------->" + right);
                    }
                })

                     (2) 处理迟到数据

 /**
         * TODO Interval join  处理迟到数据
         * 1、只支持事件时间
         * 2、指定上界、下界的偏移,负号代表时间往前,正号代表时间往后
         * 3、process中,只能处理 join上的数据
         * 4、两条流关联后的watermark,以两条流中最小的为准
         * 5、如果 当前数据的事件时间 < 当前的watermark,就是迟到数据, 主流的process不处理
         *  => between后,可以指定将 左流 或 右流 的迟到数据 放入侧输出流
         */


        OutputTag<Tuple2<String, Integer>> ks1Late = new OutputTag<>("ks1_late", Types.TUPLE(Types.STRING, Types.INT));
        OutputTag<Tuple3<String, Integer,Integer>> ks2Late = new OutputTag<>("ks1_late", Types.TUPLE(Types.STRING, Types.INT,Types.INT));

        SingleOutputStreamOperator<String> process = key1.intervalJoin(key2)
//                TODO:设置下上界的偏移量  [先下后上的设置]
                .between(Time.seconds(-3), Time.seconds(3))

                
//                TODO:处理左侧迟到数据到侧输出流
                .sideOutputLeftLateData(ks1Late)
//                TODO:处理右侧迟到数据到侧输出流
                .sideOutputRightLateData(ks2Late)
                
                
                .process(new ProcessJoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>() {
                    /**
                     *两条流的数据匹配上才会调用这个方法
                     * @param left ks1 的数据
                     * @param right ks2 的数据
                     * @param ctx 上下文
                     * @param out 采集器
                     * @throws Exception
                     */
                    @Override
                    public void processElement(Tuple2<String, Integer> left, Tuple3<String, Integer, Integer> right, ProcessJoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
//                                进入这个方法是关联上的数据
                        out.collect(left + "<------->" + right);
                    }
                });


        process.print("主流打印:");
        process.getSideOutput(ks1Late).printToErr("ks1迟到数据以错误日志方式打印:");
        process.getSideOutput(ks2Late).printToErr("ks2迟到数据以错误日志方式打印:");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值