[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fMfuBBMXbSPEwVlY33K0ZKBzJffB8Z4n3Y5BPf9CjKIk":3},{"item":4,"related":40},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":31,"sourceName":32,"sourceUrl":32,"status":33,"seoTitle":32,"seoDescription":32,"canonicalUrl":32,"isFeatured":34,"sno":35,"sortOrder":36,"publishedAt":37,"updatedAt":38,"createdAt":39},"86b63309-e12a-41b4-9d9e-db95480617b0","article","令牌桶限流：API 如何把流量洪峰变成可控波浪","token-bucket-rate-limiting-explained","令牌桶把长期平均速率和短时突发容量分开，是 API 限流中最常见的模型之一。本文从令牌补充、请求成本和 429 响应讲起，比较固定窗口、滑动窗口与漏桶，并拆解分布式限流的真实难点。","API 最怕的不是“有一个请求很慢”，而是某一秒突然涌进几十万请求。数据库连接池被占满，线程开始排队，重试又制造更多请求，最后一个本来健康的服务被拖成雪崩。令牌桶限流的做法很直观：先往桶里按固定速度放令牌，每个请求来时拿走一个；没有令牌，就等待、拒绝或降级。\n\n![令牌桶算法示意图](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FSpecial:FilePath\u002FQoS%20tocken%20bucket.svg)\n\n令牌桶属于一类流量整形与速率控制算法。IETF 的相关规范也使用“令牌桶”描述可用容量、补充速率和到达流量之间的关系。[RFC 2697](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc2697.html)；当 HTTP 服务拒绝过快的请求时，通常会返回 `429 Too Many Requests`，这个状态码定义在 [RFC 6585](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6585.html)。\n\n## 桶里到底有什么\n\n令牌桶至少有两个参数：\n\n- `capacity`：桶最多能存多少令牌，决定允许多大的瞬时突发。\n- `refillRate`：每秒补充多少令牌，决定长期平均速率。\n\n假设桶容量是 5，每秒补充 2 个令牌，每个请求消耗 1 个令牌：\n\n- 长时间平均下来，最多放行约 2 个请求\u002F秒。\n- 如果桶之前攒满了，瞬间可以连续放行 5 个请求。\n- 5 个令牌用完后，后续请求要等补充，或者直接收到 429。\n\n这解释了它比“每秒固定只能两个请求”更灵活：正常情况下允许一小段突发，但不会让突发无限延长。\n\n## 令牌桶的核心算法\n\n每次请求到达时，系统先按照距离上次计算的时间补充令牌，再判断余额够不够。补充不能超过桶容量。\n\n```text\ntokens = min(capacity,\n             tokens + (now - lastTime) * refillRate)\nlastTime = now\n\nif tokens >= requestCost:\n    tokens -= requestCost\n    allow()\nelse:\n    reject_or_wait()\n```\n\n实际代码要处理小数令牌、时间精度和并发更新。若一个请求消耗的资源差异很大，可以让读取接口消耗 1 个令牌、导出报表消耗 10 个令牌，而不是所有请求一视同仁。\n\n```mermaid\nflowchart TD\n    A[请求到达] --> B[按时间补充令牌]\n    B --> C{令牌够请求成本吗}\n    C -->|够| D[扣除令牌并放行]\n    C -->|不够| E{策略选择}\n    E -->|等待| F[排队后重试]\n    E -->|拒绝| G[返回 429]\n    E -->|降级| H[返回缓存或简化结果]\n```\n\n## 它和其他限流算法有什么区别\n\n### 固定窗口：简单，但窗口边界会放大突发\n\n“每分钟最多 60 次”很容易实现，但用户在 12:00:59 发 60 次，12:01:00 再发 60 次，短时间内可能通过 120 次。固定窗口适合粗粒度保护，却不擅长平滑流量。\n\n### 滑动窗口：更精确，但需要保存更多请求记录\n\n滑动窗口会观察最近一段时间的请求，更公平，但高并发下需要维护计数或时间桶，存储和计算成本高于固定窗口。\n\n### 漏桶：输出更平滑\n\n漏桶更强调固定速度地处理流量，像一个底部开口大小固定的桶。它适合需要平滑输出的队列，但如果业务希望“攒一会儿后允许短突发”，令牌桶通常更贴合。\n\n## 真正难的是“按谁限流”\n\n算法本身不复杂，难点是限流键和状态放在哪里。常见限流维度包括用户 ID、API Key、IP、租户、路由和全局服务。只按 IP 限制可能误伤公司网络后的所有用户，只按用户限制又挡不住匿名攻击者；生产系统经常组合多个维度。\n\n单机内存里的令牌桶只能保护一个进程。如果服务有多个实例，每个实例都允许 2 次\u002F秒，集群总量可能变成实例数乘以 2。要得到全局上限，就需要共享计数状态或让流量先经过统一网关。共享状态又带来原子更新、网络延迟和故障降级问题。\n\n一个常见实现会把 `tokens` 和 `lastTime` 放在 Redis 里，用 Lua 脚本一次完成补充和扣除，避免两个请求同时读到同一份余额。也可以按租户把请求路由到固定分片，减少共享状态，但这会牺牲部分弹性。\n\n## 429 不是一句“太快了”就结束\n\n服务返回 429 时，客户端需要知道如何恢复。响应可以附带 `Retry-After`，告诉客户端等待多久再试；客户端也应该使用指数退避和随机抖动，避免所有请求在同一时间再次冲回来。[RFC 6585 对 429 和 Retry-After 的说明](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6585.html)\n\n服务端还要区分：\n\n- 这是某个用户的配额耗尽，还是整个服务过载？\n- 请求是可以排队，还是必须立即失败？\n- 是否有缓存、只读副本或简化结果可以降级？\n- 限流状态本身故障时，是偏向放行还是偏向拒绝？\n\n“限流成功”不代表所有请求都被拒绝得很漂亮，而是系统在压力下仍然能保住核心路径，并给调用方一个可恢复的信号。\n\n## 容易踩的坑\n\n第一，时钟不一致。分布式实例用本地时间计算会产生边界误差，需要统一时间来源或接受一定误差。\n\n第二，忽视请求成本。上传大文件和读取一个短字段都扣一个令牌，往往会让限流失去保护意义。\n\n第三，只在应用代码里限流。请求已经占用了连接、TLS 和线程后才被拒绝，数据库可能仍然承受了压力。更早的网关、连接层和资源池保护通常更有效。\n\n第四，把限流当成队列。令牌桶可以控制准入，但不自动解决任务执行时间；长任务仍然需要队列、超时、取消和幂等设计。\n\n## 一句话带走\n\n令牌桶的精髓是把“长期平均速率”和“短时突发容量”分开：桶的大小负责弹性，补充速度负责纪律。把它部署到正确的边界、配上明确的 429 与退避策略，才真正能把流量洪峰变成系统可以承受的波浪。\n\n## 延伸阅读\n\n- [RFC 2697：A Single Rate Three Color Marker](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc2697.html)\n- [RFC 6585：Additional HTTP Status Codes](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6585.html)\n- [Wikimedia Commons：Token bucket 示例图](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FFile:QoS_tocken_bucket.svg)","\u002Fuploads\u002F2026-08-06\u002F952eedca-0e08-4c7c-a8a6-d07f1acd5291.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27],{"id":24,"name":25,"slug":26},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":28,"name":29,"slug":30},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse","资料来源",null,"published",false,63,0,"2026-08-06T00:00:00.000Z","2026-08-06T05:53:22.597Z","2026-08-06T05:38:26.532Z",[41,49,57],{"id":42,"type":6,"title":43,"slug":44,"summary":45,"coverUrl":46,"authorName":14,"sno":47,"publishedAt":37,"createdAt":48},"d6ff32b0-f499-453a-b1e3-c31a4a7a5b16","布隆过滤器：系统如何快速判断一个东西肯定不存在","bloom-filter-probabilistic-membership-explained","布隆过滤器用位数组和多个哈希函数，以极少内存快速筛掉肯定不存在的对象。本文讲清误报与漏报、误报率、删除难题，以及它在缓存、数据库和去重系统中的正确用法。","\u002Fuploads\u002F2026-08-06\u002F234915f6-2ef2-4820-8523-1a98544732f9.jpg",61,"2026-08-06T05:38:25.381Z",{"id":50,"type":6,"title":51,"slug":52,"summary":53,"coverUrl":54,"authorName":14,"sno":55,"publishedAt":37,"createdAt":56},"23cd6a50-6716-4620-99f3-d5307511c8ee","CRDT：两个人同时改同一段文字，为什么不会互相覆盖","crdt-collaborative-editing-explained","CRDT 允许多个副本先本地修改，再通过可合并的数据结构最终收敛。本文用计数器和协同编辑讲清 CvRDT、CmRDT、并发插入、删除标记、元数据成本与业务语义冲突。","\u002Fuploads\u002F2026-08-06\u002F50641649-08d8-4cac-94f0-647eabe9fc5f.jpg",62,"2026-08-06T05:38:23.928Z",{"id":58,"type":6,"title":59,"slug":60,"summary":61,"coverUrl":62,"authorName":14,"sno":55,"publishedAt":37,"createdAt":63},"a04c10a4-3fe8-4537-81bd-354103c1078f","WebGPU：浏览器为什么能跑 3D、滤镜和部分 AI","webgpu-browser-gpu-computing-explained","WebGPU 让网页能更直接地使用 GPU 做渲染与通用计算。本文从 CPU 与 GPU 的分工讲起，拆解适配器、设备、缓冲区、WGSL 着色器和命令队列，并说明什么时候并行计算真的值得。","\u002Fuploads\u002F2026-08-06\u002F05f26f06-cda2-4ed3-b1f2-5f26c82c2a37.jpg","2026-08-06T05:38:22.834Z"]