如何防止您的 API 在请求洪流中崩溃
\n\n想象一下:您启动了一个很棒的服務,用 TypeScript 编写了后端,然后——砰!——一个“好心”的用户运行了一个脚本,每秒向您的端点发送数千个请求。服务器崩溃,数据库不堪重负,而诚实的用户只能看到 502 错误。熟悉吗?
\n\n在 Web 开发世界中,rate limiting(速率限制) 和 throttling(节流) 不仅仅是时髦的词汇,更是您 API 的救生圈。今天,我们将探讨如何在不伤脑筋的情况下,用 TypeScript 实现这些机制。开始吧!
\n\nRate limiting vs Throttling:有什么区别?
\n\n在编写代码之前,让我们先明确区分这两个概念。很多人会混淆它们,但区别是根本性的。
\n\n- \n
- Rate limiting — 是一种硬性限制:“来自同一个 IP 的请求每分钟不超过 100 个”。超出限制?您将收到 429 Too Many Requests 错误,并在窗口期结束前被阻止。它在 API 网关层面工作。 \n
- Throttling — 是一种平滑调节:“您可以发送请求,但每秒不超过一个”。其他请求要么排队,要么被忽略,但用户不会被阻止。 \n
一个简单的类比:rate limiting 就像地铁的闸机(刷卡通过,否则等待),而 throttling 就像路口的交通指挥员(车辆通行,但有间隔)。
\n\n何时需要?真实场景
\n\n您可能会说:“我的网站很小,为什么需要这个?”原因如下:
\n\n- \n
- 防御 DDoS 和暴力破解 — 攻击者无法暴力破解密码或使服务器瘫痪。 \n
- 节省资源 — 付费 API(例如 OpenAI)每次调用都会收费。限制客户端可以节省预算。 \n
- 公平分配负载 — 一个“吵闹”的用户不会压垮所有其他用户。 \n
- 遵守 SLA — 如果您承诺 99.9% 的正常运行时间,没有限制是不行的。 \n
用 TypeScript 编写 Rate Limiting:简单入门
\n\n我们从基础示例开始——基于固定窗口(Fixed Window)的 in-memory rate limiter。它将请求计数器直接存储在应用程序内存中。
\n\n// rateLimiter.ts\nimport { Request, Response, NextFunction } from 'express';\n\ninterface RateLimitEntry {\n count: number;\n resetTime: number;\n}\n\nclass InMemoryRateLimiter {\n private store: Map<string, RateLimitEntry> = new Map();\n private windowMs: number;\n private maxRequests: number;\n\n constructor(windowMs: number, maxRequests: number) {\n this.windowMs = windowMs;\n this.maxRequests = maxRequests;\n }\n\n public middleware() {\n return (req: Request, res: Response, next: NextFunction): void => {\n const key = req.ip || 'unknown';\n const now = Date.now();\n let entry = this.store.get(key);\n\n if (!entry || now > entry.resetTime) {\n // 新窗口\n entry = { count: 0, resetTime: now + this.windowMs };\n this.store.set(key, entry);\n }\n\n entry.count++;\n\n if (entry.count > this.maxRequests) {\n res.status(429).json({\n error: 'Too Many Requests',\n retryAfter: Math.ceil((entry.resetTime - now) / 1000)\n });\n return;\n }\n\n // 在响应头中传递限制信息\n res.setHeader('X-RateLimit-Limit', this.maxRequests);\n res.setHeader('X-RateLimit-Remaining', this.maxRequests - entry.count);\n res.setHeader('X-RateLimit-Reset', Math.ceil(entry.resetTime / 1000));\n\n next();\n };\n }\n\n // 清理旧记录(防止内存泄漏)\n public cleanup(): void {\