Java项目中时间窗口及其应用场景

后端 潘老师 5个月前 (12-04) 162 ℃ (0) 扫码查看

本文主要讲解关于Java项目中时间窗口及其应用场景相关内容,让我们来一起学习下吧!

一. 前言

时间窗口在限流 ,分布式 ID 的生成方面都有很多应用,这一篇主要目的是弄清楚怎么最好的实现这个功能。

时间窗口的应用很多 : 可以用于统计和监控,也可以用于限流和流量控制,或者在指定窗口里面做实时计算,ID 生成等业务处理等

二. 原理

时间窗口其实由 窗口滑动机制 两个部分组成。窗口只是一个规约的虚拟容器,滑动机制才是整个过程的核心。

  • 窗口 :要把连续的业务切分成多大的窗口
  • 滑动机制 :窗口如何平滑的来到下一个窗口

从这个思路继续往下看,时间窗口可以分为以下几种 :

2.1 翻滚时间窗口

  • 特点 : 只会处理当前时间窗口的事情,当前处理完成后抛弃当前数据处理以下各时间周期的事情,时间周期是翻滚的形式进行跳动的

例如 : 第一个窗口时 1-5 秒 ,第二个窗口为 6-10 秒

2.2 滑动时间窗口

  • 特点 : 以滑动的形式递进,上一个时间窗口的部分数据在下一个时间窗口会被统计

例如 : 第一个时间窗口为 1-5 秒, 第二个时间窗口为 2-6 秒

总结

这两者对数据处理的形式是不同的,在实现上也会有所不同 :

滚动式的时间窗口通常把数据存放在窗口对象中 , 每一次滚动后直接把之前的窗口对象抛弃即可 , 比较使用时间戳进行大小比较就行。 所以通常实现是:

  • 一个数组管理所有的时间窗口(时间窗口没有更细粒度
  • 该时间窗口对象记录当前时间窗口的各项信息。

滑动式的时间窗口就会稍微麻烦一点,他要 先把数据按照更细小的时间段进行统计然后在更大的时间范围内汇总(方式一 : 样本式),或者把数据以时间为索引进行排序记录(方式二 :实时统计 )。但是总体来说 , 数据和当前时间窗口是分开的。

  • 一个数组记录数据,每个索引(点位)对应一个极小的时间粒度
  • 一个时间窗口对象记录当前时间窗口的开始时间,通过开始时间和结束时间从数组中统计数据

所以说,滑动式的时间窗口更加精密,但是实现和算力要求的更高。 在滑动式里面 ,使用样本窗口可以避免反复计算,但是不适用扩展更复杂的窗口形式 (会话窗口

三. 一些框架的应用

3.1 Sentinel 怎么实现滑动时间窗口的

Sentinel 实现时间窗口最核心的类为 LeapArray 。 该类会把一段时间内划分出若干个小的时间片 ,然后在这个时间片内进行数据统计。

// S1 : 定义时间窗口对象 WindowWrap
public class WindowWrap<T> {
    // ------当前窗口的时间长度
    private final long windowLengthInMs;
    // ------当前窗口的开始时间
    private long windowStart;
    // ------当前窗口内的数据
    private T value;
}


// S2 : 在 LeapArray 中通过一个数组管理时间窗口
private final ReentrantLock updateLock = new ReentrantLock();
- 该数组在构造器中初始化大小 this.array = new AtomicReferenceArray<>(sampleCount);


// S3 : 如何判断当前的时间窗口
public WindowWrap<T> currentWindow(long timeMillis) {
    // 伪代码 : 通过取模上面的数组拿到对应的时间窗口
    int idx = timeId % array.length()
    // 拿到当前时间窗口的起始时间
    long windowStart = calculateWindowStart(timeMillis);

    while (true) {
        WindowWrap<T> old = array.get(idx);
        // 👉 1. 如果没有则创建新的窗口
        if (old == null) {
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
           //  ❓ : CAS 比较 , 避免多线程并发的问题
           if (array.compareAndSet(idx, null, window)) {
                return window;
            }
        } else if (windowStart == old.windowStart()) {
            return old;
        } else if (windowStart > old.windowStart()) {
             // 👉 2 :  如果当前开始时间大于旧的时间窗口时间,说明已经到了下一轮时间窗口
             // ❓ : 因为数组是恒定的 ,所以一轮过后需要覆盖
            if (updateLock.tryLock()) {
                try {
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } 
        } else if (windowStart < old.windowStart()) {
            //  👉 2 : 说明出现了一次,不应该走这里
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

可以说这个滚动机制很简单了,就单纯的时间戳比较,核心的点在于 : 并发的控制和数组的设计

这是 Sentinel 最细粒度的窗口实现代码,基于这套代码既可以实现滚动的,也可以实现滑动的。两者的区别在于 WindowWrap 的范围设置多大

3.3 flink 里面的时间窗口

除了做限流,时间窗口第二大用处就是做规定时间内的数据统计,以 Flink 为例:

Flink 中就有3种时间窗口方式,基于相同时间间隔的有 : 滚动时间窗口和滑动时间窗口 。而基于不同时间间隔的有 会话窗口(时间周期在一个会话之中)。

对于 会话窗口 ,其窗口的时间间隔是变动的 , 时间开始时间也不是确定的。

对于会话窗口的实现和滑动时间窗口大差不差,都是数据和窗口分离开。

四. 扩展

窗口的滚动不止在时间窗口上有体现,基于上述的代码我们能了解到,窗口的滚动无非就 比较 + 替换

最终归咎到就是数组的索引,那么不是时间戳也无所谓了。 那么类似的窗口又有哪些呢?

4.1 Redis 限流的滑动窗口

Redis 限流实现时间窗口的方式也不少,常见的方案有通过 ZSet 实现的,用过 ZSet 应该了解,这个数据类型内部会按照时间戳大小进行排序。所以也可以用来实现延时队列。

数据集合 + 时间戳排序 = 时间窗口 。 妥妥的

4.2 分布式 ID 的滑动窗口

这里聊的就是百度的分布式ID算法了,也贼有意思。

分布式ID里面有一个很重要的点在于如何避免ID在并发里面重复。百度通过一个 RingBuffer 实现。

  • RingBuffer 的使用,不实时计算并发ID,预生成多个分布式ID进行保存
  • 也就是说一个在前面生产ID,一个在后面消费ID,像个窗口一样滚动

总结

以上就是关于Java项目中时间窗口及其应用场景相关的全部内容,希望对你有帮助。欢迎持续关注潘子夜个人博客(www.panziye.com),学习愉快哦!


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/12076.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】