PHP 防刷限流设计:固定窗口、滑动窗口与令牌桶的取舍

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-15 20:16 ·6 浏览 ·0 回复

结论先行:防刷限流没有「最优算法」,只有「最合适的力度」——登录爆破场景固定窗口就够用,发帖回帖这类高频小额场景选滑动窗口,需要容忍突发的开放 API 才上令牌桶。而且如果系统本身已经内置了防刷能力,先调参数往往比自己造轮子划算得多。

固定窗口:三行代码搞定,代价是边界会漏

固定窗口是最省事的方案:把时间切成片(比如 60 秒),用 Redis 计数器累加,首次写入时设过期。

$key = 'rl:post:' . $uid . ':' . floor(time() / 60);
$n = $redis->incr($key);
if ($n === 1) $redis->expire($key, 60);
if ($n > 10) exit('发帖太频繁,请稍后再试');

结论:固定窗口的硬伤是窗口边界突刺——攻击者在 00:59 打满 10 次,01:00 立刻再打 10 次,等于 2 秒内放行了 20 次。对精度要求不高的行为(签到、点赞、埋点上报)完全够用;想缓解可以维护「当前窗口 + 上一窗口」做加权计数,代码量只多几行。

滑动窗口:精度与成本的最佳平衡点

结论:滑动窗口回答的是「最近 N 秒内到底发生了几次」,这是绝大多数内容社区限流的首选。

两种实现路径:

  • ZSET 精确版:每次请求 `ZADD` 一个时间戳,`ZREMRANGEBYSCORE` 清掉窗口外的成员,`ZCARD` 判断是否超限。精度最高,但每个请求留一条 member,热点用户会吃掉大量内存,务必用 Lua 脚本包成原子操作,否则并发下判断和写入会脱节。
  • 分桶近似版:把 60 秒切成 6 个 10 秒的桶,求和「当前桶 + 前 5 个桶」。误差不超过 1 个桶(10 秒),内存恒定,生产环境我更推荐这个。

两者都属于「计数型」限流:不看请求内容,只看次数。

令牌桶:需要允许突发的场景才用它

结论:令牌桶的本质是「桶里还有存货就瞬时放行」,桶容量决定突发上限,补充速率决定长期平均速率。

典型实现是惰性补充:记一个 `last` 时间戳,每次请求按 `(now - last) × rate` 补令牌,上限不超过 capacity,然后扣 1。跟滑动窗口一样,判断和扣减必须放 Lua 里原子执行。

它适合开放 API 配额、第三方平台调用、消息推送这类「平时闲着、偶尔一涌而上」的场景。反过来,登录防爆破不要用令牌桶——登录要的是「越试越紧」,令牌桶反而给了攻击者一次性掏空整桶的机会,除非你把容量设成 1(那其实就是漏桶了)。

怎么选:按场景对号入座

场景推荐算法理由
登录失败、短信验证码固定窗口 + 递增惩罚简单、可审计、失败即锁
发帖、回帖、评论滑动窗口(分桶版)精度够、内存可控
上传、导出等重操作滑动窗口 + 并发计数次数与并发都要管
开放 API / 推送令牌桶允许合理突发

落地时的五个坑

结论:限流写错,八成不是算法选错,而是键设计和降级没做对。

  1. 键必须带维度:`rl:{action}:{uid}` 和 `rl:{action}:{ip}` 是两套独立策略,只按 IP 限会误伤同一出口 NAT 下的整个宿舍。
  2. Redis 挂了不能默认放行:降级到文件锁或 APCu 计数,宁可误杀,不可裸奔。
  3. 取 IP 要认准信任链:`X-Forwarded-For` 可伪造,只有在确认前置代理后才取其中一段。
  4. 提示要具体:「操作过于频繁,请 32 秒后重试」比「系统错误」有用得多。
  5. 先调系统自带的。以 Clara BBS 为例,后台就有发帖/回帖最小间隔、注册验证问答、敏感词拦截、IP 封禁(支持定时),登录侧还有同 IP 高频失败拦截、连错锁定(默认 5 次锁 15 分钟、后台可配)、到期自动解锁和全程登录审计日志(后台「用户体系→登录日志」可查)。这些是现成的,先用它们把基线兜住,再针对业务特有动作写自定义限流,才不会重复劳动。

收个尾:固定窗口求「快」,滑动窗口求「准」,令牌桶求「弹」。选型时先问一句——这个接口被刷,最坏结果是多写几行数据库,还是多花几千块短信费?答案不同,算法自然不同。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-383.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~