基于Redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的固定窗口限流器
大家好,我是漁夫子。
限流器是在大流量中保護(hù)服務(wù)資源的一種常用手段。限流器的實(shí)現(xiàn)有令牌桶方式、固定窗口限流器和滑動(dòng)窗口限流器。本文介紹了基于Redis如何快速的實(shí)現(xiàn)固定窗口限流器。
最近在我們的項(xiàng)目中需要快速的實(shí)現(xiàn)一個(gè)流量限流器,而目前項(xiàng)目中已經(jīng)有在用Redis了。
固定窗口限流器:它是在固定的時(shí)間窗口(例如一分鐘)內(nèi)計(jì)算接收到的請(qǐng)求數(shù)量。一旦達(dá)到最大請(qǐng)求數(shù)量,額外的請(qǐng)求將被拒絕,直到下一個(gè)窗口開(kāi)始。
要基于Redis實(shí)現(xiàn)固定窗口限流器非常簡(jiǎn)單,如下lua代碼:
local current
current = redis.call("INCR", KEYS[1])
if tonumber(current) == 1 then
redis.call("EXPIRE", KEYS[1], 60)
end
return current
每次運(yùn)行這個(gè)腳本時(shí),它都會(huì)獲取一個(gè)鍵并將其值遞增1。如果是第一次遞增該鍵時(shí),都會(huì)設(shè)置一個(gè)60秒的過(guò)期時(shí)間。它返回遞增后的當(dāng)前值。
該鍵在首次設(shè)置60秒后過(guò)期。一旦過(guò)期,它將在下一個(gè)請(qǐng)求時(shí)再次設(shè)置。
當(dāng)服務(wù)收到一個(gè)請(qǐng)求時(shí),就可以調(diào)用該段代碼。如果腳本返回的值大于允許的值,則由于速率限制而中止該請(qǐng)求。如果返回的值不大于允許的值,則處理該請(qǐng)求。
const script = `
local current
current = redis.call("INCR", KEYS[1])
if tonumber(current) == 1 then
redis.call("EXPIRE", KEYS[1], 60)
end
return current
`
func isRateLimited(ctx context.Context, key string, limit int64) (bool, error) {
v, err := redisClient.Eval(ctx, script, []string{key}).Result()
if err != nil {
return false, err
}
n, _ := v.(int64)
return n > int64(limit), nil
}
isRateLimited函數(shù)可以按如下方式使用:
func handleLogin(r *http.Request, w http.ResponseWriter) {
username := r.FormValue("username")
limited, _ := isRateLimited(context.TODO(), fmt.Sprintf("rateLimit:login:username:%s", username, 5))
if limited {
http.Error(w, "Too Many Attempts", http.StatusTooManyRequests)
return
}
// ...
}
這樣就可以工作了。
請(qǐng)注意,固定窗口限流器雖然可以有效抵御持續(xù)攻擊,但可能會(huì)影響合法用戶(hù)的體驗(yàn)。
在上面的示例中,我們基于在登錄流程中使用的用戶(hù)名進(jìn)行速率限制。如果是基于其他指標(biāo)進(jìn)行限流(例如傳入請(qǐng)求的遠(yuǎn)程IP地址),那么該限流器是不起作用的。