👈👈👈 欢迎点赞收藏关注哟
首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164...
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca...
一. 前言
时间窗口在限流 ,分布式 ID 的生成方面都有很多应用,这一篇主要目的是弄清楚怎么最好的实现这个功能。
时间窗口的应用很多 : 可以用于统计和监控,也可以用于限流和流量控制,或者在指定窗口里面做实时计算,ID 生成等业务处理等
二. 原理
时间窗口其实由 窗口 和 滑动机制 两个部分组成。窗口只是一个规约的虚拟容器,滑动机制才是整个过程的核心。
- 窗口 :要把连续的业务切分成多大的窗口
- 滑动机制 :窗口如何平滑的来到下一个窗口
从这个思路继续往下看,时间窗口可以分为以下几种 :
2.1 翻滚时间窗口
- 特点 : 只会处理当前时间窗口的事情,当前处理完成后抛弃当前数据处理以下各时间周期的事情,时间周期是翻滚的形式进行跳动的
例如 : 第一个窗口时 1-5 秒 ,第二个窗口为 6-10 秒
2.2 滑动时间窗口
- 特点 : 以滑动的形式递进,上一个时间窗口的部分数据在下一个时间窗口会被统计
例如 : 第一个时间窗口为 1-5 秒, 第二个时间窗口为 2-6 秒
总结 :
这两者对数据处理的形式是不同的,在实现上也会有所不同 :
滚动式的时间窗口通常把数据存放在窗口对象中 , 每一次滚动后直接把之前的窗口对象抛弃即可 , 比较使用时间戳进行大小比较就行。 所以通常实现是:
- 一个数组管理所有的时间窗口(
时间窗口没有更细粒度
) - 该时间窗口对象记录当前时间窗口的各项信息。
而滑动式的时间窗口 就会稍微麻烦一点,他要 先把数据按照更细小的时间段进行统计 ,然后在更大的时间范围内汇总 (方式一 : 样本式
),或者把数据以时间为索引进行排序记录(方式二 :实时统计
)。但是总体来说 , 数据和当前时间窗口是分开的。
- 一个数组记录数据,每个索引(点位)对应一个极小的时间粒度
- 一个时间窗口对象记录当前时间窗口的开始时间,通过开始时间和结束时间从数组中统计数据
所以说,滑动式的时间窗口更加精密,但是实现和算力要求的更高。 在滑动式里面 ,使用样本窗口可以避免反复计算,但是不适用扩展更复杂的窗口形式 (会话窗口
)
三. 一些框架的应用
3.1 Sentinel 怎么实现滑动时间窗口的
Sentinel 实现时间窗口最核心的类为 LeapArray
。 该类会把一段时间内划分出若干个小的时间片 ,然后在这个时间片内进行数据统计。
java
// 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,像个窗口一样滚动
总结
小小看了一下,不算很深入,各位看官见谅~~~