軟件系統限流的底層原理解析
作者:騰訊雲天御業務安全工程師 knightwwang
在軟件架構中,限流是一種控制資源使用和保護系統安全的重要機制。它通過限制在一定時間內可以處理的請求數量,來防止系統過載。
1. 限流的目的
限流主要有兩個目的:
-
防止系統過載:確保系統在高負載情況下仍能保持穩定運行。
-
保證服務質量:爲所有用戶提供公平的服務,避免某些用戶佔用過多資源。
2. 限流算法的實現
2.1 固定窗口計數器算法
固定窗口計數器算法是一種基本的限流方法,它通過在固定時間窗口內跟蹤請求的數量來實現限流。
// 這是一個簡單的實現案例
package main
import (
"fmt"
"sync"
"time"
)
// FixedWindowCounter 結構體實現固定窗口計數器限流算法。
// mu 用於同步訪問,保證併發安全。
// count 記錄當前時間窗口內的請求數量。
// limit 是時間窗口內允許的最大請求數量。
// window 記錄當前時間窗口的開始時間。
// duration 是時間窗口的持續時間。
type FixedWindowCounter struct {
mu sync.Mutex
count int
limit int
window time.Time
duration time.Duration
}
// NewFixedWindowCounter 構造函數初始化 FixedWindowCounter 實例。
// limit 參數定義了每個時間窗口內允許的請求數量。
// duration 參數定義了時間窗口的大小。
func NewFixedWindowCounter(limit int, duration time.Duration) *FixedWindowCounter {
return &FixedWindowCounter{
limit: limit,
window: time.Now(), // 設置當前時間作爲窗口的開始時間。
duration: duration, // 設置時間窗口的持續時間。
}
}
// Allow 方法用於判斷當前請求是否被允許。
// 首先通過互斥鎖保證方法的原子性。
func (f *FixedWindowCounter) Allow() bool {
f.mu.Lock()
defer f.mu.Unlock()
now := time.Now() // 獲取當前時間。
// 如果當前時間超過了窗口的結束時間,重置計數器和窗口開始時間。
if now.After(f.window.Add(f.duration)) {
f.count = 0
f.window = now
}
// 如果當前計數小於限制,則增加計數並允許請求。
if f.count < f.limit {
f.count++
return true
}
// 如果計數達到限制,則拒絕請求。
return false
}
// main 函數是程序的入口點。
func main() {
// 創建一個新的限流器,設置每分鐘(time.Minute)只允許10個請求。
limiter := NewFixedWindowCounter(10, time.Minute)
// 模擬15個請求,觀察限流效果。
for i := 0; i < 15; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
實現原理:固定窗口計數器算法通過設置一個固定的時間窗口(例如每分鐘)和一個在這個窗口內允許的請求數量限制(例如 10 個請求)。在每個時間窗口開始時,計數器重置爲零,隨着請求的到來,計數器遞增。當計數器達到限制時,後續的請求將被拒絕,直到窗口重置。
優點:
-
實現簡單直觀。
-
容易理解和實現。
-
可以保證在任何給定的固定時間窗口內,請求的數量不會超過設定的閾值。
缺點:
-
在窗口切換的瞬間可能會有請求高峯,因爲計數器重置可能導致大量請求幾乎同時被處理。
-
無法平滑地處理突發流量,可能導致服務體驗不佳。
固定窗口計數器算法適用於請求分佈相對均勻的場景,但在請求可能在短時間內集中到達的場景下,可能需要考慮更復雜的限流算法,如滑動窗口或令牌桶算法。
2.2 滑動窗口算法
滑動窗口算法是固定窗口計數器算法的一個改進,它通過覆蓋多個時間段來平滑請求流量,避免瞬時高峯。這種算法通常需要使用更高級的數據結構,如時間輪(Timing Wheel),來實現。
// 這是一個簡單的實現案例,這個代碼示例僅用於說明滑動窗口限流算法的邏輯,並非完整的工作代碼。
package main
import (
"fmt"
"sync"
"time"
)
// SlidingWindowLimiter 結構體實現滑動窗口限流算法。
type SlidingWindowLimiter struct {
mutex sync.Mutex
counters []int
limit int
windowStart time.Time
windowDuration time.Duration
interval time.Duration
}
// NewSlidingWindowLimiter 構造函數初始化 SlidingWindowLimiter 實例。
func NewSlidingWindowLimiter(limit int, windowDuration time.Duration, interval time.Duration) *SlidingWindowLimiter {
buckets := int(windowDuration / interval)
return &SlidingWindowLimiter{
counters: make([]int, buckets),
limit: limit,
windowStart: time.Now(),
windowDuration: windowDuration,
interval: interval,
}
}
// Allow 方法用於判斷當前請求是否被允許,並實現滑動窗口的邏輯。
func (s *SlidingWindowLimiter) Allow() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
// 檢查是否需要滑動窗口
if time.Since(s.windowStart) > s.windowDuration {
s.slideWindow()
}
now := time.Now()
index := int((now.UnixNano() - s.windowStart.UnixNano()) / s.interval.Nanoseconds()) % len(s.counters)
if s.counters[index] < s.limit {
s.counters[index]++
return true
}
return false
}
// slideWindow 方法實現滑動窗口邏輯,移除最舊的時間段並重置計數器。
func (s *SlidingWindowLimiter) slideWindow() {
// 滑動窗口,忽略最舊的時間段
copy(s.counters, s.counters[1:])
// 重置最後一個時間段的計數器
s.counters[len(s.counters)-1] = 0
// 更新窗口開始時間
s.windowStart = time.Now()
}
// main 函數是程序的入口點。
func main() {
limiter := NewSlidingWindowLimiter(1, time.Second, 10*time.Millisecond)
for i := 0; i < 100; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
實現原理:滑動窗口算法通過將時間分爲多個小的時間段,每個時間段內維護一個獨立的計數器。當一個請求到達時,它會被分配到當前時間所在的小時間段,並檢查該時間段的計數器是否已達到限制。如果未達到,則允許請求並增加計數;如果已達到,則拒絕請求。隨着時間的推移,舊的時間段會淡出窗口,新的時間段會加入。
優點:
-
相比固定窗口算法,滑動窗口算法能夠更平滑地處理請求,避免瞬時高峯。
-
可以提供更細緻的流量控制。
缺點:
-
實現相對複雜,需要維護多個計數器和時間索引。
-
對內存和計算的要求更高。
滑動窗口算法適用於需要平滑流量控制的場景,尤其是在面對突發流量時,能夠提供比固定窗口計數器更優的流量控制效果。
2.3 漏桶算法
漏桶算法是一種經典的流量控制方法,特別適合於平滑突發流量,確保數據以均勻的速率被處理。
// 這是一個簡單的實現案例,這個代碼示例僅用於說明漏桶算法的基本邏輯,並非完整的工作代碼。
package main
import (
"fmt"
"time"
)
// LeakyBucket 結構體,包含請求隊列
type LeakyBucket struct {
queue chan struct{} // 請求隊列
}
// NewLeakyBucket 創建一個新的漏桶實例
func NewLeakyBucket(capacity int) *LeakyBucket {
return &LeakyBucket{
queue: make(chan struct{}, capacity),
}
}
// push 將請求放入隊列,如果隊列滿了,返回 false,表示請求被丟棄
func (lb *LeakyBucket) push() bool {
// 如果通道可以發送,請求被接受
select {
case lb.queue <- struct{}{}:
return true
default:
return false
}
}
// process 從隊列中取出請求並模擬處理過程
func (lb *LeakyBucket) process() {
for range lb.queue { // 使用 range 來持續接收隊列中的請求
fmt.Println("Request processed at", time.Now().Format("2006-01-02 15:04:05"))
time.Sleep(100 * time.Millisecond) // 模擬請求處理時間
}
}
func main() {
lb := NewLeakyBucket(5) // 創建一個容量爲5的漏桶
// 啓動請求處理循環
go lb.process()
// 模擬請求
for i := 0; i < 10; i++ {
accepted := lb.push()
if accepted {
fmt.Printf("Request %d accepted at %v\n", i+1, time.Now().Format("2006-01-02 15:04:05"))
} else {
fmt.Printf("Request %d rejected at %v\n", i+1, time.Now().Format("2006-01-02 15:04:05"))
}
}
time.Sleep(2 * time.Second)
}
實現原理:通過一個固定容量的隊列來模擬桶,以恆定速率從桶中取出請求進行處理,無論請求到達的頻率如何,都保證請求以均勻的速度被處理,從而平滑流量並防止流量突增。
優點:
-
能夠強制實現固定的數據處理速率,平滑流量。
-
即使面對突發流量,也能保持穩定的處理速率。
缺點:
-
對於突發流量的處理不夠靈活,可能會延遲處理。
-
實現相對簡單,但需要維護桶的狀態。
漏桶算法適用於需要強制執行固定速率處理的場景,如網絡流量控制、API 請求限制等。通過控制令牌的添加速率,漏桶算法能夠有效地避免系統因瞬時流量高峯而過載。
2.4 令牌桶算法
令牌桶算法是一種流行的限流算法,它允許一定程度的突發流量,同時保持長期的平均速率。
// 這是一個簡單的實現案例,這個代碼示例僅用於說明令牌桶算法的基本邏輯,並非完整的工作代碼。
package main
import (
"fmt"
"sync"
"time"
)
// TokenBucket 結構體實現令牌桶限流算法。
// - mu 用於同步訪問,保證併發安全。
// - capacity 定義桶的容量,即桶中最多可以存放的令牌數。
// - tokens 表示桶中當前的令牌數。
// - refillRate 是令牌的填充速率,表示每秒向桶中添加的令牌數。
// - lastRefill 記錄上次填充令牌的時間。
type TokenBucket struct {
mu sync.Mutex
capacity int
tokens int
refillRate float64
lastRefill time.Time
}
// NewTokenBucket 構造函數初始化 TokenBucket 實例。
// - capacity 參數定義了桶的容量。
// - refillRate 參數定義了每秒向桶中添加的令牌數。
func NewTokenBucket(capacity int, refillRate float64) *TokenBucket {
// 初始化時桶被填滿,tokens 和 capacity 相等。
// lastRefill 設置爲當前時間。
return &TokenBucket{
capacity: capacity,
tokens: capacity,
refillRate: refillRate,
lastRefill: time.Now(),
}
}
// Allow 方法用於判斷當前請求是否被允許。
func (t *TokenBucket) Allow() bool {
t.mu.Lock() // 進入臨界區,確保操作的原子性。
defer t.mu.Unlock()
now := time.Now() // 獲取當前時間。
// 計算自上次填充以來經過的秒數,並轉換爲float64類型。
timeElapsed := float64(now.Unix() - t.lastRefill.Unix())
// 根據 refillRate 計算應該添加的令牌數。
tokensToAdd := t.refillRate * timeElapsed
// 更新令牌數,但不超過桶的容量。
t.tokens += int(tokensToAdd)
if t.tokens > t.capacity {
t.tokens = t.capacity // 確保令牌數不超過桶的容量。
}
// 如果桶中有令牌,則移除一個令牌並允許請求通過。
if t.tokens > 0 {
t.tokens-- // 移除一個令牌。
t.lastRefill = now // 更新上次填充時間到當前時間。
return true
}
// 如果桶中無令牌,則請求被拒絕。
return false
}
// main 函數是程序的入口點。
func main() {
// 創建一個新的令牌桶實例,桶的容量爲10,每秒填充2個令牌。
limiter := NewTokenBucket(10, 2)
// 模擬請求,觀察限流效果。
// 循環15次,每次請求判斷是否被允許。
for i := 0; i < 15; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
實現原理:令牌桶算法使用一個令牌桶來調節數據流的速率,允許一定程度的流量突發。桶初始時爲空,並以固定的速率填充令牌,直至達到預設的容量上限。與漏桶算法不同,令牌桶算法在桶未滿時,可以在每個時間間隔內向桶中添加多個令牌,從而積累處理突發請求的能力。當請求到達時,如果桶中存在令牌,算法會從桶中移除相應數量的令牌來處理請求。如果桶中的令牌不足,請求將被延遲處理或根據策略拒絕服務。如果桶已滿,額外的令牌將不會被添加,確保了令牌數量不會超過桶的容量限制。
優點:
-
允許一定程度的突發流量,更加靈活。
-
可以平滑流量,同時在桶未滿時快速處理請求。
缺點:
-
實現相對複雜,需要維護桶的狀態和時間。
-
對於計算和同步的要求更高。
令牌桶算法適用於需要處理突發流量的場景,如網絡通信、API 調用等。通過控制令牌的填充速率和桶的容量,令牌桶算法能夠有效地平衡流量,防止系統過載,同時允許在短期內處理更多的請求。
3. 限流的實現方式
限流可以通過不同的組件和層次實現
3.1 應用層限流
應用層限流是在應用程序的代碼中直接實現限流邏輯,這通常是通過使用中間件來完成的。中間件可以在處理請求之前先進行限流檢查,以決定是否繼續處理請求或者返回錯誤信息。
// 這是一個僞代碼案例,演示實現邏輯
package main
import (
"fmt"
"github.com/gin-gonic/gin" // 引入Gin框架,用於構建Web服務器和處理HTTP請求
"net/http"
"sync" // 引入sync包,用於同步原語,如互斥鎖
"time" // 引入time包,用於時間相關操作
)
// TokenBucket 結構體實現令牌桶限流算法。
// 它包含互斥鎖mu用於同步訪問,capacity代表桶的容量,
// tokens表示當前桶中的令牌數,refillRate是令牌的填充速率(每秒),
// lastRefill記錄上次填充的時間。
type TokenBucket struct {
mu sync.Mutex
capacity int
tokens int
refillRate float64
lastRefill time.Time
}
// NewTokenBucket 函數創建並初始化一個新的TokenBucket實例。
// 它設置桶的容量和填充速率,並將初始令牌數設爲容量的值。
func NewTokenBucket(capacity int, refillRate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity, // 初始化時桶被填滿
refillRate: refillRate,
lastRefill: time.Now(), // 記錄創建時的時間作爲上次填充時間
}
}
// Allow 方法用於檢查是否允許通過當前請求。
// 它首先獲取鎖,然後計算自上次填充以來應該添加的令牌數,
// 更新桶中的令牌數,但不超過桶的容量。
// 如果桶中至少有一個令牌,它將減少一個令牌並返回true,表示請求被允許。
// 如果桶爲空,則返回false,表示請求被拒絕。
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock() // 獲取鎖,保證操作的原子性
defer tb.mu.Unlock()
now := time.Now() // 獲取當前時間
// 計算自上次填充以來經過的秒數,然後乘以填充速率,得到應添加的令牌數
tokensToAdd := int(tb.refillRate * (now.Sub(tb.lastRefill).Seconds()))
tb.tokens += tokensToAdd // 更新桶中的令牌數
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity // 確保不超過桶的容量
}
if tb.tokens > 0 {
tb.tokens-- // 處理請求,減少一個令牌
tb.lastRefill = now // 更新上次填充時間爲當前時間
return true
}
return false // 如果桶爲空,返回false
}
// Middleware 函數返回一個Gin中間件,該中間件使用TokenBucket來限流。
// 如果TokenBucket的Allow方法返回false,中間件將中斷請求處理,
// 並返回HTTP狀態碼429(Too Many Requests)和錯誤信息。
// 如果請求被允許,中間件將調用c.Next()繼續執行後續的處理鏈。
func Middleware(tb *TokenBucket) gin.HandlerFunc {
return func(c *gin.Context) {
// 在處理請求之前,調用TokenBucket的Allow方法檢查是否允許請求
if !tb.Allow() {
// 如果請求被限流,返回錯誤信息和狀態碼
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
c.Abort() // 中斷請求處理
return
}
// 如果請求未被限流,繼續執行後續的處理鏈
c.Next()
}
}
func main() {
// 創建一個Gin的默認實例,用於Web服務
r := gin.Default()
// 創建TokenBucket實例,用於限流控制
tb := NewTokenBucket(10, 1.0) // 桶的容量爲10,每秒填充1個令牌
// 使用上面定義的限流中間件
r.Use(Middleware(tb))
// 定義一個簡單的路由,當訪問/hello路徑時,返回JSON格式的消息
r.GET("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hello world"})
})
// 啓動Gin服務器,默認監聽在0.0.0.0:8080
r.Run()
}
實現原理:在 Web 應用程序中,限流可以通過中間件實現。中間件在處理 HTTP 請求之前先執行,可以用來進行身份驗證、日誌記錄、限流等操作。在上述代碼中,創建了一個TokenBucket
類型的限流器,並實現了一個Middleware
函數,該函數接收一個TokenBucket
實例作爲參數,並返回一個 Gin 中間件處理器。中間件在處理請求時首先調用Allow
方法檢查是否允許請求通過。
優點:
-
易於實現和集成,可以輕鬆地添加到現有的 Web 應用程序中。
-
細粒度控制,可以針對不同的路由或用戶應用不同的限流策略。
缺點:
-
可能會增加請求處理的延遲,因爲中間件需要在每次請求時進行同步操作。
-
如果不恰當地使用,可能會降低應用程序的併發處理能力。
應用層限流適用於需要細粒度控制的場景,允許開發者根據具體的業務需求定製限流策略。通過合理配置限流器的參數,可以在保證服務質量的同時,提高應用程序的吞吐量和穩定性。
3.2 代理層限流
代理層限流是在網絡通信的代理服務器層面實現限流,例如使用 Nginx 或 HAProxy 等代理服務器。這種方法可以在請求到達後端服務之前對它們進行限制,從而保護後端服務不受過多請求的衝擊。
Nginx 配置示例
http {
# 定義一個限流區域,使用共享內存存儲狀態
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
server {
# 監聽80端口
listen 80;
# 定義一個location塊,用於匹配特定的請求路徑
location /api/ {
# 應用限流規則
limit_req zone=mylimit burst=5 nodelay;
# 代理請求到後端服務
proxy_pass http://backend/;
}
}
}
實現原理:在 Nginx 中,通過定義limit_req_zone
指令創建一個限流區域,並指定使用共享內存來存儲客戶端 IP 地址和對應的請求計數。rate
參數定義了每個客戶端每秒鐘允許的請求數量。在server
塊中,使用limit_req
指令引用之前定義的限流區域,並設置burst
參數允許一定數量的突發請求。
優點:
-
在網絡層面進行限流,可以保護所有後端服務,而不需要在每個應用程序中單獨實現限流邏輯。
-
減輕了後端服務的負擔,因爲多餘的請求在到達後端之前就被拒絕了。
-
配置靈活,可以針對不同的請求路徑和客戶端設置不同的限流規則。
缺點:
-
需要代理服務器支持限流功能,可能需要額外的配置和調優。
-
對於分佈式系統,可能需要額外的機制來同步狀態,確保全局的限流效果。
代理層限流適用於需要在多個服務或整個應用層面控制請求的場景。通過合理配置代理服務器的限流規則,可以在不同的層面上保護系統,提高整體的穩定性和可用性。
3.3 硬件層限流
在硬件層(如負載均衡器)實現限流,可以在請求到達應用服務器之前進行控制。
4. 限流策略
限流策略是確保應用程序能夠處理預期負載並防止過載的一系列規則和措施。
閾值設置
閾值設置是限流策略的基礎,它決定了系統在單位時間內能夠處理的最大請求數量。
僞代碼示例:
// RateLimiterV2 結構體增加了閾值設置功能。
type RateLimiterV2 struct {
mu sync.Mutex
tokens int
capacity int // 桶的容量,代表最大令牌數
refillRate float64 // 每秒填充的令牌數
limit int // 請求處理的閾值,即桶的容量
}
// NewRateLimiterV2 創建一個新的RateLimiterV2實例,並設置閾值。
func NewRateLimiterV2(capacity int, refillRate float64, limit int) *RateLimiterV2 {
return &RateLimiterV2{
capacity: capacity,
refillRate: refillRate,
limit: limit,
}
}
// Allow 現在考慮了設置的閾值。
func (r *RateLimiterV2) Allow() bool {
r.mu.Lock()
defer r.mu.Unlock()
// 令牌桶邏輯...
// 如果桶中的令牌數達到或超過閾值,則拒絕請求。
if r.tokens >= r.limit {
return false
}
// 允許請求邏輯...
return true
}
請求分類
請求分類允許對不同類型的請求應用不同的限流規則,例如,對 API 的不同端點設置不同的閾值。
僞代碼示例:
// RouteLimiterMap 是一個映射,存儲每個路由路徑對應的限流器實例。
// 鍵是路由的字符串表示,值是指向RateLimiterV2類型實例的指針。
var RouteLimiterMap = map[string]*RateLimiterV2{}
// SetRateLimiterForRoute 函數爲指定的路由設置一個新的限流器。
// 它接受路由的路徑、桶的容量、每秒填充的令牌數和請求處理的閾值作爲參數,
// 並創建一個新的RateLimiterV2實例,將其存儲在RouteLimiterMap中。
func SetRateLimiterForRoute(route string, capacity int, refillRate float64, limit int) {
// 在RouteLimiterMap中爲給定的路由創建或更新限流器實例。
RouteLimiterMap[route] = NewRateLimiterV2(capacity, refillRate, limit)
}
// MiddlewareWithRoute 函數返回一個Gin中間件處理函數。
// 該中間件基於路由名稱來應用限流邏輯。
func MiddlewareWithRoute(route string) gin.HandlerFunc {
// 返回一個Gin的處理函數,該函數內部封裝了限流邏輯。
return func(c *gin.Context) {
// 檢查RouteLimiterMap中是否存在對應路由的限流器。
// 如果存在,調用其Allow方法來決定當前請求是否應該被允許。
if !RouteLimiterMap[route].Allow() {
// 如果請求被限流(不允許),返回HTTP 429狀態碼和錯誤信息。
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
c.Abort() // 中斷請求的進一步處理。
return // 退出中間件函數。
}
// 如果請求未被限流,調用c.Next繼續執行Gin的處理鏈。
c.Next()
}
}
反饋機制
反饋機制在請求被限流時向用戶提供適當的反饋,如錯誤消息或重試後的時間。
僞代碼示例:
// AllowWithFeedback 提供反饋的請求允許邏輯。
func (r *RateLimiterV2) AllowWithFeedback() (bool, string) {
r.mu.Lock()
defer r.mu.Unlock()
// 令牌桶邏輯...
if r.tokens >= r.limit {
return false, "Too many requests. Please try again later."
}
// 允許請求邏輯...
r.tokens-- // 移除令牌
return true, ""
}
// 使用反饋機制的中間件。
func MiddlewareWithFeedback() gin.HandlerFunc {
return func(c *gin.Context) {
allowed, message := RouteLimiterMap["/api/"].AllowWithFeedback()
if !allowed {
c.JSON(http.StatusTooManyRequests, gin.H{"error": message})
c.Abort()
return
}
c.Next()
}
}
5. 限流的考慮因素
在設計和實施限流機制時,需要綜合考慮多個關鍵因素以確保限流系統的有效性和公平性。
公平性
公平性是限流設計中的首要原則,確保所有用戶和客戶端能夠平等地訪問服務。
僞代碼示例:
// FairLimiter 結構體實現基於用戶ID或IP的公平限流。
type FairLimiter struct {
sync.Mutex
limits map[string]*RateLimiterV2 // 爲每個用戶或IP維護一個獨立的限流器
}
// NewFairLimiter 創建一個新的FairLimiter實例。
func NewFairLimiter(capacity int, refillRate float64) *FairLimiter {
return &FairLimiter{
limits: make(map[string]*RateLimiterV2),
}
}
// Allow 根據用戶ID或IP決定是否允許請求。
func (f *FairLimiter) Allow(userID string) (bool, string) {
f.Lock()
defer f.Unlock()
if _, exists := f.limits[userID]; !exists {
// 如果用戶沒有限流器,則創建一個新的。
f.limits[userID] = NewRateLimiterV2(capacity, refillRate, limit)
}
// 使用用戶的限流器檢查請求。
return f.limits[userID].AllowWithFeedback()
}
靈活性
靈活性意味着限流策略能夠適應不同的流量模式和業務需求,例如在高流量期間放寬限制。
僞代碼示例:
// FlexibleLimiter 結構體是一個靈活的限流器,允許在運行時動態調整限流參數。
type FlexibleLimiter struct {
sync.Mutex // 使用sync.Mutex提供互斥鎖功能,確保線程安全。
capacity int // 桶的容量,表示最多可以存儲的令牌數。
refillRate float64 // 令牌的填充速率,表示每秒可以新增的令牌數。
limit int // 請求處理的閾值,用於確定是否限流。
}
// SetParams 方法允許動態設置FlexibleLimiter的限流參數。
// 這些參數包括桶的容量、填充速率和請求處理的閾值。
func (f *FlexibleLimiter) SetParams(capacity int, refillRate float64, limit int) {
f.Lock() // 使用互斥鎖進入臨界區,防止併發訪問導致的數據不一致。
defer f.Unlock() // 離開臨界區前自動釋放鎖。
// 更新FlexibleLimiter的參數。
f.capacity, f.refillRate, f.limit = capacity, refillRate, limit
}
// Allow 方法根據FlexibleLimiter當前的參數決定是否允許新的請求。
// 它首先基於當前參數創建一個新的RateLimiterV2實例,然後調用它的AllowWithFeedback方法。
func (f *FlexibleLimiter) Allow() (bool, string) {
// 根據FlexibleLimiter當前的容量、填充速率和閾值創建一個新的RateLimiterV2實例。
rl := NewRateLimiterV2(f.capacity, f.refillRate, f.limit)
// 調用RateLimiterV2的AllowWithFeedback方法,獲取是否允許請求的反饋。
// 這個方法返回一個布爾值表示是否允許請求,和一個字符串消息提供反饋信息。
return rl.AllowWithFeedback()
}
透明性
透明性要求限流規則和當前狀態對用戶可見,使用戶能夠了解他們被限流的原因和情況。
僞代碼示例:
// TransparentLimiter 結構體嵌入了RateLimiterV2,提供了額外的狀態信息,
// 包括當前剩餘的令牌數,以增強限流機制的透明性。
type TransparentLimiter struct {
*RateLimiterV2 // 嵌入RateLimiterV2,獲得其所有功能。
currentTokens int // 存儲當前桶中剩餘的令牌數。
}
// AllowWithStatus 方法允許請求並返回當前限流狀態。
// 它調用內嵌RateLimiterV2的AllowWithFeedback方法來決定是否允許請求,
// 並獲取反饋消息,同時返回當前剩餘的令牌數。
func (t *TransparentLimiter) AllowWithStatus() (bool, string, int) {
allowed, message := t.RateLimiterV2.AllowWithFeedback() // 調用內嵌限流器的允許邏輯。
return allowed, message, t.currentTokens // 返回是否允許、消息和當前令牌數。
}
// MiddlewareWithTransparency 函數創建一箇中間件,用於在HTTP響應中包含限流狀態。
// 這個中間件包裝了下一個http.Handler,並在處理請求之前檢查限流狀態。
func MiddlewareWithTransparency(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 創建或使用全局的transparentLimiter實例來檢查限流狀態。
allowed, message, tokens := transparentLimiter.AllowWithStatus()
// 如果請求被限流(不允許),則設置HTTP頭部信息和狀態碼,並返回錯誤消息。
if !allowed {
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", tokens)) // 設置剩餘令牌數的頭部。
w.WriteHeader(http.StatusTooManyRequests) // 設置HTTP狀態碼爲429。
fmt.Fprintln(w, message) // 寫入錯誤消息到響應體。
return // 中斷請求處理。
}
// 如果請求未被限流,繼續執行後續的處理鏈。
next.ServeHTTP(w, r)
})
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/EJ68f40ebapdqKTgGOG8tw