【FLink-12-Flink相关概念-allowLateness机制&窗口分类&Trigger&Evictor】

窗口计算 API

使用事件时间机制的窗口之前,必须要在前置算子中生成waterMark,然后窗口才起作用,不然窗口设置了无效,没有触发机制。处理时间机制的窗口,可以不关注。

1.窗口(Window)概念

窗口,就是把无界的数据流,依据一定规则划分成一段一段的有界数据流来计算; 既然划分成有界数据段,通常都是为了"聚合";

在这里插入图片描述

滚动窗口 等价于 滑动窗口的一个特例(窗口长度=滑动步长)

窗口的分类维度:
1.按时间维度划分 :滚动窗口、滑动窗口、回话session窗口
2.按照数据条数划分:滚动窗口、滑动窗口
3.按照上游算子是否为keyedStream类型算子划分:NonKeyedWindow 和 KeyedWindow。
Keyedwindow 重要特性:任何一个窗口,都绑定在自己所属的 key 上;不同 key 的数据肯定不会划分 到相同窗口中去!

2.allowLateness机制

  • flink中有允许迟到机制(allowLateness机制),当一个窗口已经触发完毕后,又来了一条数据,正好在之前刚刚计算过的那个桶里面,这时候如果有允许迟到机制,那么刚刚计算完毕的那个桶还会保留一段时间(这个时间就是迟到机制里面设置的时间),在这个时间里面,此条数据会被被继续放入刚计算完毕的那个桶里面,等到设置的时间完毕后,再触发一次窗口计算,此时的值,就会由3变成4。
  • 具体代码案例如下:
    • 样例数据中最后一条是迟到数据,然后触发完窗口后,又重新触发了一次窗口 在这里插入图片描述
    • 程序运算结果如下,统计结果又3变为4;
      • 允许迟到时间为2秒,这两秒的解释:当数据流中事件时间到达了上一个窗口的最大值+2S之前,也就是waterMark中记录的时间,如果这2秒内一直有数据过来,waterMark还没有更新到容错时间的最大值,不管来多少条数据都会一直触发之前窗口的逻辑进行计算;如果waterMark时间更新到容错时间之后,符合之前窗口时间的迟到数据就不会被触发了。这就是允许迟到的最大容错时间。
      • 需要注意的是:都是以waterMark中记录的时间为标准,来判断容错时间是多少,如果waterMark一直不变,容错时间内的数据会一直被触发。
      • waterMark中设置的容错时间和allowlateness设置的容错时间,对应的窗口触发的频率不同,前者是触发一次窗口,后者来多少条数据,触发多少次窗口。
      • allowedLatenss(2) 的解释:如果waterMark(此刻的事件时间)推进到了A窗口结束点后2秒,如果还来A窗口的数据,就算迟到,就不会再去触发A窗口的计算逻辑,而是输出到迟到的侧流中。

在这里插入图片描述
在这里插入图片描述
- 具体代码如下:

package com.yang.flink.window;

import com.yang.flink.vo.EventBean2;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
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.*;
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 WindowApiAllowedLatenessDemo3 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);

        // 1,e01,3000,pg02
        DataStreamSource<String> source = env.socketTextStream("localhost", 9999);

        SingleOutputStreamOperator<Tuple2<EventBean2,Integer>> beanStream = source.map(s -> {
            String[] split = s.split(",");
            EventBean2 bean = new EventBean2(Long.parseLong(split[0]), split[1], Long.parseLong(split[2]), split[3], Integer.parseInt(split[4]));
            return Tuple2.of(bean,1);
        }).returns(new TypeHint<Tuple2<EventBean2, Integer>>() {})
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<EventBean2,Integer>>forBoundedOutOfOrderness(Duration.ofMillis(0))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<EventBean2,Integer>>() {
                            @Override
                            public long extractTimestamp(Tuple2<EventBean2,Integer> element, long recordTimestamp) {
                                return element.f0.getTimeStamp();
                            }
                        }));


        OutputTag<Tuple2<EventBean2,Integer>> lateDataOutputTag = new OutputTag<>("late_data", TypeInformation.of(new TypeHint<Tuple2<EventBean2, Integer>>() {}));

        SingleOutputStreamOperator<String> sumResult = beanStream.keyBy(tp -> tp.f0.getGuid())
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))  // 事件时间滚动窗口,窗口长度为10
                .allowedLateness(Time.seconds(2))  // 允许迟到2s
                .sideOutputLateData(lateDataOutputTag)  // 迟到超过允许时限的数据,输出到该“outputtag”所标记的测流
                /*.sum("f1")*/
                .apply(new WindowFunction<Tuple2<EventBean2, Integer>, String, Long, TimeWindow>() {
                    @Override
                    public void apply(Long aLong, TimeWindow window, Iterable<Tuple2<EventBean2, Integer>> input, Collector<String> out) throws Exception {
                        int count = 0;
                        for (Tuple2<EventBean2, Integer> eventBean2IntegerTuple2 : input) {
                            count ++;
                        }
                        out.collect(window.getStart()+":"+ window.getEnd()+","+count);
                    }
                });
        DataStream<Tuple2<EventBean2, Integer>> lateDataSideStream = sumResult.getSideOutput(lateDataOutputTag);


        sumResult.print("主流结果");

        lateDataSideStream.print("迟到数据");

        env.execute();

    }
}

3.Flink乱序数据处理相关总结

  • Flink乱序相关总结:
    • 1.小乱序,利用waterMark的容错时间来解决
    • 2.中乱序,利用窗口允许迟到机制 allowLateness机制
    • 3.大乱序,利用窗口中的 迟到数据侧流输出机制 sideOutputLateData
  • 符合怎样条件标准的数据才是输出到“迟到侧流”中?
    • 超过了waterMark和allowLateness的容错机制的数据,都会被丢弃到侧流里面。

4.滚动聚合算子和全窗口聚合算子的区别

窗口聚合算子,整体上分为两类

  • 增量滚动聚合算子,如 min、max、minBy、maxBy、sum、reduce、aggregate
  • 全量聚合算子,如 apply、process
  • 两个聚合算子各有优缺点:
    • 1.增量滚动聚合算子的优点:数据在内存中占用 容量少,只存储最终结果。
    • 2.全量聚合算子的优点:灵活度高,可支持拓展的功能多。
    • 3.两个算子的选择,需要根据用户的需求来进行适当的选择。
      在这里插入图片描述

5.窗口指派的API

  • 滚动聚合函数,在写代码的时候,每次只能拿到一条数据,和当前的累加器数据进行操作。
  • keyedWindow算子,底层逻辑的核心要点:窗口是和自己的Key绑定的,不会把不同的key的数据放到同一个窗口中。
  • apply全窗口算子和process窗口聚合算子的区别,process窗口继承了rich丰富类,里面的方法比apply全窗口算子多一些,与之前process系列的一样。
  • 滚动窗口以及滑动窗口的选择需要根据业务需求来进行选择
  • flink中默认的序列化器,不是用jdk的serializerable,默认使用的avro序列化器。
/**
* NonKeyed 窗口,全局窗口 
*/ 
// 处理时间语义,滚动窗口 
source.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5))); 

// 处理时间语义,滑动窗口
source.windowAll(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(1))); 

// 事件时间语义,滚动窗口 
source.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));

 // 事件时间语义,滑动窗口 
source.windowAll(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(1))); 

 // 计数滚动窗口 
 source.countWindowAll(100); 
 
 // 计数滑动窗口 
 source.countWindowAll(100,20); 
 
 /**
 * Keyed 窗口 
 */ 
 KeyedStream<String, String> keyedStream = source.keyBy(s -> s); 
// 处理时间语义,滚动窗口 
 keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(5))); 

// 处理时间语义,滑动窗口 
keyedStream.window(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(1))); 

// 事件时间语义,滚动窗口 
 keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5))); 

// 事件时间语义,滑动窗口 keyedStream.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(1))); 

// 计数滚动窗口 
 keyedStream.countWindow(1000);
  
// 计数滑动窗口 
 keyedStream.countWindow(1000,100);

6.窗口的触发机制 Trigger

触发的机制:数据达到的时候会去检查,时间事件触发的时候会去检查
在这里插入图片描述

7.Evictor 窗口数据移除机制

  • 窗口触发前,或者触发后,对窗口中的数据移除的机制;

  • 算子在调用Trigger后,发现满足触发条件时:

    • 就会先去调用Evictor的evictBefore方法 来进行移除,
    • 然后进行计算;
    • 计算完成后,还回去再调用Evictor的evictAfter()方法,来进行数据清理。
  • 自定义窗口触发机制和自定义窗口移除机制具体代码案例如下:

package com.yang.flink.window;

import com.yang.flink.vo.EventBean2;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.java.tuple.Tuple2;
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.evictors.Evictor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.triggers.Trigger;
import org.apache.flink.streaming.api.windowing.triggers.TriggerResult;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.runtime.operators.windowing.TimestampedValue;
import org.apache.flink.util.Collector;

import java.time.Duration;
import java.util.Iterator;

/**
 * window窗口API- Triger触发器 和 Evictor窗口移除机制
 * 产品需求:统计最近10S,每个用户出现的次数
 *  除了在正常的10S窗口时间到达时触发窗口,还有另外一个特别的需求:
 *          遇到用户的行为是e0x,马上触发一次窗口计算,
 *          并且每次触发时不要包含其中的e0x这条数据
 *
 * 测试数据
 * 1,e01,10000,p01,10  [10,20)
 * 1,e02,11000,p02,20
 * 1,e02,12000,p03,40
 * 1,e0x,13000,p03,40 ==> 这里会触发一次窗口
 * 1,e04,16000,p05,50
 * 1,e03,20000,p02,10  [20,30)  ==> 时间到达也会触发一次窗口
 */
public class WindowApiTrigerEvictorDemo4 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //获取原始数据
        DataStreamSource<String> streamSource = env.socketTextStream("hadoop102", 9999);
        //将数据样例:1,e01,10000,p01,10,进行拆分后封装为对象
        SingleOutputStreamOperator<Tuple2<EventBean2, Integer>> beanStream = streamSource.map(bean -> {
            String[] arr = bean.split(",");
            EventBean2 eventBean2 = new EventBean2(Long.parseLong(arr[0]), arr[1], Long.parseLong(arr[2]), arr[3], Integer.parseInt(arr[4]));

            return Tuple2.of(eventBean2, 1);
        }).returns(new TypeHint<Tuple2<EventBean2, Integer>>() {
        });
        //给beanStream流,设置waterMark并指定事件时间是哪一个字段
        beanStream.assignTimestampsAndWatermarks(WatermarkStrategy
                .<Tuple2<EventBean2, Integer>>forBoundedOutOfOrderness(Duration.ofMillis(5000))
                .withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<EventBean2, Integer>>() {
                    @Override
                    public long extractTimestamp(Tuple2<EventBean2, Integer> element, long recordTimestamp) {
                        return element.f0.getTimeStamp();
                    }
                })
        );
        //数据按照用户guid进行keyby分组,并开窗计算数据,按照需求选择窗口类型为滚动窗口,开窗时间为10S
        beanStream.keyBy(data ->data.f0.getGuid())
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                //按照需求要求:遇到用户的行为是e0x,马上触发一次窗口计算,设置自定义触发器Trigeer
                .trigger(MyEventTimeTrigger.create())
                //按照需求要求:每次触发时不要包含其中的e0x这条数据,设置自定义移除机制Evictor
                .evictor(MyTimeEvictor.of(Time.seconds(10)))
                //按照需求要求:统计最近10S,每个用户出现的次数,使用全量窗口机制apply,来进行数据聚合
                .apply(new WindowFunction<Tuple2<EventBean2, Integer>, String, Long, TimeWindow>() {
                    @Override
                    public void apply(Long aLong, TimeWindow window, Iterable<Tuple2<EventBean2, Integer>> input, Collector<String> out) throws Exception {
                        int count = 0;
                        for (Tuple2<EventBean2, Integer> eventBean2IntegerTuple2 : input) {
                            count++;

                        }
                        out.collect("window_start:"+window.getStart()+","+ "window_end:"+window.getEnd()+",该用户出现的次数:"+count);

                    }
                });

        env.execute();


    }
}

class MyTimeEvictor implements Evictor<Object, TimeWindow> {
    private static final long serialVersionUID = 1L;

    private final long windowSize;
    private final boolean doEvictAfter;

    public MyTimeEvictor(long windowSize) {
        this.windowSize = windowSize;
        this.doEvictAfter = false;
    }

    public MyTimeEvictor(long windowSize, boolean doEvictAfter) {
        this.windowSize = windowSize;
        this.doEvictAfter = doEvictAfter;
    }

    /**
     * 窗口触发前,调用
     */
    @Override
    public void evictBefore(
            Iterable<TimestampedValue<Object>> elements, int size, TimeWindow window, EvictorContext ctx) {
        if (!doEvictAfter) {
            evict(elements, size, ctx);
        }
    }

    /**
     * 窗口触发后,调用
     */
    @Override
    public void evictAfter(
            Iterable<TimestampedValue<Object>> elements, int size, TimeWindow window, EvictorContext ctx) {
        if (doEvictAfter) {
            evict(elements, size, ctx);
        }
    }

    /**
     * 元素移除的核心逻辑
     */
    private void evict(Iterable<TimestampedValue<Object>> elements, int size, EvictorContext ctx) {
        if (!hasTimestamp(elements)) {
            return;
        }

        long currentTime = getMaxTimestamp(elements);
        long evictCutoff = currentTime - windowSize;

        for (Iterator<TimestampedValue<Object>> iterator = elements.iterator();
             iterator.hasNext(); ) {
            TimestampedValue<Object> record = iterator.next();

            Tuple2<EventBean2,Integer> tuple = (Tuple2<EventBean2, Integer>) record.getValue();

            // 加了一个条件: 数据的eventId=e0x,也移除
            if (record.getTimestamp() <= evictCutoff  || tuple.f0.getEventId().equals("e0x")) {
                iterator.remove();
            }
        }
    }

    private boolean hasTimestamp(Iterable<TimestampedValue<Object>> elements) {
        Iterator<TimestampedValue<Object>> it = elements.iterator();
        if (it.hasNext()) {
            return it.next().hasTimestamp();
        }
        return false;
    }

    /**
     * 用于计算移除的时间截止点
     */
    private long getMaxTimestamp(Iterable<TimestampedValue<Object>> elements) {
        long currentTime = Long.MIN_VALUE;
        for (Iterator<TimestampedValue<Object>> iterator = elements.iterator();
             iterator.hasNext(); ) {
            TimestampedValue<Object> record = iterator.next();
            currentTime = Math.max(currentTime, record.getTimestamp());
        }
        return currentTime;
    }


    public static   MyTimeEvictor  of(Time windowSize) {
        return new MyTimeEvictor(windowSize.toMilliseconds());
    }
}

class  MyEventTimeTrigger extends Trigger<Tuple2<EventBean2, Integer>, TimeWindow>{



    private MyEventTimeTrigger() {}

    /**
     * 来一条数据时,需要检查watermark是否已经越过窗口结束点需要触发
     * 需求:遇到用户的行为是e0x,马上触发一次窗口计算
     */
    @Override
    public TriggerResult onElement(Tuple2<EventBean2, Integer> element, long timestamp, TimeWindow window, TriggerContext ctx) throws Exception {
        // 如果窗口结束点 <= 当前的watermark
        if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
            // if the watermark is already past the window fire immediately
            return TriggerResult.FIRE;
        } else {
            // 注册定时器,定时器的触发时间为: 窗口的结束点时间
            ctx.registerEventTimeTimer(window.maxTimestamp());
            // 判断,当前数据的用户行为事件id是否等于e0x,如是,则触发
            if("e0x".equals(element.f0.getEventId())) return TriggerResult.FIRE;
            return TriggerResult.CONTINUE;
        }
    }

    @Override
    public TriggerResult onEventTime(long time, TimeWindow window, TriggerContext ctx) {
        return time == window.maxTimestamp() ? TriggerResult.FIRE : TriggerResult.CONTINUE;
    }

    @Override
    public TriggerResult onProcessingTime(long time, TimeWindow window, TriggerContext ctx)
            throws Exception {
        return TriggerResult.CONTINUE;
    }

    @Override
    public void clear(TimeWindow window, TriggerContext ctx) throws Exception {
        ctx.deleteEventTimeTimer(window.maxTimestamp());
    }

    @Override
    public boolean canMerge() {
        return true;
    }

    @Override
    public void onMerge(TimeWindow window, OnMergeContext ctx) {
        // only register a timer if the watermark is not yet past the end of the merged window
        // this is in line with the logic in onElement(). If the watermark is past the end of
        // the window onElement() will fire and setting a timer here would fire the window twice.
        long windowMaxTimestamp = window.maxTimestamp();
        if (windowMaxTimestamp > ctx.getCurrentWatermark()) {
            ctx.registerEventTimeTimer(windowMaxTimestamp);
        }
    }

    @Override
    public String toString() {
        return "EventTimeTrigger()";
    }

    /**
     * Creates an event-time trigger that fires once the watermark passes the end of the window.
     *
     * <p>Once the trigger fires all elements are discarded. Elements that arrive late immediately
     * trigger window evaluation with just this one element.
     */
    public static MyEventTimeTrigger create() {
        return new MyEventTimeTrigger();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值