纯Java我们依然可以实现滑动时间窗口限流算法|Java 刷题打卡

2,087 阅读3分钟

本文正在参加「Java主题月 - Java 刷题打卡」,详情查看 活动链接

一、题目描述

最近的请求次数

写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请你实现 RecentCounter 类:

RecentCounter() 初始化计数器,请求数为 0 。 int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。 保证 每次对 ping 的调用都使用比之前更大的 t 值。

示例

输入: ["RecentCounter", "ping", "ping", "ping", "ping"] [[], [1], [100], [3001], [3002]] 输出: [null, 1, 2, 3, 3]

解释: RecentCounter recentCounter = new RecentCounter(); recentCounter.ping(1); // requests = [1],范围是 [-2999,1],返回 1 recentCounter.ping(100); // requests = [1, 100],范围是 [-2900,100],返回 2 recentCounter.ping(3001); // requests = [1, 100, 3001],范围是 [1,3001],返回 3 recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002]

二、思路分析

  • 相信在web开发中我们都有接触过对接口的一种限制,我们统称为限流。我们常见的限流算法有【固定时间窗口算法】、【滑动时间窗口算法】、【漏桶算法】、【令牌桶算法】
  • 此题就是让我们实现一种时间窗口限流算法。如果是网络开发我们可能会使用redis等中间件作为我们流量存储的载体。

  • 但是我们这是算法场景。使用redis这是不现实的。
  • 不考虑redis的情况下,在java中本身就为我们提供了这样的数据结构。想想我们在redis中实现也无非通过redis提供的list数据结构来存储我们的数据的。今天我们同样可以使用java的Queue类来实现

image-20210524193122443

  • 首先我们得理解队列的特性FIFO 。我们先加入的1会随着后面的元素的添加逐渐跑到最前面。
  • 而本题中正好是将时间戳加入到队列中的。那么我们可以每次加入元素后就开始检索队列头部元素判断时间戳是否超时。未超时的留在队列中。最后留在队列中的元素就是我们的单位时间内的有效请求
常用方法作用失败时措施
add向队列中添加一个元素到队尾抛出错误
remove将队首元素删除并返回抛出错误
element获取队首元素,和remove不同的是不会剔除抛出错误
offer添加一个元素到队尾默认值
poll获取队首元素并删除默认值
peek获取队首元素但是不删除默认值

三、AC 代码

队列实现

  • 基于队列实现我们很好理解,这个概念和我们的滑动时间窗口算法基本是吻合的。
class RecentCounter {

    Queue<Integer> q;
    public RecentCounter() {
        q = new LinkedList();
    }

    public int ping(int t) {
        q.add(t);
        //每次有请求进来就会追加队尾,同时开始剔除时间窗口外的请求
        while (q.peek() < t - 3000)
            q.poll();
        return q.size();
    }
}

image-20210525140807345

  • 在性能综合上看表现还算不错。

set实现

  • 除了队列以外我们还可以使用set来实现。而treeset恰好就是顺序存储的。实现和队列一样只不过队列换成了set 。笔者认为通过set实现代码是真的简洁
public int ping(int t) {
    set.add(t);
    set.removeIf(item->t - item > 3000);
    return set.size();
}

image-20210525141033486

  • 不过这个性能是真的差,这里大家可以当做一种参考。抛砖引玉

set优化

  • set执行效率真的差主要原因是我追求代码的简洁了。稍加改动效果大不一样
public int ping(int t) {
    set.add(t);
    /*set = set.stream().filter(item -> {
        return t - item <= 3000;
    }).collect(Collectors.toSet());*/
    Iterator<Integer> iterator = set.iterator();
    while (iterator.hasNext()) {
        if (t - iterator.next() > 3000) {
            iterator.remove();
        } else {
            break;
        }
    }
    return set.size();
}

image-20210525141307231

  • 但是执行速度降了下来。

四、总结

  • 队列和set两种方式各有优缺点。队列在时间和内存上总体上比较平稳。set内存空间占用少。但是速度很慢

点赞、关注哈哈哈