Skip to main content
LadeStack LogoLadeStack
Backend 约 7 分钟 2024年9月9日

API 限流与节流策略:高并发生产架构完全指南

深入剖析分布式系统中的 API 限流与节流算法。基于令牌桶、滑动窗口、Redis Lua 脚本原子实现以及标准 HTTP 响应头的设计实战。

API 限流与节流架构策略图

现代 Web 应用与分布式 API 时刻面临着高并发流量波动的严峻考验。无论是营销活动带来的突发流量洪峰、网络爬虫的高频采集、自动化系统的重试风暴,还是恶意分布式拒绝服务(DoS)攻击,缺乏流量整形(Traffic Shaping)保护的 API 都是极其脆弱的系统单点。

**限流(Rate Limiting)与节流(Throttling)**是保障系统高可用性的基石架构。限流主要控制“客户端在特定时间窗口内允许发起的最大请求总数”,而节流则主动调控“系统的整体处理吞吐与速率”,以保护下游数据库与核心服务,并维护多租户之间的计算资源公平性。

本文将深度对比主流限流算法的数学模型,分析分布式架构下的并发瓶颈,并基于 Redis 与 Lua 脚本实现一套生产级的高性能原子令牌桶限流引擎。


主流限流算法核心对比

每种限流算法在计算复杂度、内存占用以及突发流量容忍度之间均有各自的工程权衡。

+---------------------------+-------------------+--------------------+------------------------+
| 算法类型                  | 内存空间复杂度    | 突发流量承载能力   | 窗口边界计算精度       |
+---------------------------+-------------------+--------------------+------------------------+
| 固定窗口计数器 (Fixed)    | 每客户端 O(1)     | 极易产生 2倍突发   | 窗口临界点存在严重漏洞 |
| 滑动窗口日志 (Log)        | 每请求 O(N)       | 严格平滑精准       | 绝对完美的时间精度     |
| 滑动窗口计数器 (Counter)  | 每客户端 O(1)     | 平滑加权平均       | 近似估算(约 99% 精度)|
| 令牌桶 (Token Bucket)     | 每客户端 O(1)     | 允许容量 C 内突发  | 恒定速度精确补充       |
| 漏桶 (Leaky Bucket)       | 队列深度 O(N)     | 强制平滑恒定流出   | 队列满载时直接丢弃     |
+---------------------------+-------------------+--------------------+------------------------+

1. 固定窗口计数器 (Fixed Window Counter)

将时间划分为固定的离散区间(例如 60 秒),每个请求累加计数器。当计数超过配额 $L$ 时拒绝后续请求。

  • 边界临界漏洞: 若客户端在第 59 秒发送 100 次请求,紧接着在第 61 秒又发送 100 次请求,系统在短短 2 秒内处理了 200 次请求,瞬时负载超出预设容量一倍,极易导致下游服务雪崩。

2. 令牌桶算法 (Token Bucket)

作为当前 AWS API Gateway、Cloudflare 等主流网关的事实标准,令牌桶拥有固定容量 $C$,并以恒定速率 $R$(每秒填充令牌数)向桶内注入令牌。 每个有效请求必须获取并扣减 1 个令牌方可执行;若桶内令牌耗尽,服务端直接返回 HTTP 429 Too Many Requests。该机制既保障了长期稳态速率不超过 $R$,又优雅支持了容量 $C$ 之内的瞬时突发请求。


生产级实战:Redis + Lua 脚本原子令牌桶

在多节点的分布式无状态服务中,如果在各节点本地内存中独立计数会导致限流配额被严重稀释。将状态下沉到 Redis 是标准做法,但朴素的 Read-Modify-Write 存在严重的并发竞态条件(Race Condition)。

通过将令牌计算与扣减逻辑封装在原子的 Redis Lua 脚本中,可确保绝对的数据一致性:

-- KEYS[1]: 限流桶键名 (如 "ratelimit:user_1024")
-- ARGV[1]: 桶最大容量 (burst limit)
-- ARGV[2]: 每秒令牌填充速率
-- ARGV[3]: 当前 Unix 时间戳 (包含毫秒的小数)
-- ARGV[4]: 消耗令牌数 (通常为 1)

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local data = redis.call("HMGET", key, "tokens", "last_updated")
local tokens = tonumber(data[1])
local last_updated = tonumber(data[2])

if tokens == nil then
    tokens = capacity
    last_updated = now
else
    local elapsed = math.max(0, now - last_updated)
    tokens = math.min(capacity, tokens + (elapsed * refill_rate))
    last_updated = now
end

if tokens >= requested then
    tokens = tokens - requested
    redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated)
    local ttl = math.ceil(capacity / refill_rate) * 2
    redis.call("EXPIRE", key, ttl)
    return {1, math.floor(tokens)} -- 放行: [1, 剩余令牌数]
else
    redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated)
    return {0, math.floor(tokens)} -- 拦截: [0, 剩余令牌数]
end

TypeScript 中间件集成示例

import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });

export interface RateLimitResult {
  allowed: boolean;
  remainingTokens: number;
  retryAfterSeconds?: number;
}

export async function checkRateLimit(
  clientId: string,
  capacity: number = 60,
  refillRate: number = 1.0 // 1 令牌/秒 = 持续 60 次/分钟
): Promise<RateLimitResult> {
  const key = `ratelimit:${clientId}`;
  const now = Date.now() / 1000;

  const [allowed, remaining] = (await redis.evalSha(LUA_SCRIPT_SHA, {
    keys: [key],
    arguments: [
      capacity.toString(),
      refillRate.toString(),
      now.toString(),
      '1'
    ],
  })) as [number, number];

  if (allowed === 1) {
    return { allowed: true, remainingTokens: remaining };
  }

  const retryAfter = Math.ceil((1 - remaining) / refillRate);
  return {
    allowed: false,
    remainingTokens: 0,
    retryAfterSeconds: Math.max(1, retryAfter),
  };
}

标准化 HTTP 响应头

向客户端透明传递限流元数据,能够指导前端与第三方 API 消费者合理执行退避重试(Exponential Backoff):

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 12
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 12

{
  "error": "rate_limit_exceeded",
  "message": "已达到请求速率上限,请稍后重试。",
  "retry_after_seconds": 12
}

More Engineering Articles