用 Go 語言實現滑動視窗限流演算法,並利用 Redis 作為儲存後端,可以按照以下步驟進行設計和編碼。滑動視窗限流的核心思想是維護一個固定時間視窗,並在視窗內記錄請求次數,當視窗滑動時,舊的請求計數被移除,新的請求計數被新增。這裡以 Redis 的有序集合(Sorted Set,簡稱 ZSet)作為資料結構,因為它可以方便地實現時間排序和計數功能。
步驟一:定義滑動視窗引數
確定滑動視窗的幾個關鍵引數:
- 時間視窗寬度(如:1秒、5分鐘等)
- 允許的最大請求數量(如:每秒100次、每分鐘1000次等)
步驟二:選擇 Redis 操作
使用 Redis 的有序集合(ZSet),其成員為請求發生的時間戳(Unix 時間戳),分值為請求的計數值(通常初始為1)。ZSet 可以自動按分值排序,便於我們管理滑動視窗內的請求。
步驟三:編寫 Go 程式碼實現限流邏輯
以下是使用 Go 語言和 Redis 實現滑動視窗限流的基本流程:
- 初始化 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
})
}
-
定義限流方法:
建立一個名為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 // 未超限,允許請求
}
- 呼叫限流方法:
在實際處理請求的邏輯中,先呼叫limitRequest
方法判斷是否允許此次請求,根據返回值決定是否繼續處理請求或返回限流響應。
請注意,以上程式碼示例假設您已經設定了 Redis 連線資訊和相關常量(如 timeWindowWidth
)。實際應用中,請根據您的具體需求調整程式碼,例如新增錯誤處理、最佳化效能(如使用 pipeline 或 Lua 指令碼)、考慮分散式環境下的鎖機制(如 RedLock)等。
此外,滑動視窗限流演算法在高併發場景下可能會對 Redis 造成一定壓力,尤其是視窗較短且請求量大的情況下。為了提高效能,可以考慮使用布隆過濾器提前過濾掉部分請求,或者結合客戶端快取減少對 Redis 的直接訪問。
Redis 發生故障時
使用 Redis 作為限流工具時,確實需要考慮其服務的穩定性。儘管 Redis 是一個高可用、高效能的鍵值資料庫,但在實際生產環境中,任何服務都可能因為各種原因(如硬體故障、網路問題、軟體錯誤等)出現暫時不可用的情況。針對 Redis 崩潰或不可用的情況,可以採取以下幾種策略來應對:
-
冗餘與高可用部署:
- 主從複製:配置 Redis 主從架構,確保資料在多個節點間同步。當主節點崩潰時,可以透過自動或手動切換到已同步資料的從節點繼續提供服務。
- 哨兵模式(Sentinel):使用 Redis Sentinel 提供自動故障檢測和主節點切換功能,進一步提升系統的自我恢復能力。
- 叢集模式:部署 Redis 叢集,將資料和負載分散在多個節點上,即使部分節點不可用,整個叢集仍能繼續提供服務。
-
客戶端容錯與重試:
- 連線池管理:在客戶端實現連線池管理,當連線失敗時能夠自動重新建立連線或從池中獲取其他可用連線。
- 重試策略:對於因 Redis 臨時不可用導致的失敗操作,實施合理的重試策略。比如,短暫延遲後重試(指數退避或固定間隔重試),避免短時間內頻繁重試加重 Redis 伺服器負擔。
- 降級策略:在 Redis 不可用時,客戶端可以暫時執行降級邏輯,如放寬限流條件、允許一定比例的請求透過(犧牲一部分限流效果),或者暫時禁用限流功能,確保服務的基本可用性。
-
本地快取與兜底邏輯:
- 本地計數:在客戶端(如應用程式伺服器)維持一個本地計數器,用於在短時間內(如幾秒鐘)進行限流。這樣,在 Redis 短暫不可用期間,可以依賴本地計數器進行限流,待 Redis 恢復後,再將本地計數同步回 Redis。
- 熔斷與降級:在客戶端或服務治理框架中設定熔斷機制,當連續檢測到 Redis 服務不可用時,觸發熔斷狀態,直接拒絕部分非關鍵請求或返回預設值,防止請求堆積導致系統雪崩。
-
監控與報警:
- 實時監控:對 Redis 服務的執行狀態、效能指標、故障事件進行實時監控,及時發現異常情況。
- 報警通知:設定警報閾值和通知機制,一旦 Redis 出現故障或效能下降,立即通知運維人員進行干預。
透過上述措施,可以在 Redis 發生故障時降低對限流功能的影響,保障系統的整體穩定性和可用性。同時,應定期對 Redis 叢集進行健康檢查、效能調優和資料備份,預防潛在問題,提升系統的健壯性。
歡迎關注公-眾-號【TaonyDaily】、留言、評論,一起學習。
Don’t reinvent the wheel, library code is there to help.
文章來源:劉俊濤的部落格
若有幫助到您,歡迎點贊、轉發、支援,您的支援是對我堅持最好的肯定(_)