博客 / 詳情

返回

wasm~tinygo寫一個基於redis的全侷限流的插件

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單獨配置
適用場景 整體流量控制 精細化接口限流
路徑過濾 白名單過濾 規則匹配

使用場景

  1. 全站流量控制:對整個站點進行統一的流量限制
  2. 核心接口保護:對重要接口進行全局訪問頻率控制
  3. 防止突發流量:在高併發場景下保護後端服務
  4. 白名單限流:只對特定域名和路徑進行限流保護

核心代碼

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
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.