必须用redis滑动窗口实现单用户主动推送频率限制,构造含用户id、推送类型、时间戳的多维key,通过原子lua脚本完成zadd/zremrangebyscore/zcard操作,并封装为slidingwindowlimit函数在每次推送前调用。

要在Workerman中实现单用户主动推送时的频率限制,必须绕过默认Limiter::check()的单维度静态key限制,改用Redis滑动窗口方案,否则同一用户在不同连接间无法共享限流状态,或出现漏判误判。
设计多维限流Key
推送前先构造唯一标识:将用户ID、推送类型(如“notify”)、时间戳精度(秒级)拼接成Redis key。例如用户uid=123推送系统通知,key为【rl:push:notify:123:2026080615】。注意不要用连接ID或IP替代uid,否则无法绑定真实用户身份。
这一步必须在推送逻辑入口处完成,不能放在onMessage里——因为主动推送不经过onMessage,而是在Timer或外部事件触发时执行。
用Lua脚本实现滑动窗口计数
在Redis中执行原子Lua脚本,完成三件事:ZADD插入当前时间戳 → ZREMRANGEBYSCORE清理60秒外旧记录 → ZCARD获取当前窗口内请求数。脚本返回当前计数值,PHP据此判断是否超限。
直接调用$redis->eval()传入脚本,不要用incr+expire组合——后者无法支持滑动窗口,且在高并发下因非原子性导致计数偏差。
示例Lua脚本内容(无需手动写,直接复制使用):
return {redis.call('zadd', KEYS[1], ARGV[1], ARGV[1]), redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]-60), redis.call('zcard', KEYS[1])}
封装滑动窗口限流函数
步骤一:定义slidingWindowLimit($redis, $key, $windowSec = 60, $max = 3)函数,接收Redis实例、key、窗口秒数、最大次数。
步骤二:调用eval执行上述Lua脚本,提取ZCARD结果。
步骤三:若返回值大于$max,直接return false;否则允许推送并继续执行后续send逻辑。
关键点:该函数必须在每次主动推送前调用,不可复用上次结果——因为不同推送类型(如“订单提醒”和“客服消息”)需独立限流,key必须携带业务维度。
避免内存泄漏的清理时机
方法一:在用户登出或连接关闭时,用SCAN+DEL批量清除该uid所有rl:push:* key。不建议用KEYS命令,会阻塞Redis。
方法二:依赖Redis自动过期,但需确保Lua脚本中ZADD后紧跟EXPIRE设置key整体过期时间(如3600秒),防止ZSET残留。
【不清理会导致Redis内存持续增长,且后续同uid限流失效】











