Global Limit 插件
插件功能
基於白名單的全侷限流插件,對指定的域名和URL路徑進行全侷限流控制,共享同一個限流計數器。
核心特性
- 按域名 + URL路徑進行全侷限流
- 使用 Redis Sorted Set 實現滑動時間窗口
- 白名單機制:只對配置的域名和路徑進行限流
- 支持正則表達式匹配URL路徑
實現邏輯
1. 請求處理流程
請求到達 → 檢查域名白名單 → 檢查路徑白名單 → Redis限流判斷 → 放行/拒絕
2. 核心組件
配置解析 (parseConfig)
- 解析域名白名單
hosts - 解析路徑白名單
paths(支持正則表達式) - 配置限流參數:
unitSecond: 統計週期,默認30秒qpm: 週期內最大請求數,默認10次key: Redis存儲的key名稱
- 初始化 Redis 客户端連接
域名過濾 (SkipHost)
- 只有在白名單中的域名才進行限流
- 未在白名單中的域名直接放行
路徑過濾 (SkipPath)
- 使用路徑過濾器匹配URL
- 支持正則表達式匹配
- 未匹配的路徑直接放行
3. 限流算法 - 滑動窗口
採用 Redis Sorted Set + Lua 腳本 實現滑動窗口限流:
-- Lua 腳本執行原子操作
1. ZREMRANGEBYSCORE: 刪除過期數據(分數 < now - window)
2. ZCOUNT: 獲取當前窗口內的請求數
3. 判斷 count >= limit,超過則返回1
4. ZADD: 添加新請求(score=時間戳,member=UUID)
5. EXPIRE: 設置key過期時間
6. 返回0表示未超限
關鍵特性
- 原子性:Lua 腳本保證操作的原子性
- 滑動窗口:基於時間戳的精確滑動窗口
- 自動過期:過期時間設為統計週期的2倍
- UUID去重:每個請求使用唯一ID作為成員
4. 限流響應
超過限制時返回:
HTTP 429 Too Many Requests
{
"code": 429,
"message": "Too Many Requests"
}
配置參數
| 參數 | 類型 | 必填 | 默認值 | 説明 |
|---|---|---|---|---|
| serviceName | string | 是 | - | Redis服務名稱 |
| servicePort | int | 是 | - | Redis端口 |
| domain | string | 是 | - | Redis域名 |
| username | string | 是 | - | Redis用户名 |
| password | string | 是 | - | Redis密碼 |
| timeout | int | 否 | - | 連接超時時間(ms) |
| hosts | []string | 是 | - | 域名白名單 |
| paths | []string | 是 | - | URL路徑白名單(支持正則) |
| unitSecond | int | 否 | 30 | 統計週期(秒) |
| qpm | int | 否 | 10 | 週期內最大請求數 |
| key | string | 否 | global-limit-plugin-key | Redis key名稱 |
配置示例
serviceName: "test-redis-service"
servicePort: 6379
domain: "xxx.redis.rds.aliyuncs.com"
username: "user"
password: "password"
timeout: 50000
hosts:
- "api.example.com"
- "www.example.com"
paths:
- "/auth/token"
- "/api/sensitive/.*"
unitSecond: 60
qpm: 100
key: "my-global-limit"
與 Route Limit 的區別
| 特性 | Global Limit | Route Limit |
|---|---|---|
| 限流維度 | 全局共享計數器 | 每個URL獨立計數 |
| 配置方式 | 統一配置qpm | 每個URL單獨配置 |
| 適用場景 | 整體流量控制 | 精細化接口限流 |
| 路徑過濾 | 白名單過濾 | 規則匹配 |
使用場景
- 全站流量控制:對整個站點進行統一的流量限制
- 核心接口保護:對重要接口進行全局訪問頻率控制
- 防止突發流量:在高併發場景下保護後端服務
- 白名單限流:只對特定域名和路徑進行限流保護
核心代碼
now := time.Now()
nowTimestamp := now.Unix() //秒數
intervalTime := int64(config.unitSecond)
// 使用 Lua 腳本實現:清理過期數據 + 計數 + 添加新記錄 + 設置過期時間
// 返回值:0 表示未超限,1 表示已超限
luaScript := `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]
local expire_time = tonumber(ARGV[5])
-- 刪除過期數據(分數小於 now - window 的成員)
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
-- 獲取當前窗口內的請求數
local count = redis.call('ZCOUNT', key, now - window, now)
if count >= limit then
return 1 -- 超過限制
end
-- 添加新請求
redis.call('ZADD', key, now, member)
-- 設置key的過期時間,防止key永久存在
redis.call('EXPIRE', key, expire_time)
return 0 -- 未超過限制
`
// 準備參數
var keyArr []interface{}
keyArr = append(keyArr, config.key)
var valueArr []interface{}
uuid := uuid.New()
expireTime := config.unitSecond * 2 // 過期時間設為統計週期的2倍
valueArr = append(valueArr, nowTimestamp, intervalTime, config.qpm, uuid.String(), expireTime)
// 執行 Lua 腳本
err := config.Client.Eval(luaScript, 1, keyArr, valueArr, func(response resp.Value) {
if response.Integer() == 1 {
// 超過限制
fmt.Println("TOO_MANY_REQUESTS 429 ,path:", ctx.Path(), ",ipAddress:", util.GetClientIP())
headers := [][2]string{{"Content-Type", "application/json"}}
proxywasm.SendHttpResponse(429, headers, []byte("{\"code\":429,\"message\":\"Too Many Requests\"}"), -1)
} else {
// 未超過限制,繼續請求
proxywasm.ResumeHttpRequest()
}
})
if err != nil {
log.Errorf("rate limit error while calling redis: %v", err)
proxywasm.ResumeHttpRequest()
}
return types.ActionPause