本文深入探讨了速率限制算法,包括固定窗口计数器、滑动窗口日志、Token桶和漏桶算法,提供了Node.js代码示例,并讨论了每种算法的适用场景和优缺点。同时,文章还分析了在生产环境中Token桶算法成为最常用选择的原因,以及与其他算法相比的优势,例如灵活性、可扩展性和用户体验。
想象一下,你有一个流畅的服务器——甚至可能是一个区块链强力服务器——但突然冒出一群机器人,把它当成自助餐一样对待。请求像打折店的黑色星期五一样涌入,你可怜的系统喘不过气来。这时,速率限制就闪亮登场了——无名英雄,数字保镖,他说:“对不起,伙计,每分钟五个请求,请取号。” 在本文中,我们将一头扎进速率限制算法的狂野世界,解开它们的秘密,并启动一些巧妙的实现。
速率限制只是一种控制客户端(无论是用户、IP 或某些隐藏的 API 密钥)在设定时间内可以骚扰你的服务器多少次的巧妙方法。它是你抵御过载的盾牌,是你公平使用的通行证,也是你对拒绝服务(DoS)攻击或过度热心的机器人的鄙视。把它想象成交通警察,控制着互联网的混乱。请继续关注我们,到最后,你不仅会明白为什么它会改变游戏规则,还会明白如何像专业人士一样把它应用到你自己的项目中。准备好智胜数字踩踏事件了吗?让我们开始吧。
一个经典的例子是 Twitter 的 API 速率限制。2023 年,Twitter(现在的 X)将免费 API 访问限制为每月 1,500 条推文,并施加了读取限制(例如,经过验证的用户每月 10,000 篇帖子)。这可以防止机器人抓取大量数据集,确保病毒事件期间的服务器稳定性,并将用户推向付费层。如果没有速率限制,单个用户可能会淹没 API,从而降低数百万用户的服务质量。
以下是四种常见的速率限制算法,以及使用 Express.js 的简单 Node.js 实现。
每种算法都假定一个基本的 npm install express
设置。
何时使用
区块链示例: 限制托管区块链服务上的钱包创建请求(例如,每个 IP 每小时 10 个钱包),其中确切的时间安排不是问题。
避免在以下情况下使用: 你需要平稳地处理突发流量或防止边缘情况下的过载(例如,晚上 11:59 的 5 个请求,凌晨 12:00 的另外 5 个请求)。
const express = require('express');
const app = express();
const requests = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const MAX_REQUESTS = 5;
app.use((req, res, next) => {
const now = Date.now();
const userId = req.ip; // 使用 IP 作为标示符
if (!requests[userId] || requests[userId].windowStart < now - WINDOW_SIZE) {
requests[userId] = { count: 1, windowStart: now };
} else if (requests[userId].count < MAX_REQUESTS) {
requests[userId].count++;
} else {
return res.status(429).send('Too Many Requests');
}
next();
});
app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000, () => console.log('Server on port 3000'));
它的作用: 想象一下一个日记本,你每次发出请求时都会记下来。你被允许在过去 60 秒内发出 5 个请求,无论它们是什么时候发生的。旧的条目会随着时间的推移而被擦除。
何时使用:
区块链示例: 速率限制对以太坊节点的 API 调用(例如,每个密钥每小时 100 个请求),以准确地提供历史交易数据而不会过载。
避免在以下情况下使用: 你正在处理数百万用户(例如,公共区块链浏览器),因为存储时间戳在内存和成本方面扩展性不佳。
const express = require('express');
const app = express();
const requestLog = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const MAX_REQUESTS = 5;
app.use((req, res, next) => {
const now = Date.now();
const userId = req.ip;
requestLog[userId] = requestLog[userId] || [];
requestLog[userId] = requestLog[userId].filter(ts => ts > now - WINDOW_SIZE);
if (requestLog[userId].length < MAX_REQUESTS) {
requestLog[userId].push(now);
next();
} else {
res.status(429).send('Too Many Requests');
}
});
app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);
何时使用:
区块链示例: 限制向Layer2汇总提交交易(例如,5 个 tx/秒,带有 20 个Token的桶),以处理代币销售期间的峰值负载。
避免在以下情况下使用: 你需要严格、平稳的输出(没有突发),或者计算资源非常有限,因为重新填充逻辑会增加轻微的复杂性。
const express = require('express');
const app = express();
const buckets = {};
const CAPACITY = 5;
const REFILL_RATE = 1; // 每秒 1 个Token
app.use((req, res, next) => {
const now = Date.now() / 1000; // 以秒为单位
const userId = req.ip;
buckets[userId] = buckets[userId] || { tokens: CAPACITY, lastRefill: now };
const timePassed = now - buckets[userId].lastRefill;
const newTokens = timePassed * REFILL_RATE;
buckets[userId].tokens = Math.min(CAPACITY, buckets[userId].tokens + newTokens);
buckets[userId].lastRefill = now;
if (buckets[userId].tokens >= 1) {
buckets[userId].tokens--;
next();
} else {
res.status(429).send('Too Many Requests');
}
});
app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);
何时使用:
区块链示例: 限制对比特币节点的块数据查询(例如,1 个查询/秒),以避免在高需求期间压垮对等网络。
避免在以下情况下使用: 丢弃请求是不可接受的(例如,关键的交易提交),或者你需要允许突发以改善用户体验。
const express = require('express');
const app = express();
const queues = {};
const QUEUE_SIZE = 5;
const LEAK_RATE = 1000; // 1 个请求/秒
app.use((req, res, next) => {
const now = Date.now();
const userId = req.ip;
queues[userId] = queues[userId] || { queue: [], lastLeak: now };
const timeSinceLeak = now - queues[userId].lastLeak;
const leaks = Math.floor(timeSinceLeak / LEAK_RATE);
if (leaks > 0) {
queues[userId].queue = queues[userId].queue.slice(leaks);
queues[userId].lastLeak += leaks * LEAK_RATE;
}
if (queues[userId].queue.length < QUEUE_SIZE) {
queues[userId].queue.push(now);
next();
} else {
res.status(429).send('Too Many Requests');
}
});
app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);
何时使用: 中等精度,但比滑动窗口日志具有更好的内存效率,适用于具有可变流量的 API(例如,提供块数据的区块链浏览器)。
const express = require('express');
const app = express();
const windows = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const SUB_WINDOW = 10 * 1000; // 10 秒
const MAX_REQUESTS = 5;
app.use((req, res, next) => {
const now = Date.now();
const userId = req.ip;
windows[userId] = windows[userId] || { slots: {}, lastUpdate: now };
// 清理旧的子窗口
const cutoff = now - WINDOW_SIZE;
for (let ts in windows[userId].slots) {
if (parseInt(ts) < cutoff) delete windows[userId].slots[ts];
}
// 当前的子窗口
const currentSlot = Math.floor(now / SUB_WINDOW) * SUB_WINDOW;
windows[userId].slots[currentSlot\
确定生产环境中“最常用”的速率限制算法,尤其是在区块链应用或通用系统中,是很棘手的,因为它很大程度上取决于具体的用例、系统架构和行业背景。没有一项通用的调查能够确定所有生产系统中的确切使用情况统计数据,但根据广泛的采用、实际优势和实际示例(包括区块链),Token桶算法脱颖而出,成为生产中最常用的算法。以下是原因,以及对比其他算法的介绍。
为什么使用最多:
- 原文链接: medium.com/@universalPho...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!