php基於redis的list型資料結構實現ip限流操作

仙人掌發表於2021-02-08

在日常的業務功能開發中,如果要 限制任意一個ip在連續的某一段時間內,只能訪問某個介面一定的次數,需要如何實現呢?
這種功能需求通常是用來應對防止指令碼惡意刷介面的情況,目前網上已經有很多比較完善的限流方案。對於一般的站點來講,可以藉助redis的連結串列型資料結構來實現ip限流功能。
舉個例子——
假如我們需要實現,對於介面A,限制任意IP在每一段連續的5秒內,最多允許3次訪問,超過3次則返回報錯。
file
對於上圖來講,在08秒的時候,最近的5秒內已經發起了4次請求,已經達到最大次數限制,所以此時訪問會受限。
採用PHP來實現的話,具體的邏輯程式碼如下——

/**
* 檢查佇列的長度是否到達設定的閾值,已到達則返回false,未到達則將當前時間戳推入佇列最末端,同時重新整理佇列整體的快取時間
* @param $key 佇列快取的key
* @param $expire 佇列快取過期時間,例如上面例子中的5秒
* @param $limit 佇列長度閾值,如上面例子中的3次
* @return bool
*/
public function checkLimit($key, $expire, $limit)
{
    $length = $this->refreshList($key, $expire);
    if ($length < $limit) {
        // 未到達訪問限制,將當前時間戳推入到list的最後邊,同時把整個key的過期時間重新更新
        $this->rPush($key, time());
        $this->expire($key, intval($limit));
        return true;
    }
    return false;
}
/** 
* 重新整理佇列,過濾掉已經不在有效時間內的值,返回最新佇列的長度
* @param $key string 自定義的快取key
* @param $expire 佇列快取過期時間,例如上面例子中的5秒
* @return bool|int
*/
public function refreshList ($key, $expire)   
 {
        if ($this->has($key)) {
            do { // 對於已存在資料的list,要先從前往後把已經過期的資料彈出
                $oldest_value = $this->lPop($key);
            } while ($oldest_value && time() - $oldest_value > $expire);
            // 把最後彈出的資料重新塞回list的最前邊
            $oldest_value && $this->lPush($key, $oldest_value);
            return $this->lLen($key);
        }
        return 0;
}

其中用到的lPop,lPush,lLen,rPush等方法,都是封裝了redis擴充之後,操作連結串列型資料結構的一些方法,引數跟返回值都與原生方法保持一致。
其實後來網上查了之後才知道,redis處理這種場景,更多的是直接用zset這種有序集來實現,邏輯也是基本一致,就是存當前時間戳,然後用滑動視窗的演算法思想,判斷當前視窗內的值長度是否已經超過限制。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章