滑动窗口限流
一、认识
滑动窗口 通过更精细化的时间统计解决固定窗口的临界问题。它利用 Redis
有序集合(ZSET
)存储请求的时间戳,并动态调整统计范围,使得时间窗口可以 滑动。具体步骤为: 为每个用户或操作通过 ZADD
创建一个有序集合键,值为请求的时间戳。当用户发起请求时, 添加当前时间戳到有序集合,通过 ZREMRANGEBYSCORE
删除集合中超出时间窗口范围的旧时间戳, 通过 ZCARD
获取集合中当前时间窗口内的元素数量, 如果数量超过限制,拒绝请求, 并设置键的过期时间, 避免无用数据长期占用存储。滑动窗口 可以实现精细化统计,能够更精确地限制请求频率,避免流量突增,解决了固定窗口的临界问题,限流更加平滑,时间粒度更加精细。并具有高灵活性,可适用于更高要求的限流场景,例如对实时性要求较高的 API
。而且可以动态调整,可以灵活调整时间窗口和请求限制。
二、Node.js
const redis = require('redis');
const express = require('express');
const app = express();
app.use(express.json());
// 配置 Redis 客户端
const redisClient = redis.createClient();
redisClient.connect().catch(console.error);
// 滑动窗口限流中间件
const slidingWindowRateLimiter = (key, limit, windowInSeconds) => {
return async (req, res, next) => {
try {
const currentTime = Date.now();
const windowStart = currentTime - windowInSeconds * 1000;
// 添加当前请求时间戳到有序集合
await redisClient.zAdd(key, { score: currentTime, value: currentTime.toString() });
// 删除超出时间窗口的记录
await redisClient.zRemRangeByScore(key, 0, windowStart);
// 获取当前时间窗口内的请求数量
const requestCount = await redisClient.zCard(key);
if (requestCount > limit) {
return res.status(429).json({ message: 'Too many requests. Please try again later.' });
}
// 设置键的过期时间(为了防止长期积累)
await redisClient.expire(key, windowInSeconds);
next();
} catch (error) {
console.error('Error in sliding window rate limiter:', error);
res.status(500).json({ message: 'Internal Server Error' });
}
};
};
// 应用滑动窗口限流中间件
app.get(
'/api',
slidingWindowRateLimiter('rate_limit:api', 100, 60), // 每分钟最多 100 次
(req, res) => {
res.json({ message: 'Request successful' });
}
);
// 启动服务器
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
三、Lua 脚本
-- 滑动窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local currentTime = tonumber(ARGV[2])
local windowInSeconds = tonumber(ARGV[3])
-- 添加当前请求时间戳到有序集合
redis.call('ZADD', key, currentTime, currentTime)
-- 删除超出窗口范围的时间戳
local windowStart = currentTime - (windowInSeconds * 1000)
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
-- 获取当前窗口内的请求数量
local count = redis.call('ZCARD', key)
-- 设置过期时间
redis.call('EXPIRE', key, windowInSeconds)
-- 返回是否允许请求
if count <= limit then
return 1
else
return 0
end
四、Pipeline
如果并发量高,可以通过 Redis Pipeline
批量执行命令,减少网络开销。
五、Lua Node.js
使用 Redis Lua
脚本和 Node.js
服务结合,可以提升限流操作的效率,因为 Lua
脚本在 Redis
服务器端执行,使用 Lua
脚本, 将限流逻辑放到 Redis
服务端执行, 可以提升限流逻辑的性能和原子性,减少了多次 ZADD
、ZCARD
等命令的网络往返,减少了网络通信开销, 避免分布式环境中的并发问题。具体步骤为: 首先加载 Lua
脚本到 Redis
, 使用 Redis
客户端的 eval
或 evalsha
方法执行 Lua
脚本。其次,定义脚本和参数,将键、参数和 Lua
脚本传递给 Redis
。最后,处理返回值,根据 Lua
脚本返回的结果,决定是否允许请求。
const redis = require('redis');
const redisClient = redis.createClient();
redisClient.connect();
// 滑动窗口限流函数
async function slidingWindowRateLimiter(key, limit, windowInSeconds) {
const luaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local currentTime = tonumber(ARGV[2])
local windowInSeconds = tonumber(ARGV[3])
redis.call('ZADD', key, currentTime, currentTime)
local windowStart = currentTime - (windowInSeconds * 1000)
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
local count = redis.call('ZCARD', key)
redis.call('EXPIRE', key, windowInSeconds)
if count <= limit then
return 1
else
return 0
end
`;
const currentTime = Date.now();
const result = await redisClient.eval(luaScript, {
keys: [key],
arguments: [limit, currentTime, windowInSeconds],
});
return result === 1; // 1 表示允许请求,0 表示限流
}
// 示例:限制每分钟最多 100 次请求
(async () => {
const isAllowed = await slidingWindowRateLimiter('user:12345', 100, 60);
console.log(isAllowed ? 'Request allowed' : 'Rate limit exceeded');
})();
六、Pipeline Node.js
如果并发量高,可以通过 Redis Pipeline
批量执行命令,减少网络开销。