限流:保护系统稳定的最后一道防线
不管你的系统设计多么完美,总有人会用你意想不到的方式调用API——爬虫、压测、BUG循环请求或者恶意攻击。限流是保护下游服务、防止系统雪崩的必备手段。
一、四种限流算法对比
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口计数 | 每N秒重置计数器 | 实现简单 | 边界突刺问题 | 粗粒度限制 |
| 滑动窗口计数 | 按时间戳滑动统计 | 平滑,无突刺 | 内存占用较高 | 精确限流 |
| 漏桶 | 固定速率流出 | 绝对平滑输出 | 不允许突发流量 | 匀速处理 |
| 令牌桶 | 匀速生产令牌,可积累 | 允许合理突发 | 实现稍复杂 | API限流首选 |
二、令牌桶算法:最实用的限流方案
令牌桶模型:桶以固定速率添加令牌(如每秒100个),每个请求消耗1个令牌;桶满时新令牌丢弃;桶空时请求被拒绝。这允许短时间内的突发流量(利用积累的令牌),同时保证长期平均速率。
import time
import threading
class TokenBucket:
def __init__(self, rate, burst):
"""rate=每秒令牌数, burst=桶容量(最大突发)"""
self.rate = rate
self.burst = burst
self.tokens = burst # 初始满桶
self.last_refill = time.time()
self.lock = threading.Lock()
def acquire(self):
with self.lock:
now = time.time()
# 计算应补充的令牌数
elapsed = now - self.last_refill
self.tokens = min(self.burst,
self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True # 通过
return False # 限流
# 使用:每秒100请求,最大突发200
limiter = TokenBucket(rate=100, burst=200)
三、分布式限流:Redis实现
单机令牌桶无法跨节点共享状态,多实例部署需要基于Redis的分布式限流:
-- Lua脚本(原子操作,Redis执行)
local key = KEYS[1] -- 限流key
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 每秒令牌数
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local last_tokens = tonumber(redis.call('HGET', key, 'tokens') or capacity)
local last_time = tonumber(redis.call('HGET', key, 'ts') or now)
-- 补充令牌
local elapsed = (now - last_time) / 1000.0
local new_tokens = math.min(capacity, last_tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HSET', key, 'tokens', new_tokens - 1, 'ts', now)
redis.call('EXPIRE', key, 60)
return 1 -- 通过
else
redis.call('HSET', key, 'tokens', new_tokens, 'ts', now)
return 0 -- 限流
end
四、限流实践建议
- 多维度限流:同时按IP、用户ID、接口维度限流,防止单点突破
- 限流响应设计:返回429状态码,响应头加入Retry-After、X-RateLimit-Remaining告知客户端
- 白名单机制:内部服务、监控探针需要跳过限流
- 限流指标监控:记录被限流的请求量,突增时告警,可能是攻击或BUG
- 降级优于拒绝:被限流的请求返回缓存数据比直接503用户体验更好
总结:令牌桶是API限流的首选算法,兼顾平滑限流和合理突发。生产环境用Redis Lua脚本实现分布式令牌桶,配合Nginx层的初级限流(limit_req)做双重防护,基本能应对绝大多数流量攻击和误用场景。
