基于 Redis 的访问限流方法

基于 Redis 的访问限流主要有两种常见方法:计数器限流(固定窗口)滑动窗口限流。它们各有优缺点,适用于不同的场景。


1. 计数器限流(固定窗口)

实现方式

  • 使用 Redis 的 INCR + EXPIRE 实现固定时间窗口内的请求计数。
  • 例如:限制 100 请求/分钟,每分钟重置计数器。

PHP 示例代码

function isRateLimited(string $key, int $limit, int $windowSeconds): bool
{
    $redis = Redis::connection();
    $current = $redis->incr($key);

    if ($current === 1) {
        $redis->expire($key, $windowSeconds); // 首次设置过期时间
    }

    return $current > $limit;
}

调用方式

$key = "rate_limit:user_123"; // 限流 key
$limit = 100;                 // 100 次/分钟
$windowSeconds = 60;          // 60 秒窗口
if (isRateLimited($key, $limit, $windowSeconds)) {
    throw new Exception("请求太频繁,请稍后再试");
}

优点

实现简单,适合低并发场景
性能高,仅需 INCR + EXPIRE
内存占用低,仅存储一个计数

缺点

临界时间问题(窗口边界可能突破限制)

  • 例如:
    • 00:59 发送 100 次请求
    • 01:00 又发送 100 次请求
    • 实际 2 秒内处理了 200 次请求,超出预期

2. 滑动窗口限流

实现方式

  • 使用 Redis 的 ZSET(有序集合)记录每次请求的时间戳
  • 每次请求时,清理过期的请求,并计算当前窗口内的请求数

PHP 示例代码

function isRateLimitedSlidingWindow(string $key,int $limit,int $windowSeconds): bool {
    $redis = Redis::connection();
    $now = microtime(true) * 1000; // 毫秒时间戳
    $windowStart = $now - ($windowSeconds * 1000);

    // 1. 移除过期请求
    $redis->zremrangebyscore($key, 0, $windowStart);

    // 2. 获取当前窗口内的请求数
    $currentCount = $redis->zcard($key);

    if ($currentCount >= $limit) {
        return true; // 限流
    }

    // 3. 记录当前请求
    $redis->zadd($key, $now, $now);
    $redis->expire($key, $windowSeconds); // 避免 ZSET 无限增长

    return false;
}

调用方式

$key = "rate_limit_sliding:user_123";
$limit = 100;      // 100 次/分钟
$windowSeconds = 60;
if (isRateLimitedSlidingWindow($key, $limit, $windowSeconds)) {
    throw new Exception("请求太频繁,请稍后再试");
}

优点

更精确的限流,避免固定窗口的临界问题
平滑控制,不会出现短时间突发流量

缺点

实现较复杂,需要 ZSET 操作
性能稍低,每次请求需清理过期数据
内存占用更高,存储所有请求时间戳


3. 两种方法的对比

对比项 计数器(固定窗口) 滑动窗口
实现复杂度 ⭐⭐(简单) ⭐⭐⭐⭐(较复杂)
性能 ⭐⭐⭐⭐(高) ⭐⭐⭐(稍低)
内存占用 ⭐(低) ⭐⭐(较高)
精确度 ❌(临界问题) ✅(更精确)
适用场景 低并发、允许少量突发 高并发、严格限流

4. 如何选择?

  • 计数器(固定窗口)

    • 适用于 API 限流、低频业务(如登录防暴力破解)
    • 精确度要求不高,但需要 高性能 的场景
  • 滑动窗口

    • 适用于 高并发 API、支付接口、秒杀系统
    • 需要 严格限流,避免突发流量

5. 进阶方案

  • 令牌桶(Token Bucket)
    • 使用 Redis + Lua 实现更平滑的限流(如 Google GuavaRateLimiter
  • 漏桶(Leaky Bucket)
    • 适用于 恒定速率处理(如消息队列消费限速)

如果你需要 更严格的限流,建议使用 滑动窗口令牌桶。如果只是简单限流,计数器 就够用了。