幾種限流、控頻策略對比

這裏主要討論以下幾種方式,有更好的建議,歡迎留言

  • 基於數據庫(Mysql爲例)的統計進行限流
  • 基於redis自增長及過期策略的限流
  • 基於內存(linkedlist爲例)的限流
  • 基於木桶算法的限流

本次介紹,主要關注實現策略、控制粒度及時間窗口問題,示例代碼中可能存在編碼不規範的情況,請忽略

1. 基於數據庫的統計進行限流

      基於數據庫的統計進行限流主要思想是,將每次的信息連同時間寫入一條數據庫記錄,然後根據時間範圍統計信息,決策是否需要限流。舉例如下:

場景:
       密碼1分鐘內輸入錯誤次數超限,凍結賬號。
要點:
      1. 登錄失敗時,將用戶的登錄名、登錄時間寫入數據庫記錄。
      2.登錄前根據登錄名,查詢近1分鐘登錄失敗的次數。
      3.判斷登錄次數是否大於設定的超限值,如果大於則進行凍結處理,如果未超過,正常執行登錄判斷(登錄失敗時執行第一要點)。
代碼要點:

 select count(登錄名) from 登錄錯誤記錄表 where 登錄名='登錄名' and 登錄時間 > DATE_SUB(NOW(), INTERVAL 1 MINUTE)

分析:
      此方案的粒度控制在於代碼邏輯的堆砌和數據庫的設計,較爲靈活,時間窗口爲最近一分鐘(假定),屬於滑動時間窗口;由於時基於數據庫的,分佈式部署的情況下也能有效,不足之處在於會增加數據庫的壓力。

2. 基於redis自增長及過期策略的限流

      基於redis自增長及過期策略主要思想是:在redis中維護一個key,並設置過期時間,每次控制前讀取該key,如果該key對應的值大於了限制值,則被限制,如果不大於,則通過redis的incr對key進行自增長處理,如果爲空,則重新維護key,並設置過期時間。如此往復達到限流目的。
場景:
      對接口進行會話級別的訪問控制
要點
      1. 讀取key,根據key對應值value的不同情況進行處理:
      a. value不存在,設置set key 及過期時間
      b. value大於限定值,限流
      c. valuex不大於限定值,value自增長1
代碼要點:
      涉及到的redis命令如下:

## 設置key的值5秒過期
set kkk 1 EX 5
### 獲取key的值
get kkk
### 自增長
incr kkk
### 手動設置過期時間
expire kkk 5

      代碼示例

private boolean accessCheck(HttpSession session) {
        String key = "ACCESS"+session.getId();
        long current = 1;
        // key不存在,進行設置
        if( valueOperations.get(key) == null){
            //一步到位 
            //valueOperations.set(key,1,5,TimeUnit.SECONDS);
            // 也可以分兩步設置
            valueOperations.increment(key);
            valueOperations.getOperations().expire(key,5,TimeUnit.SECONDS);
        }else {
        	// key已經存在了,自增+1
            valueOperations.increment(key);
            current = Long.parseLong(valueOperations.get(key));
        }

        if(current > 20){
            throw new RuntimeException("訪問次數過於多");
        }

        return true;
    }

分析:
      基於redis同樣可以適用分佈式環境,時間窗口固定,因redis特性,可適應較大流量衝擊,控制粒度在於設計。

3. 基於內存(linkedlist爲例)的限流

      基於內存的限流主要策略是在內存中維護一個時間窗口數據,基於對數據的統計進行限流。
場景:
      對接口調用頻度進行控制
要點
      1.維護一個以時間正序的列表(不是正序需要掃描全部數據)
      2.去除時間窗口之外數據
      3.末尾處加入最新時間
代碼要點:
      以linkedlist爲例

 private boolean accessCheckByLinkedList()  {
        synchronized(accessList) {
            Date data = null;
            Date now = new Date();
            if(accessList.size() > 0){
                data = accessList.getFirst();
                while (data != null && now.getTime() - data.getTime() > 5000) {
                    accessList.removeFirst();
                    if(accessList.size() > 0){
                        data = accessList.getFirst();
                    }else {
                        data = null;
                    }
                }
                if (accessList.size() > 5) {
                    throw new RuntimeException( "訪問次數過於多");
                }
            }
            accessList.add(new Date());
        }
        return true;
    }

分析:
      基於內存的限流,其粒度控制依賴於設計的是否高明,因爲實在內存中處理的,需要考慮線程間同步(採用同步的存儲如ConcurrentHashMap,可能不需要處理同步問題,但是要注意排序問題)及併發問題。控制粒度爲應用級別,能從應用層面控制當前訪問的壓力,避免負載均衡策略失效或異常情況下帶來的大流量衝擊。

4. 基於木桶算法的限流

      基於木桶算法的限流策略主要是維護一個固定大小的“池”,根據場景消耗“池”的存量,並不斷的向內添加,但永遠維護“池”中存量不會超過最大值
場景:
      應用級別接口請求頻度控制
要點
      1.增加“池”中存量,存量達到最大值,暫停增加
      2.使用“池”中存量,存量小於0限流
      3.保證使用“池”不會溢出,也不會透支
代碼要點:
      基於該策略,以下完整代碼描述了整個過程,其中的各頻度控制僅爲實驗效果而定。

public class BucketAlgorithmTest {
    public static void main(String[] args) throws InterruptedException {
        // 定時流入
        new Thread(()-> {
            while (true){
                Bucket.add();
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Thread.sleep(200);// 如果直接默認初始值爲滿的,此處可省略
        // 定時流出,模擬控制域
        new Thread(()->{
            int errorcout = 0;
            while (errorcout < 10) {
                try {
                    Bucket.get();
                    Thread.sleep(40);
                } catch (Exception e) {
                    errorcout ++;
                    e.printStackTrace();
                    try {
                        Thread.sleep(100);
                    }catch (Exception ee){

                    }
                }
            }
        }).start();
    }


}
class Bucket{
	/**
	* 當前存量
	*/
    private static Integer currentSize = 0;
    
	/**
	* 容量
	*/
    private final static int totalSize = 20;
    /**
    * 流入
    */
    public static void add(){
        synchronized (currentSize) {
            if(currentSize < totalSize) {
                currentSize++;
                System.out.println("[添加++++]可用" + Bucket.getCurrentSize());
            }
        }
    }
    /**
    * 流出
    */
    public static void get(){
        synchronized (currentSize) {
            if(currentSize > 0) {
                currentSize--;
                System.out.println("[使用---]可用" + Bucket.getCurrentSize());
            }else {
                throw new RuntimeException("當前沒有剩餘連接可供使用");
            }
        }
    }
    public static Integer getCurrentSize(){
        return currentSize;
    }
}

爲方便理解,這裏給補充一張運行結果方便理解
在這裏插入圖片描述

分析:
      木桶原理算法控制粒度可調性較大,依賴對算法原理的理解程度,在峯值大流量衝擊下,可能造成恢復時間增長(相對而言),該策略也類似是滑動“時間”窗口(其實滑動的是流量,這裏簡單理解爲滑動窗口吧)。該策略的數據既可以在內存中,也可以在數據庫中,也可以在redis等緩存中,使用場景也可以根據需要靈活設定,可應用級,也可以在負載層面作爲負載策略的擴充。

簡單總結對比一下

方案 時間窗口 基於 分佈式環境 應用範圍
基於數據庫 滑動 數據庫 適用 登錄控制
基於Redis 固定 Redis 適用 接口訪問控制
內存 滑動 內存 不適用 應用級別訪問控制
木桶算法 滑動 內存/Redis/數據庫 適用 根據應用位置而定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章