Skip to content

多层限流防护

提出问题

高并发场景下,系统不可能无限扩容。当流量洪峰超出服务容量,不做限流就会导致连锁崩溃——一个接口承受不住,线程池打满,Tomcat 请求队列溢出,最终整个应用不可用。

限流不是"挡用户",是"保系统"。但单层限流往往不够:接入层限流只认 IP,同 NAT 下 50 个同事共享一个公网 IP,限到 100r/s 时一个人刷页面其他人全被误伤;网关层限流粒度粗,区分不了「下单」和「查历史订单」的优先级差异;应用层限流很准确,但等流量打到应用再拦截,已经消耗了 CPU 和内存——一台 4C8G 实例在 2000r/s 下 CPU 已经飙到 70%,把这 2000 个请求全放进来再拒绝一半,浪费 1000 次请求的计算资源。

所以生产环境需要多层限流防护,每层拦截自己能处理的流量,后端只处理经过道道过滤后的安全流量。面试官问这个问题,本质是考察你是否理解"防御纵深"——不是靠一个魔法参数解决所有问题,而是不同层各司其职。

分析问题

接入层限流(Nginx)

最外层防线,在流量进入内网之前就拦截。Nginx 提供两个核心模块:

  • limit_req_zone:基于漏桶算法的请求速率限制,控制每秒请求数
  • limit_conn_zone:限制并发连接数,防止慢客户端耗尽连接池
nginx
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 分组、按来源应用、按参数维度限流。

java
@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 提供更丰富的限流器,支持 RateLimiterBulkhead(信号量/线程池隔离)、CircuitBreaker 组合使用。

java
@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 脚本原子地申请令牌。

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
// 在 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 调用优先单机,命中率低才上分布式
全局热点(秒杀下单)各实例争抢,分布不均统一控制,公平必须分布式
非核心(查询历史)单机足够杀鸡用牛刀单机即可

限流后的处理策略

限流不只是"拒绝请求",更关键的是被限后怎么处理

  1. 直接拒绝:返回 429 Too Many Requests + Retry-After 头部,客户端收到后主动退避。适合短时突发流量。
  2. 排队等待:请求进入队列,异步处理。适合削峰填谷(如秒杀),但必须设置超时时间,防止队列积压 OOM。
  3. 降级返回:返回缓存数据或简化版响应。适合读多写少的场景,比如首页推荐降级为热门缓存。
  4. 熔断与快速失败:当错误率超过阈值(如 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 源码分析

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。