限速算法之滑动窗口

前言

假设作为一个完全不懂算法的人,让你去实现一个限速功能(1秒内最多100次),你可能会想到的最简单方式就是记录一个开始时间,然后开始计数,当计数达到100之后限制调用,等待时间间隔达到1秒的时候重置计数器,然后重新计数,如此往复。

这种方式被称为计数器算法,也可以理解为固定时间窗口计数法,因为与之对应的还有一个方法叫作滑动时间窗口计数法

 

 

1.计数器法

这种算法实现起来并不难,主要要点在于重置计数器和开始时间,我这里用Go,以一个方法的调用为例:

package main

import (
    "fmt"
    "time"
)

func main() {
    var count = 1
    var sTime = time.Now()

    for {
        //如果当前时间段超过1秒了,重新计数,并重置开始时间
        if time.Now().Sub(sTime).Seconds() > 1  {
            count = 1
            sTime = time.Now()
        }
        //如果计数机大于100了,改时间段内就不能再做操作了
        if count > 100  {
            time.Sleep(time.Millisecond * 10)
            continue
        }
        doSomething(count)
        count++
    }
}

func doSomething(n int) {
    fmt.Printf("do something #%d\n", n)
}

 

这种算法最大问题在于临界点问题,比如说在上面这个例子里面,假设我们在前面1秒钟中最后999ms的时候打印了100个,又在第二个1秒的前1ms内又打印了100个,那么总得算起来,其在2ms内就打印了200个,还是没有达到我们想要的效果。

 

 

2.滑动窗口计数法

 

假设我要实现10s内有5次通过,看看具体是怎么实现的:

1、维护一个队列,队列的最大长度是5:

 

 

 

2、第1秒时来了一个事件,因为刚开始size=0,小于5,都没有到限制的次数,完全不用考虑时间窗口,可以直接把这次事件的时间戳放到0的位置 :

 

 

3、第2.8秒的时候,第二个事件来了。因为此时size=1,还是小于5,把这次事件的时间戳放到0的位置,原来第1秒来的事件时间戳会往后移动一格: 

 

 

 

4、陆续的又来了3个事件,队列大小变成了5,先来的时间戳依次向后移动。此时,第6个事件来了,时间是第8秒:

 

 

5、因为size=5,不小于5,此时已经达到限制次数,以后都需要考虑时间窗口了。所以取出位置4的时间(离现在最远的时间),和第6个事件的时间戳做比较:

 

 

6、得到的差是7秒,小于时间窗口10秒,说明在10秒内,来的事件个数大于5了,所以本次不允许通过:

 

 

 

 

7、接下来即便来上100个事件,只要时间差小于等于10秒,都同上,拒绝通过:

 

 

 

 

8、第11.1秒,第101次事件过来了。因为size=5,不小于5,所以取出位置4的时间(离现在最远的时间),和第101个事件的时间戳做比较:

 

 

 

9.得到的差是10.1秒,大于时间窗口10秒,说明在10秒内,来的事件个数小于等于5了,所以本次允许通过:

 

 

 

10.删除位置4的时间(离现在最远的时间),把这次事件的时间戳放到0的位置,后面的时间戳依次向后移动:

 

 

 

往后再来其他事件,就是重复4-10的步骤,即可实现,在任意滑动时间窗口内,限制通过的次数

其本质思想是转换概念,将原本问题的确定时间大小,进行次数限制。转换成确定次数大小,进行时间限制。

 

测试代码:

 

package utils
 
import "time"
 
var LimitQueue map[string][]int64
var ok bool
 
//单机时间滑动窗口限流法
func LimitFreqSingle(queueName string, count uint, timeWindow int64) bool {
    currTime := time.Now().Unix()
    if LimitQueue == nil {
        LimitQueue = make(map[string][]int64)
    }
    if _, ok = LimitQueue[queueName]; !ok {
        LimitQueue[queueName] = make([]int64, 0)
    }
    //队列未满
    if uint(len(LimitQueue[queueName])) < count {
        LimitQueue[queueName] = append(LimitQueue[queueName], currTime)
        return true
    }
    //队列满了,取出最早访问的时间
    earlyTime := LimitQueue[queueName][0]
    //说明最早期的时间还在时间窗口内,还没过期,所以不允许通过
    if currTime-earlyTime <= timeWindow {
        return false
    } else {
        //说明最早期的访问应该过期了,去掉最早期的
        LimitQueue[queueName] = LimitQueue[queueName][1:]
        LimitQueue[queueName] = append(LimitQueue[queueName], currTime)
    }
    return true

}

 

 

 

func limitIpFreq(c *gin.Context, timeWindow int64, count uint) bool {
    ip := c.ClientIP()
    key := "limit:" + ip
    if !utils.LimitFreqSingle(key, count, timeWindow) {
        c.JSON(200, gin.H{
            "code": 400,
            "msg":  "error Current IP frequently visited",
        })

        return false
    }
    return true

}

 

posted @ 2021-09-17 10:15  独揽风月  阅读(382)  评论(0编辑  收藏  举报