多层限流防护
提出问题
高并发场景下,系统不可能无限扩容。当流量洪峰超出服务容量,不做限流就会导致连锁崩溃——一个接口承受不住,线程池打满,Tomcat 请求队列溢出,最终整个应用不可用。
限流不是"挡用户",是"保系统"。但单层限流往往不够:接入层限流只认 IP,同 NAT 下 50 个同事共享一个公网 IP,限到 100r/s 时一个人刷页面其他人全被误伤;网关层限流粒度粗,区分不了「下单」和「查历史订单」的优先级差异;应用层限流很准确,但等流量打到应用再拦截,已经消耗了 CPU 和内存——一台 4C8G 实例在 2000r/s 下 CPU 已经飙到 70%,把这 2000 个请求全放进来再拒绝一半,浪费 1000 次请求的计算资源。
所以生产环境需要多层限流防护,每层拦截自己能处理的流量,后端只处理经过道道过滤后的安全流量。面试官问这个问题,本质是考察你是否理解"防御纵深"——不是靠一个魔法参数解决所有问题,而是不同层各司其职。
分析问题
接入层限流(Nginx)
最外层防线,在流量进入内网之前就拦截。Nginx 提供两个核心模块:
limit_req_zone:基于漏桶算法的请求速率限制,控制每秒请求数limit_conn_zone:限制并发连接数,防止慢客户端耗尽连接池
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 50;
proxy_pass http://backend;
}
}
}burst=20 允许短暂突发流量堆积,nodelay 让这些突发请求立即被处理而不人为延迟。缺点是只按 IP 限流,同 NAT 下的多个用户会被当作一个来源,所以 Nginx 层适合做粗粒度兜底,不适合精细化限流。
踩坑实录:某次大促,Nginx 按 IP 限了 100r/s,结果办公室 NAT 出口 IP 被限——200 个运营同事同时刷后台,前端疯狂报 503。临时方案是把运营网段加到白名单,长期方案是网关层按用户 ID 重新限流。
网关层限流(Spring Cloud Gateway + Sentinel)
网关层比 Nginx 更了解业务——知道哪个 URL 对应哪个服务、哪个用户属于哪个等级。以 Sentinel 网关限流为例,支持按 API 分组、按来源应用、按参数维度限流。
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("order-service", r -> r.path("/order/**")
.filters(f -> f.requestRateLimiter(config -> {
config.setRateLimiter(redisRateLimiter());
config.setKeyResolver(userKeyResolver());
}))
.uri("lb://order-service"))
.build();
}
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders()
.getFirst("X-User-Id") // 按用户 ID 限流
);
}
}网关层可以访问 Redis 做分布式限流,实现如"每个用户每秒最多 10 次下单"这类精细规则。同时网关还能做熔断降级——当后端服务响应超时比例超过阈值,直接短路返回 fallback,不让后续请求继续打进去。
Sentinel 网关限流核心参数:
| 参数 | 说明 | 常见值 | 影响 |
|---|---|---|---|
| grade | 限流维度 | QPS / 线程数 | QPS 模式更常用 |
| count | 阈值 | 核心接口 500,非核心 100 | 超过即触发限流 |
| controlBehavior | 流量效果 | 直接拒绝 / 排队等待 / 预热 | 排队等待适合削峰填谷 |
| maxQueueingTimeMs | 排队最大等待 | 500ms | 超时直接返回,不阻塞线程 |
应用层限流(Guava / Resilience4j)
流量打到具体服务实例后,最后一道防线在应用代码里。Guava 的 RateLimiter 基于令牌桶,简单但单机版本(不能跨进程同步)。Resilience4j 提供更丰富的限流器,支持 RateLimiter、Bulkhead(信号量/线程池隔离)、CircuitBreaker 组合使用。
@Component
public class OrderService {
private final RateLimiter rateLimiter = RateLimiter.create(200); // 每秒 200 个令牌
@CircuitBreaker(name = "orderService", fallbackMethod = "orderFallback")
public OrderResult createOrder(OrderRequest request) {
if (!rateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
throw new RateLimitException("当前下单人数过多,请稍后重试");
}
// 实际业务逻辑
return doCreateOrder(request);
}
public OrderResult orderFallback(OrderRequest request, Throwable t) {
return OrderResult.fallback("系统繁忙,已为您排队,稍后处理");
}
}应用层限流的优势是精准——可以结合业务上下文做判断,比如"VIP 用户不限流、普通用户限流"。缺点是需要每个业务代码都加上限流逻辑,且单机版本不能感知全局流量。
Resilience4j 限流器实测对比:
| 实现 | 单机 QPS 上限 | 额外延迟 | 适用场景 |
|---|---|---|---|
| Guava RateLimiter | 单机 ~2000 | 纳秒级 | 简单单机限流 |
| Resilience4j RateLimiter | 单机 ~1500 | <0.1ms | 需配合熔断/隔离 |
| Resilience4j Bulkhead(信号量) | 依赖池大小 | 纳秒级 | 隔离慢调用 |
| Resilience4j Bulkhead(线程池) | 依赖线程数 | ~1ms(上下文切换) | 隔离不同下游 |
分布式限流(Redis + Lua)
单机限流在多实例部署下会失效——每个实例 200 QPS,10 个实例就是 2000 QPS。需要分布式限流时,用 Redis + Lua 脚本保证原子性:令牌桶状态存 Redis,多个实例通过 Lua 脚本原子地申请令牌。
-- token_bucket.lua
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌产生速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间戳(秒)
local requested = tonumber(ARGV[4]) -- 本次请求需要的令牌数
local bucket = redis.call('hmget', key, 'tokens', 'lastRefill')
local tokens = bucket[1] and tonumber(bucket[1]) or capacity
local lastRefill = bucket[2] and tonumber(bucket[2]) or now
local elapsed = math.max(0, now - lastRefill)
local newTokens = math.min(capacity, tokens + elapsed * rate)
if newTokens >= requested then
redis.call('hmset', key, 'tokens', newTokens - requested, 'lastRefill', now)
redis.call('expire', key, 10)
return 1 -- 放行
else
return 0 -- 拒绝
end// 在 Java 中调用
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long allowed = redisTemplate.execute(script,
List.of("rate:order:create"),
"100", "500", System.currentTimeMillis() / 1000, "1");分布式限流的代价是每次请求都多一次 Redis 调用,会增加几毫秒延迟。生产上通常只在关键接口(支付、下单、登录)启用,非核心接口走单机限流即可。
分布式限流 vs 单机限流典型场景:
| 场景 | 单机限流效果 | 分布式限流效果 | 推荐 |
|---|---|---|---|
| 10 实例,每个限 200 QPS | 总 QPS 可达 2000 | 精确控制 1000 QPS | 若总容量只有 1000,必须分布式 |
| 慢查询接口 (<10ms) | 0.1ms 额外延迟 | 加 1-3ms Redis 调用 | 优先单机,命中率低才上分布式 |
| 全局热点(秒杀下单) | 各实例争抢,分布不均 | 统一控制,公平 | 必须分布式 |
| 非核心(查询历史) | 单机足够 | 杀鸡用牛刀 | 单机即可 |
限流后的处理策略
限流不只是"拒绝请求",更关键的是被限后怎么处理:
- 直接拒绝:返回 429 Too Many Requests + Retry-After 头部,客户端收到后主动退避。适合短时突发流量。
- 排队等待:请求进入队列,异步处理。适合削峰填谷(如秒杀),但必须设置超时时间,防止队列积压 OOM。
- 降级返回:返回缓存数据或简化版响应。适合读多写少的场景,比如首页推荐降级为热门缓存。
- 熔断与快速失败:当错误率超过阈值(如 50% 请求 5xx),直接断开不再发请求到下游,定时探测恢复。防止雪崩。
生产事故案例:某电商平台大促,下单接口限流策略只配了"直接拒绝"没配降级,结果用户被限流后反复重试(重试次数 3 次,间隔 100ms),导致 Nginx 层看到的请求量是实际用户的 3 倍,整体限流阈值被击穿,最终订单服务 10 分钟内从 200ms 响应劣化到 5s 超时。修复方案:降级返回"排队中"页面 + 客户端重试指数退避(初始 1s,最多 3 次)。
各层交互时序
用户请求
│
▼
┌─────────────────────┐
│ 接入层 (Nginx) │ ≤ 1000 r/s,按 IP 限流
│ rate=1000r/s │ 超限 → 返回 503
│ burst=200 │
└─────────┬───────────┘
│ 通过
▼
┌─────────────────────┐
│ 网关层 (Sentinel) │ ≤ 500 r/s,按用户 ID / API 分组
│ /order/* 500r/s │ 超限 → 降级 "排队中"
│ /search/* 200r/s │
└─────────┬───────────┘
│ 通过
▼
┌─────────────────────┐
│ 应用层 (实例本地) │ 单机 200 r/s,按业务方法
│ Guava/Resilience4j │ 超限 → 熔断降级
│ Bulkhead=20 │
└─────────┬───────────┘
│ 通过
▼
┌──────────┐
│ 业务逻辑 │
└──────────┘每层流量递减一个数量级:Nginx 1000r/s → 网关 500r/s → 应用 200r/s。任何一个实例被单点打死前,上层就兜住了。
关键要点
- 限流不只看 QPS,更要看预期行为——被限了是排队、降级还是直接返回错误?生产事故往往出在"限流了但客户端不断重试"这个环节。
- 每层减一个数量级:Nginx 1000r/s → 网关 500r/s → 应用 200r/s,任何一个实例被单点打死前,上层就兜住了
- 限流需要配合监控告警,如果不看限流命中率,等于在给系统吃降压药但不量血压。建议监控指标:每层限流次数、被限后重试占比、降级调用占比
- 从 8 年 Java 后端转 Agent 工程的视角:限流设计在 Agent 系统中同样关键——LLM API 调用本身就是按 Token 计费的,一个 Agent 编排不当可能在一个循环里调用 50 次 LLM。你的 Agent 框架需要做两层限流:调用层(限制每秒 LLM 调用次数,防止 API Key 被限)和 Agent 编排层(限制单个任务总 Token 消耗,防止预算炸表)。工具调用(function calling)也需要限流,比如一个 Agent 循环调用 Search API,不加限流 30 秒就能烧掉 100 刀。
参考
Sentinel 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html Redis 分布式限流 Lua 脚本:https://redis.io/commands/incr/ 参考:Resilience4j RateLimiter 源码 参考:Nginx ngx_http_limit_req_module 源码分析