马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
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"
- }
复制代码 设置参数
参数范例必填默认值分析serviceNamestring是-Redis服务名称servicePortint是-Redis端口domainstring是-Redis域名usernamestring是-Redis用户名passwordstring是-Redis暗码timeoutint否-毗连超时时间(ms)hosts[]string是-域名白名单paths[]string是-URL路径白名单(支持正则)unitSecondint否30统计周期(秒)qpmint否10周期内最大哀求数keystring否global-limit-plugin-keyRedis 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 LimitRoute 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
复制代码 |