【深入浅出flink】第8篇:一篇文章搞懂flink window中的所有内容,保证你完全明白window的分类、window的组成以及window API

【深入浅出flink】第8篇:一篇文章搞懂flink window中的所有内容,保证你完全明白window的分类、window的组成以及window API,第1张

【深入浅出flink】第8篇:一篇文章搞懂flink window中的所有内容,保证你完全明白window的分类、window的组成以及window API

大家好,我是雷恩Layne,这是《深入浅出flink》系列的第八篇文章,我旨在用最直白的语言写好flink,希望能让所有看到的人一目了然。如果大家喜欢,欢迎点赞、关注,也欢迎留言,共同交流flink的点点滴滴 O(∩_∩)O

文章目录

一、窗口的分类

1.1 滚动窗口1.2 滑动窗口1.3 会话窗口1.4 全局窗口1.5 Keyd 和 NonKeyed窗口1.6 自定义窗口 二、窗口的组成

2.1 重分区2.2 开窗 *** 作2.3 计算 *** 作2.4 可选 *** 作 三、Window API(重点)

3.1 window3.2 Window Reduce3.3 Window Aggregate3.4 Window Apply3.5 Window process3.6 windowAll3.7 window trigger3.8 window evictors

在流式计算中,数据持续不断的流入计算引擎,需要一个窗口限定计算范围,比如监控场景的近2分钟或者精准计算的每隔2分钟计算一次,而窗口(window)定义了该范围,将无界流切割成一系列有界的数据集。

所谓窗口(window),就是把一个无限的stream拆分成有限大小的buckets桶,我们可以在这些桶上做计算 *** 作。

一、窗口的分类

Flink支持多种窗口类型,按照驱动类型分为:时间驱动的Time Window(如每30秒钟)和数据驱动的Count Window(如每100个事件)。按照窗口的滚动方式又可以分成:滚动窗口(Tumbling Window,无重叠),滑动窗口(Sliding Window,有重叠)和会话窗口(Session Window,活动间隙)。按照当前流有没有通过keyBy分区,分为Keyed Window和Non Keyed Window。另外,还有全局窗口(global windows)和自定义出窗口(UDF window)。正是这些窗口组成了flink中的窗口计算。

1.1 滚动窗口

滚动窗口(Tumbling Window)按固定的时间段或长度(比如小时或元素个数)来分片stream中的元素。滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。

滚动窗口如下图所示,window size是窗口大小,可以是时间,也可以是元素个数。

滚动窗口特点:时间对齐,窗口长度固定,没有重叠。

滚动窗口分为两类:

滚动窗口按固定的时间段(如每30秒钟)来分片stream中的元素,被称作滚动时间窗口(Tumbling Time Window)滚动窗口按固定的长度或个数(如每100个事件)来分片stream中的元素,被称作滚动计数窗口(Tumbling Count Window) 1.2 滑动窗口

滑动窗口(Sliding Window)是滚动窗口的更广义的一种形式,滑动窗口由固定的窗口大小和滑动间隔组成(比如以小时作为窗口大小,分钟作为滑动周期)。如果滑动周期小于窗口大小,那么窗口会发生部分重叠,在这种情况下元素会被分配到多个窗口中。而如果滑动周期跟窗口大小相等,则该窗口就是滚动窗口。

滚动窗口如下图所示,window size是窗口大小,window slide是滑动步长。

滑动窗口特点:时间对齐,窗口长度固定,可以有重叠。

滑动窗口分为两类:

窗口大小和滑动间隔都是时间段(比如窗口大小是1min,滑动间隔是10s)的滑动窗口,被称作滑动时间窗口(Sliding Time Window)窗口大小和滑动间隔都是长度或个数(比如窗口大小是60个元素,滑动间隔是10个元素)的滑动窗口,被称作滑动计数窗口(Sliding Count Window)

适用场景:对最近一个时间段内的统计,比如按某接口最近5min的失败率来决定是否要报警。

滑动窗口有重叠说明一个数据元素可以同时属于多个窗口的,一条数据最多可以属于多少个窗口呢?计算公式是:窗口长度/滑动间隔,比如窗口长度为1个hour,滑动间隔为5min,则一条数据最多属于12个窗口。

1.3 会话窗口

会话窗口(Session Window)会定义一个session间隔(session gap),类似于web应用的session,这个session间隔定义了非活跃周期的长度,当事件之间发生的时间间隔小于session间隔,那么它们归属于同一个会话。当事件之间发生的时间间隔大于session间隔,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。

会话窗口常用于用户行为分析,即观察在一个会话窗口内用户的一系列 *** 作所产生的事件。会话窗口如下图所示:

会话窗口特点:时间无对齐。session间隔(session gap)必须基于时间,没有基于个数的会话窗口。

1.4 全局窗口

全局窗口会将所有的元素分配到同一个窗口中,其通常配合触发器 (trigger) 进行使用。如果没有相应触发器,则计算将不会被执行。

1.5 Keyd 和 NonKeyed窗口

通常来说,按照当前流有没有通过keyBy分区,分为Keyed Window和Non Keyed Window。

要判定是否是Keyed Window 或 Non Keyed Window,第一件事需要明确的是你的stream需要keyBy分区或者不需要,这个必须要窗口定义前确定。使用keyBy将会把你的无尽的stream切割成逻辑的keyed stream,在此基础上进行window *** 作就是Keyed Window。如果一个stram没有被keyBy,那么进行的window *** 作是Non Keyed Window。

在已经keyed stream中,你写进来的事件任意属性可以使用key。由于使用了keyed stream可以允许你的windowd 计算在并行的多任务的模式下运行,每一个逻辑的keyed stream可以独立的运行而相互没有影响,所有具有相同key的元素会被发射到相同的并行任务上执行。

如果在non-keyed streams中,你原有的stream不会分割成不同的逻辑stream,并且所有的window逻辑只会执行在一个单独的任务上使用(也就说所有的数据会汇总到一个task上执行,任务并发度为1)。

1.6 自定义窗口

⼀般前⾯window就能解决我们所遇到的业务场景了,如果不能解决,我们也可以自定义window,不过本⼈⾄今还没遇到需要⾃定义window的场景。

二、窗口的组成

本小节直介绍窗口由哪些部分组成,具体的window api *** 作在第三节实现。所以,遇到不理解的别慌,看一下第三节对应的实 *** 就都明白了。

窗口计算一般分三步:重分区(使用keyBy)、开窗 *** 作、计算 *** 作。

2.1 重分区

重分区 *** 作:DataStream -> KeyedStream。

在window api的 *** 作中,只能用keyBy进行重分区,不能使用其它重分区算子。因为只有keyBy能将DataStream转化为KeyedStream外,其它重分区算子均不会改变Stream的类型。而开窗的所有 *** 作都是在KeyedStream基础上完成。

当然,重分区也是可选的 *** 作,当使用keyBy重分区时,后面必须用window方法开窗,当不使用重分区时,后面必须用windowAll方法开窗。

2.2 开窗 *** 作

开窗 *** 作:DataStream -> WindowedStream 或 AllWindowedStream。

在重分区方式下,通过DataStream 的.window()方法 来定义一个窗口,然后在window中传入窗口分配器(WindowAssigner)进行开窗。窗口分配器(WindowAssigner)负责将每一个到来的元素分配给一个或者多个窗口,Flink提供了一些常用的预定义窗口分配器,常见的有:

SlidingProcessingTimeWindows(滑动时间窗口)SlidingEventTimeWindowsTumblingEventTimeWindows(滚动时间窗口)TumblingProcessingTimeWindowsMergingWindowAssignerbaseAlignedWindowAssignerGlobalWindows(全局窗口)

ProcessingTime和EventTime是时间语义的概念,现在先不用管,后面我会单独写一篇文章来介绍

如果Flink预定义的窗口分配器不能满足需求,我们也可以通过继承WindowAssigner类来自定义窗口分配器。

在不使用重分区时,通过.windowAll()方法定义窗口,然后传入上述窗口分配器进行开窗 *** 作。

另外,也可以不直接.window()或.windowAll()定义窗口,可直接使用更加简单的 .timeWindow 和 .countWindow 方法来定义窗口和进行开窗 *** 作,它们其实内部也是通过.window()或.windowAll()传入窗口分配器实现的。

2.3 计算 *** 作

计算 *** 作:WindowedStream 或 AllWindowedStream -> DataStream。

进行开窗 *** 作之后,就是在窗口之上进行相应的计算 *** 作了。window function 定义了要对窗口中收集的数据做的计算 *** 作,主要可以分为两类:

增量聚合函数(incremental aggregation functions):每条数据到来就进行计算,保持一个简单的状态,也就是说,来一个数据计算一个,中间不进行输出,等计算完窗口中的最后一条数据再把结果输出。典型的增量聚合函数有ReduceFunction(通过reduce方法传入)、AggregateFunction(通过aggregate方法传入)。另外,sum、max、min也属于增量聚合函数。全窗口函数(full window functions):先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。ProcessWindowFunction(通过process方法传入)、WindowFuncition(通过apply方法传入)就是全窗口函数。

全窗口函数ProcessWindowFunction比WindowFunction多了一些窗口的上下文信息,除此之外功能相同。

增量聚合函数相比于全窗口函数占用的资源更少,执行速度也更快。至于用哪种函数执行计算 *** 作,看具体的应用场景。比如,我们统计个数,可以来一个统计一个,优先选用增量聚合函数,而如果求平均值,就不能来一个计算一个了,必须收集到所有的元素再进行计算,所以只能选择全窗口函数。

2.4 可选 *** 作

除此之外,可有一些可选(optional)的 *** 作,它们虽然不是必须的,但是在窗口计算中扮演重要的角色。

每一个window都可以设置trigger(触发器)和 evitor(移除器),还可以迟到数据进行处理,对应的方法分别如下:

.trigger()——触发器:定义窗口什么时候关闭,触发计算并输出结果。.evitor()——移除器:定义某些数据的移除逻辑。这个驱逐器可以在触发器触发之前或者之后,或者窗口函数被应用之前清理窗口中的元素。.allowedLateness():运行处理迟到的数据。比如设置2min,这样窗口时间截止以后先输出一个近似的结果,然后在2min内来一个数据更新一次结果,等待时间2min到了之后,关闭窗口。allowedLateness在event-time windows(时间语义窗口)下才有效.sideOutputLateData():将allowedLateness范围之外的迟到的数据放入侧输出流,侧输出流可以接收所有后来所有迟到的数据,它收集元素的开始时间是allowedLateness的截止时间,它没有结束时间。.getSideOutput():获取侧输出流

是不是有些懵,哈哈,别着急,第三节实 *** window api才是重点,看了实践之后,你会一目了然。

总的来说,一个stream完整的window有如下方法组成:

(1)Keyed Windows

stream
	.keyBy(...)  <- keyed versus non-keyed windows
	.window(...)  <- required: "assigner"
	[.trigger(...)]  <- optional: "trigger"(else default trigger)
	[.evictor(...)]  <- optional: "evictor"(else no evictor)
	[.allowedLateness(...)]  <- optional: "lateness"(else zero)
	[.sideOutputLateData(...)] <- optional: "output tag"(else no side output for late data)
	.reduce/aggregate/process/apply()  <- required: "function"
	[.getSideOutput(...)]  <- optional: "output tag"

后面带有optional的都是可选 *** 作,剩下的就是keyBy分区,window开窗,reduce/aggregate/process/apply是计算 *** 作。所以说开窗 *** 作和计算 *** 作是必须的,而其它的是可选的,根据实际需要来定义。

(2)Non-Keyed Windows

stream
	.windowAll(...)  <- required: "assigner"
	[.trigger(...)]  <- optional: "trigger"(else default trigger)
	[.evictor(...)]  <- optional: "evictor"(else no evictor)
	[.allowedLateness(...)]  <- optional: "lateness"(else zero)
	[.sideOutputLateData(...)] <- optional: "output tag"(else no side output for late data)
	.reduce/aggregate/process/apply()  <- required: "function"
	[.getSideOutput(...)]  <- optional: "output tag"

需要注意的是,如果在non-keyed streams中,你原有的stream不会分割成不同的逻辑stream,并且所有的窗口逻辑只会执行在一个单独的任务上使用,也就说所有的数据会汇总到一个task上执行,任务并发度为1。

三、Window API(重点) 3.1 window

window:KeyedStream -> WindowedStream(开窗 *** 作)。Windows 可以在已经分区的 KeyedStream 上定义。Windows 根据某些特征(例如,在最近5秒内到达的数据)对每个Keys中的数据进行分组。

在window方法中传入是窗口分配器(WindowAssigner),根据不同的窗口分配器可以实现不同类型的窗口

(1)window实现时间窗口

时间窗口(TimeWindow)是将指定时间范围内的所有数据组成一个window,一次对一个window里面的所有数据进行计算。

时间窗口的实现有两种方式:

在window中窗口分配器实现直接使用 .timeWindow实现

这两种方式内部原理一样,也就是说,.timeWindow内部也是基于窗口分配器(WindowAssigner)实现的。时间窗口分配器有四种:

SlidingProcessingTimeWindows(滑动时间窗口)SlidingEventTimeWindowsTumblingEventTimeWindows(滚动时间窗口)TumblingProcessingTimeWindows

实质上只有两种,只是时间语义不同,其它完全一样。Flink默认的时间窗口根据Processing Time语义 进行窗口的划分。

示例1:通过窗口分配器实现窗口大小为10s滚动时间窗口

//通过nc -lk 7777 发送数据
DataStream dataStream = env.socketTextStream("localhost", 7777);
KeyedStream keyedStream = dataStream.keyBy(value -> value);
//window必须在已经分区的 KeyedStream 开窗
//WindowedStream有三个泛型:第一个是stream中的数据类型,第二个是key的类型,第三个是窗口类型
WindowedStream windowStream = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//在窗口上执行计算:求出10s内字典顺序最小的value
DataStream minDataStream = windowStream.min(0);
minDataStream.print();

示例2:通过.timeWindow实现窗口大小为10s滚动时间窗口

KeyedStream keyedStream = dataStream.keyBy(value -> value);
//window必须在已经分区的 KeyedStream 开窗
WindowedStream windowStream = keyedStream.timeWindow(Time.seconds(10));
//在窗口上执行计算:求出10s内字典顺序最小的value
DataStream minDataStream = windowStream.min(0);

示例3:通过窗口分配器实现窗口大小为10s、滑动步长为2s的滑动时间窗口

KeyedStream keyedStream = dataStream.keyBy(value -> value);
WindowedStream windowStream = keyedStream.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(2)));
DataStream minDataStream = windowStream.min(0);

示例4:通过.timeWindow实现窗口大小为10s、滑动步长为2s的滑动时间窗口

KeyedStream keyedStream = dataStream.keyBy(value -> value);
WindowedStream windowStream = keyedStream.timeWindow(Time.seconds(10),Time.seconds(2));
DataStream minDataStream = windowStream.min(0);

(2)window实现计数窗口

计数窗口(CountWindow)根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。

注意:CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数。

计数窗口的实现有两种方式:

在window中窗口分配器实现直接使用 .countWindow实现

同理,.countWindow内部也是基于窗口分配器(WindowAssigner)实现的。计数窗口的分配器比较复杂,它是通过GlobalWindow窗口分配器结合trigger(触发器)实现的。

CountWindow没有时间语义的概念,只有TimeCount有,这点要注意。

下面只举例.countWindow实现的例子,窗口分配器的实现我们到windowAll那里再举例。

示例5:通过.countWindow实现窗口长度为15的滚动计数窗口

KeyedStream keyedStream = dataStream.keyBy(value -> value);
//可以看到,窗口类型为GlobalWindow
WindowedStream windowStream = keyedStream.countWindow(15);
DataStream minDataStream = windowStream.min(0);

示例6:通过.countWindow实现窗口长度为15,滑动步长为2的滑动计数窗口

KeyedStream keyedStream = dataStream.keyBy(value -> value);
WindowedStream windowStream = keyedStream.countWindow(15,2);
DataStream minDataStream = windowStream.min(0);
3.2 Window Reduce

window reduce :WindowedStream -> DataStream(计算 *** 作)。Reduce方法属于计算 *** 作中的增量聚合函数,每条数据到来就进行计算,保持一个简单的状态,也就是说,来一个数据计算一个,中间不进行输出,等计算完窗口中的最后一条数据再把结果输出。

示例:用window reduce实现wordcount

(1)新建一个WordCount类

public class WordCount {
    private String word;
    private Long count;

    //必须有空参的构造方法
    public WordCount() {
    }

    public WordCount(String word, Long count) {
        this.word = word;
        this.count = count;
    }

    public String getWord() {
        return word;
    }

    public void setWord(String word) {
        this.word = word;
    }

    public Long getCount() {
        return count;
    }

    public void setCount(Long count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return "WordCount{" +
                "word='" + word + ''' +
                ", count=" + count +
                '}';
    }
}

(2)实现wordcount

DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(word -> new WordCount(word,1L));
//按照WordCount对象的word属性分区
KeyedStream keyedStream = mapDataStream.keyBy(wc -> wc.getWord());
//窗口大小为10s滚动时间窗口
WindowedStream windowStream = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//reduce实现wordCount
DataStream reduceDataStream = windowStream.reduce(new ReduceFunction() {
	@Override
	//wc1是以前reduce后的数据,wc2是当前来的最新的一条数据
	public WordCount reduce(WordCount wc1, WordCount wc2) throws Exception {
		wc1.setCount(wc1.getCount()+wc2.getCount());
		return wc1;
	}
});
reduceDataStream.print();
3.3 Window Aggregate

window aggregate :WindowedStream -> DataStream(计算 *** 作)。 aggregate方法属于计算 *** 作中的增量聚合函数。和window reduce一样,都可以做增量聚合,不同的是,aggregate必须有一个初值,这个初值就是累加器(accumulator),所有数据都是通过累加器聚合的。

示例:用window aggregate实现wordcount

DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(word -> new WordCount(word,1L));
//按照WordCount对象的word属性分区(keyed分区后对相同hash值的元素做计算)
KeyedStream keyedStream = mapDataStream.keyBy(wc -> wc.getWord());
//窗口大小为10s滚动时间窗口
WindowedStream windowStream = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
// aggregate实现wordCount

DataStream aggregateDataStream = windowStream.aggregate(new AggregateFunction() {
	@Override
	public WordCount createAccumulator() {
		return new WordCount("", 0L);
	}

	@Override
	public WordCount add(WordCount value, WordCount accumulator) {//将新到来的数据放入累加器中
		//由于是对相同hash值的元素做计算,所以word是相同的,只需放入一次就行
		if(accumulator.getWord().equals("")) accumulator.setWord(value.getWord());
		accumulator.setCount(accumulator.getCount() + value.getCount());
		return accumulator;
	}

	@Override
	public WordCount getResult(WordCount accumulator) {//返回结果
		return accumulator;
	}

	@Override
	public WordCount merge(WordCount a, WordCount b) {//合并两个数据
		a.setCount(a.getCount() + b.getCount());
		return a;
	}
});

aggregateDataStream.print();
3.4 Window Apply

window apply :WindowedStream -> DataStream 或 AllWindowedStream -> DataStream(计算 *** 作)。apply方法属于计算 *** 作中的全窗口函数,先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。

示例:用window apply实现wordcount

DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(word -> new WordCount(word,1L));
//按照WordCount对象的word属性分区(keyed分区后对相同hash值的元素做计算)
KeyedStream keyedStream = mapDataStream.keyBy(wc -> wc.getWord());
//窗口大小为10s滚动时间窗口
WindowedStream windowStream = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
// apply实现wordCount

DataStream applyDataStream = windowStream.apply(new WindowFunction() {
	@Override
	//s代表key,这里就是word
	public void apply(String s, TimeWindow window, Iterable input, Collector out) throws Exception {
		Long count = 0L;
		Iterator iterator = input.iterator();
		while (iterator.hasNext()) {
			count += iterator.next().getCount();
		}
		out.collect(new WordCount(s, count));
	}
});

applyDataStream.print();
env.execute();
3.5 Window process

window process :WindowedStream -> DataStream 或 AllWindowedStream -> DataStream(计算 *** 作)。process方法属于计算 *** 作中的全窗口函数,它和apply几乎一样,唯一区别在于process中的ProcessWindowFunction多了一些上下文信息。

示例:用window process实现wordcount

DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(word -> new WordCount(word,1L));
//按照WordCount对象的word属性分区(keyed分区后对相同hash值的元素做计算)
KeyedStream keyedStream = mapDataStream.keyBy(wc -> wc.getWord());
//窗口大小为10s滚动时间窗口
WindowedStream windowStream = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//process实现wordCount

DataStream processDataStream = windowStream.process(new ProcessWindowFunction() {
	@Override
	public void process(String s, Context context, Iterable elements, Collector out) throws Exception {
		Long count = 0L;
		Iterator iterator = elements.iterator();
		while (iterator.hasNext()) {
			count += iterator.next().getCount();
		}
		out.collect(new WordCount(s, count));
	}
});

processDataStream.print();
env.execute();
3.6 windowAll

WindowAll :DataStream -> AllWindowedStream。如果是Non-keyed类型,则调用windowAll方法,所有的数据都会在窗口算子中由一个Task计算,并得到全局统计结果。也就是说WindowAll并行度只能1,且不可设置并行度。

WindowAll中传入的也是窗口分配器(WindowAssigner),前面提到的任意一种窗口分配器都可以在windowall中使用。

示例1:通过windowAll开窗,求每10s最小的值

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(value -> Integer.parseInt(value));

//通过windowAll开窗:定义一个大小为10s的滚动时间窗口
AllWindowedStream allWindowedStream = mapDataStream.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)));
DataStream minDataStream = allWindowedStream.min(0);
System.out.println("windowAll开窗前算子并行度:"+mapDataStream.getParallelism());
System.out.println("windowAll开窗后算子并行度:"+minDataStream.getParallelism());
minDataStream.print();
env.execute();

输出:

windowAll开窗前算子并行度:3
windowAll开窗后算子并行度:1

示例2:通过timeWindowAll开窗,求每10s最小的值

DataStream dataStream = env.socketTextStream("localhost", 7777);
DataStream mapDataStream = dataStream.map(value -> Integer.parseInt(value));

AllWindowedStream allWindowedStream = mapDataStream.timeWindowAll(Time.seconds(10));
DataStream minDataStream = allWindowedStream.min(0);

timeWindowAll内部就是用的windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10))),所以这两者代表的含义一样。

3.7 window trigger

Trigger决定了什么时候窗口准备就绪了,⼀旦窗口准备就绪就可以使用WindowFunction(窗口计算 *** 作)进行计算。每⼀个 WindowAssigner(窗口分配器) 都会有⼀个默认的Trigger。如果默认的Trigger不满足用户的需求用户可以⾃定义Trigger。

由于flink提供了很多种window trigger,所以我打算后面专门写一篇博客来介绍,敬请期待。

3.8 window evictors

Evictors(移除器)定义某些数据的移除逻辑,Evictors可以在触发器触发后,应用Window Function之前或之后从窗口中删除元素。

Flink DataStream Window内置了三种剔除器: CountEvictor、DeltaEvictor、TimeEvictor。

CountEvictor: 数量剔除器。在Window中保留指定数量的元素,并从窗口头部开始丢弃其余元素。DeltaEvictor: 阈值剔除器。计算Window中最后一个元素与其余每个元素之间的增量,丢弃增量大于或等于阈值的元素。TimeEvictor: 时间剔除器。保留Window中最近一段时间内的元素,并丢弃其余元素。

如果一直写下去文章篇幅会特别长,后续我也专门写一篇博客来介绍。

都看到这里了,麻烦各种观众点个赞再走吧!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5705380.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存