使用 Redis 實現限流——滑動窗口算法

用 Go 語言實現滑動窗口限流算法,並利用 Redis 作爲存儲後端,可以按照以下步驟進行設計和編碼。滑動窗口限流的核心思想是維護一個固定時間窗口,並在窗口內記錄請求次數,當窗口滑動時,舊的請求計數被移除,新的請求計數被添加。這裏以 Redis 的有序集合(Sorted Set,簡稱 ZSet)作爲數據結構,因爲它可以方便地實現時間排序和計數功能。

步驟一:定義滑動窗口參數

確定滑動窗口的幾個關鍵參數:

  • 時間窗口寬度(如:1秒、5分鐘等)
  • 允許的最大請求數量(如:每秒100次、每分鐘1000次等)

步驟二:選擇 Redis 操作

使用 Redis 的有序集合(ZSet),其成員爲請求發生的時間戳(Unix 時間戳),分值爲請求的計數值(通常初始爲1)。ZSet 可以自動按分值排序,便於我們管理滑動窗口內的請求。

步驟三:編寫 Go 代碼實現限流邏輯

以下是使用 Go 語言和 Redis 實現滑動窗口限流的基本流程:

  1. 初始化 Redis 客戶端
    使用 github.com/go-redis/redis/v8 庫或其他您熟悉的 Redis 客戶端庫創建一個 Redis 客戶端實例。
import (
    "github.com/go-redis/redis/v8"
)

var rdb *redis.Client

func init() {
    rdb = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })
}
  1. 定義限流方法
    創建一個名爲 limitRequest 的函數,接收請求的標識符(如 API 路徑或用戶 ID)和當前時間作爲參數。在函數內部執行以下操作:

    a. 計算窗口邊界
    根據當前時間計算出滑動窗口的起始和結束時間戳。

    b. 清理過期請求
    使用 ZSet 的範圍刪除(ZREMRANGEBYSCORE)命令移除窗口起始時間之前的元素,確保窗口內僅包含有效請求。

    c. 累加新請求
    將當前請求的時間戳作爲成員加入 ZSet,如果已有相同時間戳的成員,則使用 ZINCRBY 命令遞增其分值(表示多次請求在同一時刻)。

    d. 檢查請求是否超限
    使用 ZCARD 命令獲取窗口內請求總數,與允許的最大請求數比較。若超出限制,則返回限流結果;否則,允許此次請求並返回成功。

func limitRequest(identifier string, now int64, maxRequestsInWindow int) (bool, error) {
    key := fmt.Sprintf("rate_limit:%s", identifier)

    // 計算窗口邊界
    windowStart := now - timeWindowWidth
    windowEnd := now

    // 清理過期請求
    if err := rdb.ZRemRangeByScore(ctx, key, "-inf", windowStart).Err(); err != nil {
        return false, err
    }

    // 累加新請求
    pipe := rdb.Pipeline()
    exists, _ := pipe.ZScore(ctx, key, now).Result()
    if exists == nil {
        pipe.ZAdd(ctx, key, &redis.Z{Score: float64(now), Member: now})
    } else {
        pipe.ZIncrBy(ctx, key, 1, now)
    }
    _, err := pipe.Exec(ctx)
    if err != nil {
        return false, err
    }

    // 檢查請求是否超限
    count, err := rdb.ZCard(ctx, key).Result()
    if err != nil {
        return false, err
    }
    if count > int64(maxRequestsInWindow) {
        return false, nil // 超限,拒絕請求
    }

    return true, nil // 未超限,允許請求
}
  1. 調用限流方法
    在實際處理請求的邏輯中,先調用 limitRequest 方法判斷是否允許此次請求,根據返回值決定是否繼續處理請求或返回限流響應。

請注意,以上代碼示例假設您已經設置了 Redis 連接信息和相關常量(如 timeWindowWidth)。實際應用中,請根據您的具體需求調整代碼,例如添加錯誤處理、優化性能(如使用 pipeline 或 Lua 腳本)、考慮分佈式環境下的鎖機制(如 RedLock)等。

此外,滑動窗口限流算法在高併發場景下可能會對 Redis 造成一定壓力,尤其是窗口較短且請求量大的情況下。爲了提高性能,可以考慮使用布隆過濾器提前過濾掉部分請求,或者結合客戶端緩存減少對 Redis 的直接訪問。

Redis 發生故障時

使用 Redis 作爲限流工具時,確實需要考慮其服務的穩定性。儘管 Redis 是一個高可用、高性能的鍵值數據庫,但在實際生產環境中,任何服務都可能因爲各種原因(如硬件故障、網絡問題、軟件錯誤等)出現暫時不可用的情況。針對 Redis 崩潰或不可用的情況,可以採取以下幾種策略來應對:

  1. 冗餘與高可用部署

    • 主從複製:配置 Redis 主從架構,確保數據在多個節點間同步。當主節點崩潰時,可以通過自動或手動切換到已同步數據的從節點繼續提供服務。
    • 哨兵模式(Sentinel):使用 Redis Sentinel 提供自動故障檢測和主節點切換功能,進一步提升系統的自我恢復能力。
    • 集羣模式:部署 Redis 集羣,將數據和負載分散在多個節點上,即使部分節點不可用,整個集羣仍能繼續提供服務。
  2. 客戶端容錯與重試

    • 連接池管理:在客戶端實現連接池管理,當連接失敗時能夠自動重新建立連接或從池中獲取其他可用連接。
    • 重試策略:對於因 Redis 臨時不可用導致的失敗操作,實施合理的重試策略。比如,短暫延遲後重試(指數退避或固定間隔重試),避免短時間內頻繁重試加重 Redis 服務器負擔。
    • 降級策略:在 Redis 不可用時,客戶端可以暫時執行降級邏輯,如放寬限流條件、允許一定比例的請求通過(犧牲一部分限流效果),或者暫時禁用限流功能,確保服務的基本可用性。
  3. 本地緩存與兜底邏輯

    • 本地計數:在客戶端(如應用程序服務器)維持一個本地計數器,用於在短時間內(如幾秒鐘)進行限流。這樣,在 Redis 短暫不可用期間,可以依賴本地計數器進行限流,待 Redis 恢復後,再將本地計數同步回 Redis。
    • 熔斷與降級:在客戶端或服務治理框架中設置熔斷機制,當連續檢測到 Redis 服務不可用時,觸發熔斷狀態,直接拒絕部分非關鍵請求或返回默認值,防止請求堆積導致系統雪崩。
  4. 監控與報警

    • 實時監控:對 Redis 服務的運行狀態、性能指標、故障事件進行實時監控,及時發現異常情況。
    • 報警通知:設置警報閾值和通知機制,一旦 Redis 出現故障或性能下降,立即通知運維人員進行干預。

通過上述措施,可以在 Redis 發生故障時降低對限流功能的影響,保障系統的整體穩定性和可用性。同時,應定期對 Redis 集羣進行健康檢查、性能調優和數據備份,預防潛在問題,提升系統的健壯性。



歡迎關注公-衆-號【TaonyDaily】、留言、評論,一起學習。

公衆號

Don’t reinvent the wheel, library code is there to help.

文章來源:劉俊濤的博客


若有幫助到您,歡迎點贊、轉發、支持,您的支持是對我堅持最好的肯定(_)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章