文章目录
-
- 将时间划分为固定窗口(如 1 分钟) 每个窗口内最多允许 N 次请求 窗口切换时计数重置
- import redis import time class FixedWindowRateLimiter: def __init__(self, redis_client, key_prefix, limit, window=60): self.redis = redis_client self.key_prefix = key_prefix self.limit = limit self.window = window # 秒 def is_allowed(self, key: str) -> bool: full_key = f”{self.key_prefix}:{key}” current = int(time.time()) window_start = (current // self.window) * self.window # 使用 Redis pipeline 保证原子性 pipe = self.redis.pipeline() pipe.zremrangebyscore(full_key, 0, window_start – 1) pipe.zcard(full_key) pipe.zadd(full_key, {str(current): current}) pipe.expire(full_key, self.window + 1) _, count, _, _ = pipe.execute() return count < self.limit
- 临界问题:在窗口切换瞬间可能接受 2×limit 请求 (如 00:59 发 100 次,01:00 又发 100 次)
-
- 记录每个请求的时间戳 每次请求时,清理 当前时间 – 窗口 之前的记录 统计剩余请求数是否超限
- class SlidingWindowRateLimiter: def __init__(self, redis_client, key_prefix, limit, window=60): self.redis = redis_client self.key_prefix = key_prefix self.limit = limit self.window = window def is_allowed(self, key: str) -> bool: full_key = f”{self.key_prefix}:{key}” now = time.time() window_start = now – self.window pipe = self.redis.pipeline() # 移除窗口外的请求 pipe.zremrangebyscore(full_key, 0, window_start) # 获取当前窗口内请求数 pipe.zcard(full_key) # 添加当前请求 pipe.zadd(full_key, {str(now): now}) # 设置过期时间(避免冷 key 占用内存) pipe.expire(full_key, int(self.window) + 1) _, count, _, _ = pipe.execute() return count <= self.limit
- 解决了固定窗口的临界问题 精确控制任意时间窗口内的请求量
- 内存占用高(需存储所有时间戳) 高频请求下 Redis ZSET 操作开销大
-
- 桶容量 = burst(突发流量) 令牌生成速率 = rate(如 100 token/秒) 请求到来时尝试获取令牌,失败则拒绝
- import json class TokenBucketRateLimiter: LUA_SCRIPT = “”” local tokens_key = KEYS[1] local timestamp_key = KEYS[2] local rate = tonumber(ARGV[1]) local capacity = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local requested = tonumber(ARGV[4]) local last_tokens = redis.call(‘GET’, tokens_key) if not last_tokens then last_tokens = capacity end local last_time = redis.call(‘GET’, timestamp_key) if not last_time then last_time = now end local tokens = tonumber(last_tokens) local last_time = tonumber(last_time) — 计算新令牌数 local new_tokens = tokens + (now – last_time) * rate if new_tokens > capacity then new_tokens = capacity end local allowed = new_tokens >= requested if allowed then new_tokens = new_tokens – requested end — 更新状态 redis.call(‘SET’, tokens_key, new_tokens) redis.call(‘SET’, timestamp_key, now) redis.call(‘EXPIRE’, tokens_key, 10) redis.call(‘EXPIRE’, timestamp_key, 10) return {allowed and 1 or 0, new_tokens} “”” def __init__(self, redis_client, key_prefix, rate, capacity): self.redis = redis_client self.key_prefix = key_prefix self.rate = rate # 令牌生成速率(token/秒) self.capacity = capacity # 桶容量 self.script = self.redis.register_script(self.LUA_SCRIPT) def is_allowed(self, key: str, tokens=1) -> bool: tokens_key = f”{self.key_prefix}:tokens:{key}” timestamp_key = f”{self.key_prefix}:time:{key}” now = time.time() result = self.script( keys=[tokens_key, timestamp_key], args=[self.rate, self.capacity, now, tokens] ) return bool(result[0])
- 支持突发流量(burst) 平滑限流,符合真实业务场景 Redis Lua 脚本保证高并发下的原子性
- 注:漏桶算法通常用于流量整形(如消息队列),而非 Web API 限流,此处略。
- 维度 示例 工具 全局限流 整个服务 QPS ≤ 10,000 Nginx + Lua 用户级限流 每个用户 100 次/分钟 Redis + Token Bucket IP 限流 单 IP 50 次/秒 FastAPI Middleware 接口级限流 /pay 接口 10 次/秒 装饰器 业务级限流 用户 A 每天最多发 5 条短信 数据库计数
- graph LR A[Client] –> B[Nginx/LB] B –> C[Service Instance 1] B –> D[Service Instance 2] C & D –> E E –> F[Token Bucket State] 关键:所有实例共享 Redis 状态,实现集群级限流
- # rate_limiter.py from fastapi import Request, HTTPException, status from starlette.middleware.base import BaseHTTPMiddleware from .token_bucket import TokenBucketRateLimiter # 上述实现 redis_client = redis.Redis(host=”localhost”, port=6379, decode_responses=True) limiter = TokenBucketRateLimiter( redis_client, key_prefix=”api”, rate=10, # 10 token/秒 capacity=20 # 允许突发 20 次 ) class RateLimitMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 获取限流 key(可按 IP、用户 ID、路径组合) client_ip = request.client.host path = request.url.path # 例如:按 IP + 路径限流 rate_key = f”{client_ip}:{path}” if not limiter.is_allowed(rate_key): raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=”Too Many Requests”, headers={“Retry-After”: “1”} # 建议重试时间 ) response = await call_next(request) return response
- # main.py from fastapi import FastAPI from rate_limiter import RateLimitMiddleware app = FastAPI() app.add_middleware(RateLimitMiddleware) @app.get(“/hello”) async def hello(): return {“message”: “Hello World”}
- from functools import wraps def rate_limit(rate: float, capacity: int, key_func=None): def decorator(func): limiter = TokenBucketRateLimiter( redis_client, key_prefix=f”func:{func.__name__}”, rate=rate, capacity=capacity ) @wraps(func) async def wrapper(*args, **kwargs): # 从请求中提取 key(需根据框架调整) request = kwargs.get(“request”) or args[0] key = key_func(request) if key_func else request.client.host if not limiter.is_allowed(key): raise HTTPException(429, “Rate limit exceeded”) return await func(*args, **kwargs) return wrapper return decorator # 使用 @app.post(“/send-sms”) @rate_limit(rate=1/60, capacity=1) # 每用户每分钟 1 次 async def send_sms(request: Request, phone: str): # 发送短信逻辑 pass
- Redis 不可用时:切换到本地内存限流(如 cachetools.TTLCache) 配置动态调整:通过 Consul/Etcd 动态修改限流参数 # 降级示例 try: allowed = redis_limiter.is_allowed(key) except redis.ConnectionError: allowed = local_limiter.is_allowed(key) # 本地限流
- 限流拒绝率:rate_limit_rejected / total_requests 桶填充率:监控令牌消耗速度 告警规则:拒绝率 > 5% 持续 5 分钟 # Prometheus 指标 from prometheus_client import Counter RATE_LIMIT_REJECTED = Counter( “rate_limit_rejected_total”, “Total number of rate limited requests”, [“endpoint”, “client”] ) # 在限流拒绝时增加计数 if not allowed: RATE_LIMIT_REJECTED.labels(endpoint=path, client=ip).inc() raise HTTPException(429, …)
目录
- 一、为什么需要限流
- 二、限流算法详解(附 Python 实现)
- 1.固定窗口(Fixed Window)— 最简单但有缺陷
- 原理
- Python 实现(Redis 版)
- 缺陷
- 2.滑动窗口(Sliding Window)— 更平滑
- 原理
- Python 实现(Redis ZSET)
- 优点
- 缺点
- 3.令牌桶(Token Bucket)— 推荐生产使用
- 原理
- Python 实现(Redis Lua 脚本保证原子性)
- 优势
- 4.漏桶(Leaky Bucket)— 适合匀速处理
- 三、生产级限流方案设计
- 多维度限流策略
- 分布式限流架构
- 四、FastAPI 集成示例(推荐)
- 创建限流中间件
- 在 FastAPI 应用中启用
- 接口级精细限流(装饰器)
- 五、高可用与监控
- 降级策略
- 监控指标
- 六、性能压测对比(10,000 QPS)
- 七、安全增强
- 防止限流绕过
- 限流响应规范
- 八、总结:限流决策树
适用读者:Python 后端工程师、SRE、API 网关开发者
技术栈:FastAPI / Flask / Django + Redis + asyncio
场景:Web API、微服务、爬虫防护、支付系统
更新日期:2026 年 2 月
在高并发场景下,无限制的请求会导致:
- 服务雪崩:CPU/内存打满,响应超时
- 数据库击穿:大量请求穿透缓存压垮 DB
- 资源耗尽:第三方 API 调用超配额(如短信、支付)
限流的核心目标:“在保证服务质量的前提下,优雅拒绝超额请求。”
- 将时间划分为固定窗口(如 1 分钟)
- 每个窗口内最多允许 N 次请求
- 窗口切换时计数重置
import redis
import time
class FixedWindowRateLimiter:
def __init__(self, redis_client, key_prefix, limit, window=60):
self.redis = redis_client
self.key_prefix = key_prefix
self.limit = limit
self.window = window # 秒
def is_allowed(self, key: str) -> bool:
full_key = f"{self.key_prefix}:{key}"
current = int(time.time())
window_start = (current // self.window) * self.window
# 使用 Redis pipeline 保证原子性
pipe = self.redis.pipeline()
pipe.zremrangebyscore(full_key, 0, window_start - 1)
pipe.zcard(full_key)
pipe.zadd(full_key, {str(current): current})
pipe.expire(full_key, self.window + 1)
_, count, _, _ = pipe.execute()
return count < self.limit
临界问题:在窗口切换瞬间可能接受 2×limit 请求
(如 00:59 发 100 次,01:00 又发 100 次)
- 记录每个请求的时间戳
- 每次请求时,清理
当前时间 - 窗口 之前的记录
- 统计剩余请求数是否超限
当前时间 - 窗口 之前的记录
class SlidingWindowRateLimiter:
def __init__(self, redis_client, key_prefix, limit, window=60):
self.redis = redis_client
self.key_prefix = key_prefix
self.limit = limit
self.window = window
def is_allowed(self, key: str) -> bool:
full_key = f"{self.key_prefix}:{key}"
now = time.time()
window_start = now - self.window
pipe = self.redis.pipeline()
# 移除窗口外的请求
pipe.zremrangebyscore(full_key, 0, window_start)
# 获取当前窗口内请求数
pipe.zcard(full_key)
# 添加当前请求
pipe.zadd(full_key, {str(now): now})
# 设置过期时间(避免冷 key 占用内存)
pipe.expire(full_key, int(self.window) + 1)
_, count, _, _ = pipe.execute()
return count <= self.limit
- 解决了固定窗口的临界问题
- 精确控制任意时间窗口内的请求量
- 内存占用高(需存储所有时间戳)
- 高频请求下 Redis ZSET 操作开销大
- 桶容量 =
burst(突发流量)
- 令牌生成速率 =
rate(如 100 token/秒)
- 请求到来时尝试获取令牌,失败则拒绝
burst(突发流量)rate(如 100 token/秒)
import json
class TokenBucketRateLimiter:
LUA_SCRIPT = """
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local last_tokens = redis.call('GET', tokens_key)
if not last_tokens then
last_tokens = capacity
end
local last_time = redis.call('GET', timestamp_key)
if not last_time then
last_time = now
end
local tokens = tonumber(last_tokens)
local last_time = tonumber(last_time)
-- 计算新令牌数
local new_tokens = tokens + (now - last_time) * rate
if new_tokens > capacity then
new_tokens = capacity
end
local allowed = new_tokens >= requested
if allowed then
new_tokens = new_tokens - requested
end
-- 更新状态
redis.call('SET', tokens_key, new_tokens)
redis.call('SET', timestamp_key, now)
redis.call('EXPIRE', tokens_key, 10)
redis.call('EXPIRE', timestamp_key, 10)
return {allowed and 1 or 0, new_tokens}
"""
def __init__(self, redis_client, key_prefix, rate, capacity):
self.redis = redis_client
self.key_prefix = key_prefix
self.rate = rate # 令牌生成速率(token/秒)
self.capacity = capacity # 桶容量
self.script = self.redis.register_script(self.LUA_SCRIPT)
def is_allowed(self, key: str, tokens=1) -> bool:
tokens_key = f"{self.key_prefix}:tokens:{key}"
timestamp_key = f"{self.key_prefix}:time:{key}"
now = time.time()
result = self.script(
keys=[tokens_key, timestamp_key],
args=[self.rate, self.capacity, now, tokens]
)
return bool(result[0])
- 支持突发流量(burst)
- 平滑限流,符合真实业务场景
- Redis Lua 脚本保证高并发下的原子性
注:漏桶算法通常用于流量整形(如消息队列),而非 Web API 限流,此处略。
| 维度 | 示例 | 工具 |
|---|---|---|
| 全局限流 | 整个服务 QPS ≤ 10,000 | Nginx + Lua |
| 用户级限流 | 每个用户 100 次/分钟 | Redis + Token Bucket |
| IP 限流 | 单 IP 50 次/秒 | FastAPI Middleware |
| 接口级限流 | /pay 接口 10 次/秒 | 装饰器 |
| 业务级限流 | 用户 A 每天最多发 5 条短信 | 数据库计数 |
graph LR
A[Client] –> B[Nginx/LB]
B –> C[Service Instance 1]
B –> D[Service Instance 2]
C & D –> E
E –> F[Token Bucket State]
graph LR
A[Client] –> B[Nginx/LB]
B –> C[Service Instance 1]
B –> D[Service Instance 2]
C & D –> E
E –> F[Token Bucket State]
关键:所有实例共享 Redis 状态,实现集群级限流
# rate_limiter.py
from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from .token_bucket import TokenBucketRateLimiter # 上述实现
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
limiter = TokenBucketRateLimiter(
redis_client,
key_prefix="api",
rate=10, # 10 token/秒
capacity=20 # 允许突发 20 次
)
class RateLimitMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 获取限流 key(可按 IP、用户 ID、路径组合)
client_ip = request.client.host
path = request.url.path
# 例如:按 IP + 路径限流
rate_key = f"{client_ip}:{path}"
if not limiter.is_allowed(rate_key):
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too Many Requests",
headers={"Retry-After": "1"} # 建议重试时间
)
response = await call_next(request)
return response
# main.py
from fastapi import FastAPI
from rate_limiter import RateLimitMiddleware
app = FastAPI()
app.add_middleware(RateLimitMiddleware)
@app.get("/hello")
async def hello():
return {"message": "Hello World"}
from functools import wraps
def rate_limit(rate: float, capacity: int, key_func=None):
def decorator(func):
limiter = TokenBucketRateLimiter(
redis_client,
key_prefix=f"func:{func.__name__}",
rate=rate,
capacity=capacity
)
@wraps(func)
async def wrapper(*args, **kwargs):
# 从请求中提取 key(需根据框架调整)
request = kwargs.get("request") or args[0]
key = key_func(request) if key_func else request.client.host
if not limiter.is_allowed(key):
raise HTTPException(429, "Rate limit exceeded")
return await func(*args, **kwargs)
return wrapper
return decorator
# 使用
@app.post("/send-sms")
@rate_limit(rate=1/60, capacity=1) # 每用户每分钟 1 次
async def send_sms(request: Request, phone: str):
# 发送短信逻辑
pass
- Redis 不可用时:切换到本地内存限流(如
cachetools.TTLCache)
- 配置动态调整:通过 Consul/Etcd 动态修改限流参数
# 降级示例
try:
allowed = redis_limiter.is_allowed(key)
except redis.ConnectionError:
allowed = local_limiter.is_allowed(key) # 本地限流
cachetools.TTLCache)
- 限流拒绝率:
rate_limit_rejected / total_requests
- 桶填充率:监控令牌消耗速度
- 告警规则:拒绝率 > 5% 持续 5 分钟
# Prometheus 指标
from prometheus_client import Counter
RATE_LIMIT_REJECTED = Counter(
"rate_limit_rejected_total",
"Total number of rate limited requests",
["endpoint", "client"]
)
# 在限流拒绝时增加计数
if not allowed:
RATE_LIMIT_REJECTED.labels(endpoint=path, client=ip).inc()
raise HTTPException(429, ...)
rate_limit_rejected / total_requests
| 方案 | CPU 使用率 | P99 延迟 | 内存占用 | 准确性 |
|---|---|---|---|---|
| 固定窗口 | 35% | 8ms | 低 | ❌(临界问题) |
| 滑动窗口 | 65% | 25ms | 高 | ✅ |
| 令牌桶(Lua) | 40% | 12ms | 中 | ✅ |
| 无限流 | 95% | 200ms+ | 极高 | – |
结论:令牌桶 + Redis Lua 是生产环境最佳选择
- Key 设计:使用
user_id + ip + user_agent 组合,防代理池绕过
- 黑名单机制:对恶意 IP 永久封禁
user_id + ip + user_agent 组合,防代理池绕过
HTTP/1.1 429 Too Many Requests
Retry-After: 5
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708761600
graph TD
A[需要限流?] –>|是| B{数据一致性要求高?}
B –>|是| C[用 Redis Token Bucket]
B –>|否| D[用本地内存限流]
C –> E[写 Lua 脚本保证原子性]
E –> F[多维度 Key 设计]
F –> G[集成监控告警]
G –> H[配置动态调整]
graph TD
A[需要限流?] –>|是| B{数据一致性要求高?}
B –>|是| C[用 Redis Token Bucket]
B –>|否| D[用本地内存限流]
C –> E[写 Lua 脚本保证原子性]
E –> F[多维度 Key 设计]
F –> G[集成监控告警]
G –> H[配置动态调整]
终极建议:
- 新项目直接用 Token Bucket + Redis
- 关键接口单独配置限流策略
- 永远返回 429 而非 500
- 监控比限流本身更重要
以上就是从原理到生产落地详解Python高并发服务限流的终极方案的详细内容,更多关于Python高并发服务限流的资料请关注风君子博客其它相关文章!
您可能感兴趣的文章:
- 使用Python手搓一个生产级限流器
- Python实现常见限流算法的示例代码
- Python实现简易的限流器介绍
- Python+redis通过限流保护高并发系统
- python分布式环境下的限流器的示例