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

刘俊涛的博客發表於2024-04-25

用 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.

文章來源:劉俊濤的部落格


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

相關文章