跳到主要内容

固定窗口限流

2025年01月06日
柏拉文
越努力,越幸运

一、认识


固定窗口限流 将时间划分为固定大小的时间段(如每分钟、每秒),在每个时间段内限制请求次数。当进入下一个时间段时,计数器会重置。具体步骤为: 在 Redis 中为每个用户或操作创建一个计数器键,键名包含时间窗口标识(如时间戳)。当用户发起请求时, 如果键不存在,则创建键并设置初始值为 1,并设置过期时间(等于窗口时长)。如果键已存在,则递增计数器。如果计数器值超过限制,拒绝请求。在固定时间窗口中,限流逻辑可能会出现的问题为: 如果窗口周期结束,计数会立即重置,可能导致流量突增。例如,某 API 每分钟限流 100 次,可能在最后一秒发送 100 次请求,而在下一秒又发送 100 次请求,导致 200 次请求集中在 2 秒内完成。对于精度要求较高的场景,这种计数方式可能不够精确。

二、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 rateLimiter = (key, limit, windowInSeconds) => {
return async (req, res, next) => {
try {
const currentCount = await redisClient.get(key);

if (currentCount) {
if (parseInt(currentCount) >= limit) {
return res.status(429).json({ message: 'Too many requests. Please try again later.' });
}
// 增加计数
await redisClient.incr(key);
} else {
// 初始计数并设置过期时间
await redisClient.set(key, 1, { EX: windowInSeconds });
}

next();
} catch (error) {
console.error('Error in rate limiter:', error);
res.status(500).json({ message: 'Internal Server Error' });
}
};
};

// 应用限流中间件
app.get(
'/api',
rateLimiter('rate_limit:global', 100, 60), // 限制全局请求每分钟不超过 100 次
(req, res) => {
res.json({ message: 'Request successful' });
}
);

// 限制用户在 60 秒内最多发送 3 条短信
app.post(
'/send-sms',
rateLimiter('rate_limit:sms:{user_id}', 3, 60), // 限制每个用户每分钟最多 3 条
(req, res) => {
res.json({ message: 'SMS sent successfully' });
}
);

// 启动服务器
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

三、Lua 脚本


-- 固定窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local windowInSeconds = tonumber(ARGV[2])

-- 增加计数器
local count = redis.call('INCR', key)

if count == 1 then
-- 第一次请求时设置过期时间
redis.call('EXPIRE', key, windowInSeconds)
end

-- 返回是否允许请求
if count <= limit then
return 1
else
return 0
end

四、Pipeline


如果并发量高,可以通过 Redis Pipeline 批量执行命令,减少网络开销。

五、Lua Node.js


使用 Redis Lua 脚本和 Node.js 服务结合,可以提升限流操作的效率,因为 Lua 脚本在 Redis 服务器端执行,使用 Lua 脚本, 将限流逻辑放到 Redis 服务端执行, 可以提升限流逻辑的性能和原子性,减少了多次 ZADDZCARD 等命令的网络往返,减少了网络通信开销, 避免分布式环境中的并发问题。具体步骤为: 首先加载 Lua 脚本到 Redis, 使用 Redis 客户端的 evalevalsha 方法执行 Lua 脚本。其次,定义脚本和参数,将键、参数和 Lua 脚本传递给 Redis。最后,处理返回值,根据 Lua 脚本返回的结果,决定是否允许请求。

const redis = require('redis');

const redisClient = redis.createClient();
redisClient.connect();

// 固定窗口限流函数
async function fixedWindowRateLimiter(key, limit, windowInSeconds) {
const luaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local windowInSeconds = tonumber(ARGV[2])
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, windowInSeconds)
end
if count <= limit then
return 1
else
return 0
end
`;

const result = await redisClient.eval(luaScript, {
keys: [key],
arguments: [limit, windowInSeconds],
});

return result === 1; // 1 表示允许请求,0 表示限流
}

// 示例:限制每分钟最多 100 次请求
(async () => {
const isAllowed = await fixedWindowRateLimiter('user:12345', 100, 60);
console.log(isAllowed ? 'Request allowed' : 'Rate limit exceeded');
})();

六、Pipeline Node.js


如果并发量高,可以通过 Redis Pipeline 批量执行命令,减少网络开销。