Workerman需组合Limiter组件、自定义策略与外部存储实现多维动态限流;默认Limiter::check()仅支持单维度固定窗口,不适用于长连接场景下的用户+IP+接口+滑动窗口需求。

Workerman 本身不内置多维度动态限流逻辑,必须靠组合 Limiter 组件 + 自定义策略 + 外部存储驱动来实现。直接用 Limiter::check() 只能做单维度固定窗口限流,无法满足“按用户+IP+接口路径+时间滑动窗口”这类真实业务需求。
为什么默认 Limiter::check() 不适合长连接多维限流
默认调用 Limiter::check($key, $max, $ttl, $message) 的 $key 是静态字符串,无法自动提取请求上下文中的多个变量;$ttl 是固定过期时间,不支持滑动窗口(如“最近60秒内最多10次”);且所有维度都得手动拼接,容易出错或漏判。
- 长连接下,同一个 TCP 连接可能承载多个用户身份(比如 WebSocket 登录前后)、不同子协议、甚至跨租户请求,仅靠连接 ID 或 IP 限流会误杀或放行
- 使用
memory驱动时,限流状态无法在多进程间共享,导致实际通过量翻倍 - 用
apcu驱动虽可进程共享,但不支持分布式部署,且无法按需清理某类 key(如清空某个用户的全部限流记录)
用 Redis 驱动 + Lua 脚本实现滑动窗口多维限流
核心是把限流逻辑下沉到 Redis,用原子脚本避免竞态,同时让 key 携带多维标识。例如:一个 WebSocket 接口需限制「每个已登录用户每分钟最多发5条消息」,且要求滑动窗口:
- key 设计为
rl:msg:{user_id}:{ws_conn_id},其中{ws_conn_id}可从$connection->id获取,{user_id}来自连接绑定的 session 或 token 解析结果 - 不用
Limiter::check(),改写一个slidingWindowLimit($key, $windowSec = 60, $max = 5)函数,内部执行 Redis 的EVAL脚本 - 脚本逻辑:ZADD 记录当前时间戳 → ZREMRANGEBYSCORE 清理超窗旧记录 → ZCARD 获取当前窗口请求数 → 大于
$max则返回失败
示例关键片段(Redis Lua):
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max = tonumber(ARGV[3])
redis.call('ZADD', key, now, now)
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count > max then
return 0
else
return 1
end长连接场景下必须规避的三个坑
长连接生命周期远超 HTTP 请求,限流状态不能简单绑定到连接建立时刻,否则会导致“一连永逸”或“一连封死”:
-
不要在
onConnect里初始化限流 key —— 用户可能未登录,后续认证后才应开始计数;应在首次携带有效凭证的onMessage中生成并校验 -
避免用
$connection->getRemoteIp()作为主维度 —— WebSocket 代理后 IP 可能被覆盖,且同一 IP 下多个用户会互相干扰;优先用业务身份(user_id / app_key) -
定时清理失效 key 很关键 —— 用户断连后,其限流 key 若长期滞留 Redis,会浪费内存并影响新连接分配;建议在
onClose中主动DEL对应 key,或给 key 设置较短的EXPIRE(如 2 小时)
真正难的不是写限流代码,而是厘清哪些维度该组合、窗口粒度怎么定、失败后是否要透传具体原因(比如返回 Retry-After 头),这些都得贴着业务规则来设计,没法套模板。

















