缓存能提升读性能但当流量真正超过系统承载极限时缓存也救不了你。双十一零点、明星官宣、热搜爆发——瞬间涌入的流量可能是平时的几十甚至上百倍。这时候你需要的不是更快地处理请求而是有策略地拒绝请求。限流就是高并发架构的第一道防线。一、为什么需要限流先看一个真实场景平时 QPS2000系统承载上限10000 QPS某个热点事件爆发瞬间 QPS 飙到 50000不限流的结果→ 线程池打满 → 数据库连接耗尽 → 响应超时 → 用户疯狂重试→ QPS 进一步飙升 → 服务雪崩 → 全站不可用限流的本质在系统扛不住之前主动丢弃超出能力的请求保证已接受的请求能正常处理。宁可少服务一部分用户也不能所有用户都用不了。限流一般在三个位置做限流方案对比位置实现方式特点网关层Nginx / Spring Cloud Gateway / Kong全局入口挡住大部分恶意流量和突发流量服务层Sentinel / Guava RateLimiter / 自研单服务维度保护核心接口客户端前端按钮防抖、验证码、排队页从源头减少请求量二、四大限流算法原理2.1 固定窗口计数器最简单直观的算法把时间切成固定窗口比如每秒每个窗口内维护一个计数器超过阈值就拒绝。时间线|-------- 1s -------- | -------- 1s -------- || 请求计数: 98/100 | 请求计数: 0/100 || ✅✅...✅❌❌ | ✅✅✅... |阈值100 QPS第 98 个请求 → 放行第 101 个请求 → 拒绝新窗口开始 → 计数归零代码实现public class FixedWindowRateLimiter { private final int maxRequests; // 窗口内最大请求数 private final long windowSizeMs; // 窗口大小毫秒 private long windowStart; // 当前窗口起始时间 private int counter; // 当前计数 public FixedWindowRateLimiter(int maxRequests, long windowSizeMs) { this.maxRequests maxRequests; this.windowSizeMs windowSizeMs; this.windowStart System.currentTimeMillis(); this.counter 0; } public synchronized boolean tryAcquire() { long now System.currentTimeMillis(); // 进入新窗口重置计数 if (now - windowStart windowSizeMs) { windowStart now; counter 0; } if (counter maxRequests) { counter; return true; } return false; // 限流 } }致命缺陷——临界突刺问题阈值100 QPS窗口 1 窗口 2|...........[50个请求]|[50个请求]...........|↑ 0.9s ↑ 0.1s在 0.9s ~ 1.1s 这 200ms 内实际通过了 100 个请求相当于瞬时 QPS 500远超限制窗口切换的瞬间两个窗口的请求叠加可能产生两倍于阈值的突发流量。2.2 滑动窗口计数器滑动窗口是固定窗口的升级版把一个大窗口拆成多个小窗口滑动统计。阈值100/秒拆成 10 个 100ms 的小窗口时间 →|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1.0|1.1|1.2|[10][8 ][12][9 ][11][10][13][8 ][9 ][10]└─────────── 统计这 10 个小窗口的总和 ──────────┘总计 100 → 到达阈值滑动后 →[8 ][12][9 ][11][10][13][8 ][9 ][10][5 ]└─────────── 统计这 10 个小窗口的总和 ──────────┘总计 95 → 还可以放行 5 个小窗口越多精度越高。当小窗口数 窗口时间时比如 1 秒拆 1000 个 1ms 窗口就接近理想的滑动窗口了。public class SlidingWindowRateLimiter { private final int maxRequests; private final int subWindowCount; // 小窗口数量 private final long subWindowSizeMs; // 每个小窗口的时间跨度 private final int[] subWindowCounts; // 每个小窗口的计数 private long currentSubWindowStart; // 当前小窗口起始时间 private int currentIndex; public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs, int subWindowCount) { this.maxRequests maxRequests; this.subWindowCount subWindowCount; this.subWindowSizeMs windowSizeMs / subWindowCount; this.subWindowCounts new int[subWindowCount]; this.currentSubWindowStart System.currentTimeMillis(); this.currentIndex 0; } public synchronized boolean tryAcquire() { long now System.currentTimeMillis(); // 滑动窗口清零过期的小窗口 long elapsed now - currentSubWindowStart; int slideCount (int) (elapsed / subWindowSizeMs); if (slideCount 0) { for (int i 0; i Math.min(slideCount, subWindowCount); i) { currentIndex (currentIndex 1) % subWindowCount; subWindowCounts[currentIndex] 0; } currentSubWindowStart slideCount * subWindowSizeMs; } // 统计所有小窗口的总请求数 int total 0; for (int count : subWindowCounts) { total count; } if (total maxRequests) { subWindowCounts[currentIndex]; return true; } return false; } }Sentinel 和 TCP 的拥塞控制都用的是滑动窗口思想。2.3 漏桶算法Leaky Bucket漏桶的思路不管请求怎么突发处理速率恒定。请求涌入不均匀↓ ↓ ↓↓↓ ↓ ↓↓┌────────────────────┐│ ████████████████ │ ← 桶缓冲区│ ██████████████ │ 容量有限满了就溢出拒绝│ ████████████ │└────────┬───────────┘│═════╧═════ ← 恒定速率流出↓ ↓ ↓匀速处理请求核心特点不管上游多猛下游永远是匀速的。public class LeakyBucketRateLimiter { private final int capacity; // 桶容量 private final double leakRatePerMs; // 每毫秒流出速率 private double water; // 当前水量 private long lastLeakTime; // 上次漏水时间 public LeakyBucketRateLimiter(int capacity, int leakRatePerSecond) { this.capacity capacity; this.leakRatePerMs leakRatePerSecond / 1000.0; this.water 0; this.lastLeakTime System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long now System.currentTimeMillis(); // 先漏水 double leaked (now - lastLeakTime) * leakRatePerMs; water Math.max(0, water - leaked); lastLeakTime now; // 判断能否加水 if (water capacity) { water; return true; } return false; // 桶满了拒绝 } }漏桶的缺点太死板。即使系统有能力处理突发流量漏桶也只会匀速放行。比如秒杀开始瞬间用户体验会很差——明明系统还扛得住却被限流了。2.4 令牌桶算法Token Bucket令牌桶是漏桶的灵活版允许一定程度的突发流量。┌────────────────────────────┐│ 以恒定速率放入令牌 ││ ↓ ↓ ↓ ↓ ↓ ↓ ││ ┌──────────────────────┐ ││ │ │ │ ← 令牌桶有上限│ │ │ ││ └──────────┬───────────┘ │└─────────────┼──────────────┘│请求来了 → 取一个令牌 → 有令牌就放行没有就拒绝关键桶里可以攒令牌空闲时攒了 50 个令牌 → 突发来 50 个请求 → 全部放行和漏桶的核心区别- 漏桶恒定速率**处理**请求多余的排队或拒绝- 令牌桶恒定速率**生成**令牌有令牌就放行——所以允许突发public class TokenBucketRateLimiter { private final int maxTokens; // 桶容量 private final double refillPerMs; // 每毫秒生成令牌数 private double tokens; // 当前令牌数 private long lastRefillTime; public TokenBucketRateLimiter(int maxTokens, int refillPerSecond) { this.maxTokens maxTokens; this.refillPerMs refillPerSecond / 1000.0; this.tokens maxTokens; // 初始满桶 this.lastRefillTime System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long now System.currentTimeMillis(); // 补充令牌 double newTokens (now - lastRefillTime) * refillPerMs; tokens Math.min(maxTokens, tokens newTokens); lastRefillTime now; // 消耗令牌 if (tokens 1) { tokens--; return true; } return false; } }2.5 四种算法对比算法突发流量实现复杂度适用场景代表实现固定窗口有临界突刺最简单粗粒度限流Redis INCR EXPIRE滑动窗口较平滑中等精确限流Sentinel漏桶完全平滑中等恒定速率场景Nginx limit_req令牌桶允许突发中等大多数场景推荐Guava RateLimiter选型建议大部分场景用令牌桶。需要严格匀速比如调第三方 API 有硬性速率限制时用漏桶。三、Guava RateLimiter 源码解析Google Guava 的 RateLimiter 是 Java 生态最经典的单机限流实现基于令牌桶算法。3.1 两种模式// 平滑突发限流SmoothBursty // 允许突发桶里有令牌就直接放 RateLimiter limiter RateLimiter.create(100); // 100 QPS // 平滑预热限流SmoothWarmingUp // 冷启动时速率慢逐渐加速到目标速率 // 适合依赖懒加载的场景数据库连接池、缓存预热 RateLimiter limiter RateLimiter.create(100, 3, TimeUnit.SECONDS); // 预热期 3 秒从慢到快逐渐达到 100 QPS3.2 核心源码分析RateLimiter.acquire() 的核心逻辑// 简化后的核心逻辑 public double acquire(int permits) { long microsToWait reserve(permits); // 如果需要等待就 sleep stopwatch.sleepMicrosUninterruptibly(microsToWait); return 1.0 * microsToWait / SECONDS.toMicros(1L); } final long reserve(int permits) { synchronized (mutex()) { return reserveAndGetWaitLength(permits, stopwatch.readMicros()); } } // 核心计算需要等待多久 final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { // 1. 补充令牌根据时间差计算应该补多少 resync(nowMicros); long returnValue nextFreeTicketMicros; // 2. 消耗存量令牌 double storedPermitsToSpend min(requiredPermits, this.storedPermits); // 3. 还差多少令牌需要预支透支未来的时间 double freshPermits requiredPermits - storedPermitsToSpend; // 4. 计算预支这些令牌需要等待的时间 long waitMicros storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) (long) (freshPermits * stableIntervalMicros); this.nextFreeTicketMicros LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); this.storedPermits - storedPermitsToSpend; return returnValue; }关键设计——预支机制Guava RateLimiter 允许透支未来的令牌。比如桶里只有 1 个令牌但请求需要 5 个它不会拒绝而是放行这个请求并让下一个请求多等一会儿当前令牌1请求需要 5 个→ 立即放行不让当前请求等→ 透支 4 个令牌的时间 4 * 10ms 40ms→ 下一个请求需要等 40ms 才能获取令牌这种设计让调用方体验更好当前请求不用等代价由下一个请求承担。3.3 RateLimiter 使用实战RestController public class OrderController { // 限制下单接口 200 QPS private final RateLimiter rateLimiter RateLimiter.create(200); PostMapping(/api/order) public Result createOrder(RequestBody OrderDTO order) { // 方式一阻塞等待不推荐会导致线程被占用 // rateLimiter.acquire(); // 方式二尝试获取超时就快速失败推荐 if (!rateLimiter.tryAcquire(50, TimeUnit.MILLISECONDS)) { return Result.fail(系统繁忙请稍后重试); } return orderService.createOrder(order); } }Guava RateLimiter 的局限-只能单机限流不支持分布式- 没有规则动态调整能力- 没有监控面板所以在微服务场景下一般用 Sentinel。四、Sentinel 限流实战Alibaba Sentinel 是目前 Java 微服务生态最主流的流量治理组件不仅支持限流还支持熔断降级、系统保护、热点参数限流。4.1 快速接入!-- pom.xml -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-sentinel/artifactId /dependency !-- 控制台可选提供可视化管理界面 -- dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-transport-simple-http/artifactId /dependency# application.yml spring: cloud: sentinel: transport: dashboard: localhost:8080 # Sentinel 控制台地址 port: 8719 # 与控制台通信的端口4.2 注解方式限流RestController public class ProductController { GetMapping(/api/product/{id}) SentinelResource( value getProduct, blockHandler getProductBlock, // 限流后的处理 fallback getProductFallback // 异常后的降级 ) public Result getProduct(PathVariable Long id) { return Result.ok(productService.getById(id)); } // 限流处理方法参数必须和原方法一致多一个 BlockException public Result getProductBlock(Long id, BlockException ex) { return Result.fail(访问过于频繁请稍后重试); } // 降级处理方法 public Result getProductFallback(Long id, Throwable ex) { return Result.fail(服务暂时不可用请稍后重试); } }4.3 限流规则配置Configuration public class SentinelRuleConfig { PostConstruct public void initRules() { ListFlowRule rules new ArrayList(); // 规则一QPS 限流 FlowRule qpsRule new FlowRule(); qpsRule.setResource(getProduct); qpsRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS 模式 qpsRule.setCount(1000); // 阈值 1000 qpsRule.setControlBehavior( RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 预热模式 qpsRule.setWarmUpPeriodSec(10); // 预热时间 10s rules.add(qpsRule); // 规则二线程数限流 FlowRule threadRule new FlowRule(); threadRule.setResource(createOrder); threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 线程数模式 threadRule.setCount(50); // 最多 50 个线程同时处理 rules.add(threadRule); // 规则三关联限流写接口触发限流时限制读接口 FlowRule relateRule new FlowRule(); relateRule.setResource(getProduct); relateRule.setStrategy(RuleConstant.STRATEGY_RELATE); relateRule.setRefResource(updateProduct); // 关联资源 relateRule.setCount(500); rules.add(relateRule); FlowRuleManager.loadRules(rules); } }4.4 Sentinel 三种流控效果流控效果行为适用场景快速失败直接拒绝抛 FlowException大多数场景Warm Up预热冷启动慢慢加速到阈值预热期内阈值 count / coldFactor秒杀开始、服务刚启动匀速排队请求匀速通过漏桶算法超时的才拒绝消息消费、批处理场景// 匀速排队示例500 QPS每个请求间隔 2ms 通过最多排队 5 秒 FlowRule rule new FlowRule(); rule.setResource(processMessage); rule.setCount(500); rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); rule.setMaxQueueingTimeMs(5000); // 最大排队等待 5 秒4.5 热点参数限流场景某个商品被疯狂访问只限制这个商品的 QPS不影响其他商品。GetMapping(/api/product/{id}) SentinelResource(value getProduct, blockHandler handleBlock) public Result getProduct(PathVariable Long id) { return Result.ok(productService.getById(id)); }GetMapping(/api/product/{id}) SentinelResource(value getProduct, blockHandler handleBlock) public Result getProduct(PathVariable Long id) { return Result.ok(productService.getById(id)); }// 热点参数限流规则 ParamFlowRule rule new ParamFlowRule(getProduct) .setParamIdx(0) // 第 0 个参数id .setCount(100); // 每个参数值 QPS 上限 100 // 特定参数值单独设置阈值 ParamFlowItem item new ParamFlowItem() .setObject(1001) // 商品 ID 1001 .setClassType(long.class.getName()) .setCount(10); // 这个商品只允许 10 QPS比如已售罄 rule.setParamFlowItemList(Collections.singletonList(item)); ParamFlowRuleManager.loadRules(Collections.singletonList(rule));五、网关层限流网关是限流的最佳位置——在请求进入业务服务之前就挡住。5.1 Nginx 限流# nginx.conf # 定义限流区域基于客户端 IP10MB 共享内存100 QPS limit_req_zone $binary_remote_addr zoneapi_limit:10m rate100r/s; # 定义连接数限制 limit_conn_zone $binary_remote_addr zoneconn_limit:10m; server { # 接口限流允许突发 50 个排队 location /api/ { limit_req zoneapi_limit burst50 nodelay; # burst50 最多排队 50 个请求 # nodelay 排队的请求不延迟直接处理不加则匀速处理 limit_conn conn_limit 20; # 单 IP 最多 20 个并发连接 proxy_pass http://backend; } # 限流后返回 JSON 而不是默认的 503 页面 error_page 503 rate_limited; location rate_limited { default_type application/json; return 429 {code:429,message:请求过于频繁请稍后重试}; } }5.2 Spring Cloud Gateway 限流# application.yml spring: cloud: gateway: routes: - id: product-service uri: lb://product-service predicates: - Path/api/product/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 500 # 每秒补充 500 个令牌 redis-rate-limiter.burstCapacity: 1000 # 桶容量 1000 redis-rate-limiter.requestedTokens: 1 # 每个请求消耗 1 个令牌 key-resolver: #{ipKeyResolver}Configuration public class RateLimiterConfig { // 按 IP 限流 Bean public KeyResolver ipKeyResolver() { return exchange - Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); } // 按用户限流 Bean public KeyResolver userKeyResolver() { return exchange - Mono.just( exchange.getRequest().getHeaders().getFirst(X-User-Id) ); } // 按接口限流 Bean public KeyResolver apiKeyResolver() { return exchange - Mono.just( exchange.getRequest().getPath().value() ); } }Gateway 的限流底层用 Redis Lua 脚本实现分布式令牌桶多个网关实例共享同一个限流计数。5.3 基于 Redis 的分布式限流当需要自定义分布式限流逻辑时直接用 Redis Lua-- rate_limiter.lua -- KEYS[1] 限流 key -- ARGV[1] 最大令牌数 -- ARGV[2] 每秒生成令牌速率 -- ARGV[3] 当前时间戳秒 -- ARGV[4] 请求的令牌数 local key KEYS[1] local max_tokens tonumber(ARGV[1]) local refill_rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local requested tonumber(ARGV[4]) -- 获取上次的令牌数和时间 local data redis.call(hmget, key, tokens, last_time) local tokens tonumber(data[1]) or max_tokens local last_time tonumber(data[2]) or now -- 计算应该补充多少令牌 local elapsed math.max(0, now - last_time) tokens math.min(max_tokens, tokens elapsed * refill_rate) -- 判断令牌是否足够 local allowed 0 if tokens requested then tokens tokens - requested allowed 1 end -- 更新状态 redis.call(hmset, key, tokens, tokens, last_time, now) redis.call(expire, key, math.ceil(max_tokens / refill_rate) * 2) return allowedService public class DistributedRateLimiter { private final StringRedisTemplate redis; private final DefaultRedisScriptLong script; public DistributedRateLimiter(StringRedisTemplate redis) { this.redis redis; this.script new DefaultRedisScript(); this.script.setLocation(new ClassPathResource(scripts/rate_limiter.lua)); this.script.setResultType(Long.class); } public boolean tryAcquire(String key, int maxTokens, int refillRate) { Long result redis.execute(script, Collections.singletonList(rate_limit: key), String.valueOf(maxTokens), String.valueOf(refillRate), String.valueOf(System.currentTimeMillis() / 1000), 1 ); return result ! null result 1; } }六、生产环境踩坑与调优6.1 限流阈值怎么定不要拍脑袋定要压测定。步骤1. 单机压测找到系统能承受的最大 QPSP99 延迟 200ms 的上限比如单机最大 QPS 30002. 留 20-30% buffer单机限流阈值 3000 × 0.7 21003. 分布式限流阈值 单机阈值 × 实例数比如 5 台机器 → 集群限流阈值 105004. 上线后根据监控数据持续调优6.2 限流后怎么返回// ❌ 错误做法直接返回 500 return Result.fail(服务器错误); // ✅ 正确做法返回 429 友好提示 Retry-After 头 ExceptionHandler(BlockException.class) public ResponseEntityResult handleBlock(BlockException ex) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .header(Retry-After, 3) // 建议 3 秒后重试 .body(Result.fail(访问过于频繁请稍后重试)); }6.3 常见踩坑坑原因解决方案限流不生效限流 key 设错了比如内网 IP 都一样检查限流维度改用 userId 或 header 中的真实 IP内部调用被限流服务间 RPC 也走了限流逻辑区分外部请求和内部调用内部调用走白名单集群限流不准各节点时钟不同步用 Redis 做分布式限流或 NTP 同步时钟限流导致用户体验差只有简单的请稍后重试提供排队机制、倒计时、验证码等柔性方案预热期被打满服务刚启动就接到大量流量用 Sentinel Warm Up 或先小比例灰度引流七、面试高频问题Q1限流算法有哪些各自的优缺点 四种主要算法固定窗口、滑动窗口、漏桶、令牌桶。固定窗口最简单但有临界突刺问题滑动窗口把固定窗口拆成多个小窗口滑动统计精度更高漏桶以恒定速率处理请求完全平滑但不允许突发令牌桶以恒定速率生成令牌允许一定程度的突发流量是最常用的方案。Q2Guava RateLimiter 的原理 Guava RateLimiter 基于令牌桶算法有两种模式SmoothBursty 允许突发、SmoothWarmingUp 支持预热。它有一个巧妙的预支机制——当前请求可以透支未来的令牌立即放行代价由下一个请求承担。内部用 nextFreeTicketMicros 记录下一个令牌可用的时间点实现无锁设计。不过它只支持单机限流不适合分布式场景。Q3Sentinel 和 Hystrix 的区别 Hystrix 已停止维护Sentinel 是目前主流。主要区别Sentinel 支持 QPS 限流 线程数限流 热点参数限流Hystrix 只有线程池/信号量隔离Sentinel 有可视化控制台和动态规则推送运维更友好Sentinel 的滑动窗口统计性能更好对应用的侵入更小。Q4分布式限流怎么做 两种方案。一是集中式用 Redis Lua 脚本实现令牌桶或滑动窗口所有节点共享一个计数器精确但有网络开销。二是分散式每个节点按照集群阈值 / 节点数做本地限流简单但不够精确节点流量不均匀时有偏差。实践中通常网关层用 Redis 做集中限流服务层用 Sentinel 做单机限流兜底。Q5限流、熔断、降级的区别 限流是控制**流入速率**在请求还没被处理前就挡住一部分熔断是发现**下游故障**后自动切断调用链防止雪崩扩散降级是在**资源不足**时主动关闭非核心功能保证核心链路可用。三者经常组合使用限流是第一道防线挡住过量请求熔断是第二道防线防止故障扩散降级是最后的兜底保证核心功能存活。总结┌─────────────────────────────────────────────────────┐ │ 算法 │ 令牌桶最通用 │ 漏桶最平滑 │ 滑动窗口最精确 ├─────────────────────────────────────────────────────┤ │ 单机 │ Guava RateLimiter简单 │ Sentinel功能全 ├─────────────────────────────────────────────────────┤ │ 分布式│ Redis Lua │ Gateway RequestRateLimiter ├─────────────────────────────────────────────────────┤ │ 网关 │ Nginx limit_req │ Gateway │ Sentinel 网关插件 ├─────────────────────────────────────────────────────┤ │ 阈值 │ 压测得出留 20-30% buffer上线持续调优 ├─────────────────────────────────────────────────────┤ │ 体验 │ 429 Retry-After │ 排队/验证码等柔性方案 └─────────────────────────────────────────────────────┘限流不是目的保护系统才是目的。好的限流方案应该让大部分用户感知不到限流的存在只在真正扛不住的时候才触发且被限流的用户也有友好的引导。---下一篇我们讲线程池与连接池调优——限流是从外部控制流量池化是从内部优化资源利用两者配合才是完整的并发治理方案。---光看不练假把式。限流算法、Sentinel 配置、令牌桶原理这些话题面试官一定会追着问实现细节。推荐用 **面霸**AI 模拟面试平台实战模拟几轮AI 面试官会根据你的回答深度追问令牌桶和漏桶的本质区别你们限流阈值怎么定的这类问题练到能自然表达出来才算真会。体验地址http://106.12.14.47:8090/ 懒得注册直接用测试账号体验 - 手机号18088889999 - 密码test123#$qaz---觉得有帮助的话点赞收藏不迷路。有问题评论区见。