ratelimiter 的限制规则在用户模型的 getratelimit() 和 loadallowance() 方法中定义,前者返回 [最大请求数, 时间窗口秒数],后者返回 [剩余配额, 上次更新时间戳]。

RateLimiter 的限制规则在哪定义?
Yii2 的限流逻辑不靠配置文件或全局开关驱动,而是由用户模型(User)实现的两个方法决定:getRateLimit() 和 loadAllowance()。只要这个类实现了 RateLimitInterface,框架就会在请求进入时自动调用它们。
-
getRateLimit($request, $action)返回数组[最大请求数, 时间窗口秒数],比如[10, 60]表示“每分钟最多 10 次” -
loadAllowance($request, $action)返回当前剩余配额和上次检查时间戳,即[$this->allowance, $this->allowance_updated_at]
常见错误:只改了 getRateLimit() 却没同步更新数据库字段或缓存逻辑,导致限流始终按旧值跑;或者返回了 null 或非整数,触发静默跳过限流(日志里只写 Rate limit skipped: user not logged in)
未登录用户怎么加限流?不能直接用 RateLimiter 过滤器
默认情况下,RateLimiter 过滤器依赖已登录用户对象,如果 Yii::$app->user->identity 是 null,它会直接跳过限流,不会报错,也不会限流——这是最容易被忽略的坑。
要支持未登录用户(如公开 API、注册页、登录页),必须绕开原生过滤器,手动实现:
- 基于 IP 限流:用
$request->getUserIP()当作 key,存到 Redis(推荐)或文件缓存 - 全局限流:所有请求共用一个计数器,适合防御爬虫或突发流量
- 自定义行为类:继承
yii\filters\RateLimiter并重写isRateLimited(),把用户判空逻辑替换成 IP 提取逻辑
注意:别在控制器里手写 if (++$count > 100) throw new TooManyRequestsHttpException() —— 缺少时间窗口滑动、配额恢复等关键逻辑,容易被绕过。
存储从数据库切到 Redis 怎么做?
原生限流默认读写用户表的 allowance 和 allowance_updated_at 字段,高并发下易成瓶颈。换成 Redis 只需两步:
- 在
User::loadAllowance()中,用 IP 或用户 ID 为 key,从 Redis 读取 JSON:Yii::$app->cache->get("rate<em>limit</em>{$key}") - 在
User::saveAllowance()中,把新值存回:Yii::$app->cache->set("rate<em>limit</em>{$key}", [$allowance, $timestamp], 3600)
关键细节:
-
Yii::$app->cache必须配置为 Redis 缓存组件(不是 file cache) - key 要区分登录/未登录场景,比如登录用户用
"rate_limit<em>user</em>{$this->id}",未登录用"rate_limit<em>ip</em>{$request->getUserIP()}" - 别漏掉
saveAllowance()的调用时机——它只在配额被消耗后才触发,不是每次请求都执行
为什么 X-RateLimit-Reset 头总是不准?
这个响应头的值由 RateLimiter::calculateWaitSeconds() 计算,本质是“当前时间窗口结束还剩几秒”。但它基于服务器本地时间,且不校准客户端时钟或网络延迟。
典型问题:
- 服务部署在多个时区服务器上,时间不同步 → 头部值混乱
- 时间窗口设为
60秒,但用户在第 59 秒发起请求,头部却显示1,实际可能因处理延迟已跨窗 - Redis 存储的时间戳用了
time(),而计算 wait 时用了microtime(true),精度不一致
解决办法:统一用 time()(秒级)做所有时间运算;上线前用 date -s 校准所有服务器时间;不要依赖该 header 做前端倒计时,仅作服务端参考。
改限流规则不是改个数字就完事,真正卡住人的永远是存储一致性、未登录路径的兜底逻辑,还有那个看起来很准、其实常飘的 X-RateLimit-Reset。











