基于白名单的全局限流插件,对指定的域名和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