天天看點

Java 實作滑動時間視窗限流算法,你見過嗎?

在網上搜滑動時間視窗限流算法,大多都太複雜了,本人實作了個簡單的,先上代碼:

package cn.dijia478.util;

import java.time.LocalTime;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 滑動時間視窗限流工具
 * 本限流工具隻适用于單機版,如果想要做全局限流,可以按本程式的思想,用redis的List結構去實作
 *
 * @author dijia478
 * @date 2020-10-13 10:53
 */
public class SlideWindow {

    /** 隊列id和隊列的映射關系,隊列裡面存儲的是每一次通過時候的時間戳,這樣可以使得程式裡有多個限流隊列 */
    private volatile static Map<String, List<Long>> MAP = new ConcurrentHashMap<>();

    private SlideWindow() {}

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            // 任意10秒内,隻允許2次通過
            System.out.println(LocalTime.now().toString() + SlideWindow.isGo("ListId", 2, 10000L));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }

    /**
     * 滑動時間視窗限流算法
     * 在指定時間視窗,指定限制次數内,是否允許通過
     *
     * @param listId     隊列id
     * @param count      限制次數
     * @param timeWindow 時間視窗大小
     * @return 是否允許通過
     */
    public static synchronized boolean isGo(String listId, int count, long timeWindow) {
        // 擷取目前時間
        long nowTime = System.currentTimeMillis();
        // 根據隊列id,取出對應的限流隊列,若沒有則建立
        List<Long> list = MAP.computeIfAbsent(listId, k -> new LinkedList<>());
        // 如果隊列還沒滿,則允許通過,并添加目前時間戳到隊列開始位置
        if (list.size() < count) {
            list.add(0, nowTime);
            return true;
        }

        // 隊列已滿(達到限制次數),則擷取隊列中最早添加的時間戳
        Long farTime = list.get(count - 1);
        // 用目前時間戳 減去 最早添加的時間戳
        if (nowTime - farTime <= timeWindow) {
            // 若結果小于等于timeWindow,則說明在timeWindow内,通過的次數大于count
            // 不允許通過
            return false;
        } else {
            // 若結果大于timeWindow,則說明在timeWindow内,通過的次數小于等于count
            // 允許通過,并删除最早添加的時間戳,将目前時間添加到隊列開始位置
            list.remove(count - 1);
            list.add(0, nowTime);
            return true;
        }
    }

}      

運作可以看到,任意10秒内,通過的次數不超過2次。或者按照實作原理來說,任意通過2次内的時間差,都不超過10秒:

Java 實作滑動時間視窗限流算法,你見過嗎?

這裡畫圖做說明,為什麼這樣可以做到滑動視窗限流,假設10秒内允許通過5次

1.這條線就是隊列list,當第一個事件進來,隊列大小是0,時間是第1秒:

Java 實作滑動時間視窗限流算法,你見過嗎?

2.因為size=0,小于5,都沒有到限制的次數,完全不用考慮時間視窗,直接把這次事件的時間戳放到0的位置:

Java 實作滑動時間視窗限流算法,你見過嗎?

3.第2.8秒的時候,第二個事件來了。因為此時size=1,還是小于5,把這次事件的時間戳放到0的位置,原來第1秒來的事件時間戳會往後移動一格:

Java 實作滑動時間視窗限流算法,你見過嗎?

4.陸續的又來了3個事件,隊列大小變成了5,先來的時間戳依次向後移動。此時,第6個事件來了,時間是第8秒:

Java 實作滑動時間視窗限流算法,你見過嗎?

5.因為size=5,不小于5,此時已經達到限制次數,以後都需要考慮時間視窗了。是以取出位置4的時間(離現在最遠的時間),和第6個事件的時間戳做比較:

Java 實作滑動時間視窗限流算法,你見過嗎?

6.得到的差是7秒,小于時間視窗10秒,說明在10秒内,來的事件個數大于5了,是以本次不允許通過:

Java 實作滑動時間視窗限流算法,你見過嗎?

7.接下來即便來上100個事件,隻要時間差小于等于10秒,都同上,拒絕通過:

Java 實作滑動時間視窗限流算法,你見過嗎?

8.第11.1秒,第101次事件過來了。因為size=5,不小于5,是以取出位置4的時間(離現在最遠的時間),和第101個事件的時間戳做比較:

Java 實作滑動時間視窗限流算法,你見過嗎?

9.得到的差是10.1秒,大于時間視窗10秒,說明在10秒内,來的事件個數小于等于5了,是以本次允許通過:

Java 實作滑動時間視窗限流算法,你見過嗎?

10.删除位置4的時間(離現在最遠的時間),把這次事件的時間戳放到0的位置,後面的時間戳依次向後移動:

Java 實作滑動時間視窗限流算法,你見過嗎?

往後再來其他事件,就是重複4-10的步驟,即可實作,在任意滑動時間視窗内,限制通過的次數

其本質思想是轉換概念,将原本問題的确定時間大小,進行次數限制。轉換成确定次數大小,進行時間限制。