从原理到实战的完整指南
目录导读
- 为什么需要接口限流?—— 高频攻击与资源过载的真相
- 主流限流算法详解:计数器、漏斗、令牌桶、滑动窗口
- 限流配置实战:Nginx、Spring Cloud Gateway、Redis+Lua
- 常见问题与问答环节(Q&A)
- 最佳实践:如何避免“误杀”正常用户?
为什么需要接口限流?—— 高频攻击与资源过载的真相
想象一下:你的在线商城突然涌入10万次/秒的请求,服务器CPU飙升到100%,数据库连接池爆满,最终所有用户都看到“502 Bad Gateway”,这不是黑客攻击,仅仅是某个大V在微博提到了你的产品。

接口限流的核心目标:在系统承载能力范围内,拒绝超出阈值的请求,保护后端服务不被冲垮,常见的滥用场景包括:
- 恶意爬虫:批量抓取商品价格、用户数据
- DDoS攻击:利用僵尸网络发送海量请求
- 突发流量:秒杀活动、热点新闻带来的瞬时峰值
- 程序Bug:客户端代码中的死循环请求
关键数据:Google研究表明,99%的接口攻击都可通过合理的限流配置在入口层拦截。
主流限流算法详解:计数器、漏斗、令牌桶、滑动窗口
1 固定窗口计数器(最基础,但不推荐)
每1秒统计请求次数,超过阈值直接拒绝。
问题:如果请求集中在窗口边界(第999ms 100个请求,第1001ms 100个请求),实际2秒内可达200个请求,突破阈值。
2 滑动窗口计数器(推荐)
将窗口细分为10个格点(每100ms),每次请求记录当前时间窗内的累计次数。
优点:平滑度远优于固定窗口,能精准控制每秒请求数。
3 令牌桶算法(最常用)
固定速率生成令牌(如每秒10个),请求必须获取令牌才能通过,允许短时突发(桶内可存最多100个令牌)。
适用场景:希望允许正常用户偶尔的突发行为,同时控制平均速率。
4 漏桶算法(最严格)
请求先进入漏桶(队列),以固定速率从桶底流出。
缺点:无法应对突发,即使系统空闲,用户也只能按恒定速度获取响应,常用于流量整形,如视频直播推流。
选型建议:企业级API推荐令牌桶 + 滑动窗口组合,网关层(如Kong、Spring Cloud Gateway)内置支持。
限流配置实战:Nginx、Spring Cloud Gateway、Redis+Lua
1 使用Nginx做全局限流(适合入口层)
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; # 每IP每秒10次
server {
location /api/ {
limit_req zone=api burst=20 nodelay; # 允许突发20个请求,不延迟
proxy_pass http://backend;
}
}
}
参数说明:
rate=10r/s:平均速率burst=20:最大突发数nodelay:不等待,直接拒绝超出突发的请求
2 使用Spring Cloud Gateway(微服务首选)
spring:
cloud:
gateway:
routes:
- id: user_service
uri: lb://user-service
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 10 # 每秒生成令牌数
redis-rate-limiter.burstCapacity: 20 # 最大突发数
依赖:需集成Spring Data Redis,令牌桶实现存储在Redis中。
3 基于Redis+Lua的自定义方案(最灵活)
-- 滑动窗口Lua脚本(原子操作)
local key = KEYS[1] -- 限流key:api_xxx
local window_ms = ARGV[1] -- 窗口大小:1000ms
local max_requests = ARGV[2] -- 阈值:10
local current_time = redis.call('TIME')[1]
local window_start = current_time - window_ms
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count < tonumber(max_requests) then
redis.call('ZADD', key, current_time, current_time .. '_' .. math.random())
redis.call('EXPIRE', key, window_ms / 1000)
return 1
else
return 0
end
调用示例:
Jedis jedis = new Jedis("localhost");
Long result = (Long) jedis.eval(luaScript, 1, "user_api_rate", "1000", "10");
if (result == 0) { // 被限流
return "Too many requests, please try later.";
}
常见问题与问答环节(Q&A)
Q1:限流阈值设为多少合适?
A:根据压测结果计算,单台服务器处理能力为1000 QPS,集群2台,则单机阈值可设为800,留20%余量应对突发,建议先小(如50)后逐步调大。
Q2:如何区分“正常用户”和“爬虫”?
A:结合限流与防爬策略:
- 用户维度:同一IP每秒10次,同一用户Token每秒5次
- 来源维度:对API密钥、UserAgent做精细限制
- 行为模式:如果用户在1秒内访问5个不同接口,触发熔断
Q3:限流后如何友好提示?
A:返回HTTP 429状态码 + JSON错误信息:
{"code": 429, "message": "请求过于频繁,请1秒后再试", "retryAfter": 1}
同时设置Retry-After响应头。
Q4:分布式限流时,时钟不同步怎么办?
A:使用Redis单实例或Redis Cluster的TIME命令获取统一时间,避免依赖服务器本地时钟,或采用令牌桶算法(不依赖严格时间窗口)。
Q5:限流会影响SEO吗?
A:仅对同IP超频请求限流,搜索引擎爬虫通常遵守robots.txt和Crawl-delay指令,建议在白名单中为Googlebot、Bingbot分配更高阈值(如100次/秒),避免误伤。
最佳实践:如何避免“误杀”正常用户?
-
分级限流:
- L1(入口层):限制IP维度,拒绝明显恶意流量
- L2(业务层):限制用户ID/API Key维度,保护核心资源
- L3(熔断降级):当错误率超过50%,自动熔断5秒
-
动态调整阈值:
根据CPU/内存使用率自动降级,CPU<60%时阈值为100,CPU>80%时自动降至50。 -
增加预热机制:
服务启动后,前10秒逐步开放限流阈值,避免冷启动造成误伤。 -
提供查询接口:
允许用户或管理员查看自己的剩余配额(如GET /api/v1/rate-limit-status)。 -
完善监控告警:
对限流触发次数、被限流的错误码(429)做监控,低于预期可能意味着阈值过高,高于预期则需要排查正常业务是否被误杀。
接口限流不是“一刀切”,而是通过算法与配置的结合,在防护与用户体验之间找到平衡,从Nginx的简单配置,到Redis+Lua的复杂动态控制,关键是要根据业务场景选择合适方案。限流是为了服务更稳定,而不是让用户感觉“被针对”。