基于 Redis 的访问限流主要有两种常见方法:计数器限流(固定窗口) 和 滑动窗口限流。它们各有优缺点,适用于不同的场景。
INCR
+ EXPIRE
实现固定时间窗口内的请求计数。 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 次请求 ZSET
(有序集合)记录每次请求的时间戳 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
操作
❌ 性能稍低,每次请求需清理过期数据
❌ 内存占用更高,存储所有请求时间戳
对比项 | 计数器(固定窗口) | 滑动窗口 |
---|---|---|
实现复杂度 | ⭐⭐(简单) | ⭐⭐⭐⭐(较复杂) |
性能 | ⭐⭐⭐⭐(高) | ⭐⭐⭐(稍低) |
内存占用 | ⭐(低) | ⭐⭐(较高) |
精确度 | ❌(临界问题) | ✅(更精确) |
适用场景 | 低并发、允许少量突发 | 高并发、严格限流 |
计数器(固定窗口):
滑动窗口:
Redis
+ Lua
实现更平滑的限流(如 Google Guava
的 RateLimiter
) 如果你需要 更严格的限流,建议使用 滑动窗口 或 令牌桶。如果只是简单限流,计数器 就够用了。