# 限流策略与算法 ## 问题 1. 为什么需要限流?常见的限流场景有哪些? 2. 有哪些常见的限流算法?各自的原理和优缺点是什么? 3. 固定窗口算法有什么问题?如何优化? 4. 滑动窗口算法是如何实现的? 5. 令牌桶和漏桶算法的区别是什么? 6. 分布式限流如何实现?(Redis、Sentinel) 7. 在实际项目中,你是如何设计限流策略的? --- ## 标准答案 ### 1. 限流的目的和场景 #### **为什么需要限流?** **保护系统**: - 防止系统过载(CPU、内存、数据库) - 防止雪崩效应(服务级联失败) - 保护核心资源(数据库连接数、API 配额) **保证服务质量**: - 保证大部分用户的正常使用 - 防止恶意攻击(爬虫、DDoS) - 实现公平性(防止单个用户占用资源) --- #### **常见限流场景** | 场景 | 限流对象 | 目的 | |------|---------|------| | **API 接口** | QPS、TPS | 保护后端服务 | | **数据库** | 连接数、QPS | 防止数据库打挂 | | **第三方接口** | 调用次数 | 控制成本(如短信接口) | | **用户行为** | 操作次数 | 防止刷单、恶意抢购 | | **爬虫防护** | IP 请求频率 | 保护数据 | --- ### 2. 限流算法对比 | 算法 | 原理 | 优点 | 缺点 | 适用场景 | |------|------|------|------|----------| | **固定窗口** | 固定时间窗口计数 | 简单 | 临界突变、不精确 | 低要求场景 | | **滑动窗口** | 滑动时间窗口计数 | 精确 | 内存占用大 | 高精度要求 | | **漏桶** | 恒定速率流出 | 平滑流量 | 无法应对突发 | 恒定速率场景 | | **令牌桶** | 恒定速率放入令牌 | 允许突发 | 实现复杂 | 通用场景 | --- ### 3. 固定窗口算法 #### **原理** 将时间划分为固定窗口,每个窗口内计数,超过阈值则拒绝。 **示例**: ``` 窗口大小:1 分钟 阈值:100 次请求 10:00:00 - 10:00:59 → 100 次请求 10:01:00 - 10:01:59 → 重置计数器,重新开始 ``` --- #### **Java 实现** ```java public class FixedWindowRateLimiter { private final int limit; // 阈值 private final long windowSizeMs; // 窗口大小(毫秒) private int count; // 当前计数 private long windowStart; // 窗口开始时间 public FixedWindowRateLimiter(int limit, long windowSizeMs) { this.limit = limit; this.windowSizeMs = windowSizeMs; this.windowStart = System.currentTimeMillis(); } public synchronized boolean allowRequest() { long now = System.currentTimeMillis(); // 超出窗口,重置 if (now - windowStart >= windowSizeMs) { windowStart = now; count = 0; } // 检查是否超限 if (count < limit) { count++; return true; } return false; } } ``` **使用示例**: ```java // 限制:每分钟 100 次请求 FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(100, 60 * 1000); for (int i = 0; i < 150; i++) { boolean allowed = limiter.allowRequest(); System.out.println("请求 " + i + ": " + (allowed ? "通过" : "限流")); } ``` --- #### **问题:临界突变** **场景**: ``` 阈值:100 / 分钟 10:00:59 → 100 次请求(窗口 1 满) 10:01:00 → 100 次请求(窗口 2 满) ↓ 10:01:00 前后 1 秒内,实际处理了 200 次请求! ``` **图解**: ``` 时间 10:00:59 10:01:01 ↓ ↓ 窗口1 █████████████████ (100 请求) 窗口2 █████████████████ (100 请求) ↑ 临界点突变 ``` --- ### 4. 滑动窗口算法 #### **原理** 将时间窗口划分为多个小窗口,滑动计数。 **示例**: ``` 大窗口:1 分钟,阈值 100 小窗口:10 秒 10:00:00 - 10:00:10 → 10 次 10:00:10 - 10:00:20 → 15 次 10:00:20 - 10:00:30 → 20 次 10:00:30 - 10:00:40 → 25 次 10:00:50 - 10:01:00 → 20 次 10:00:35 时,统计最近 1 分钟: 10:00:00 - 10:00:10 → 10 次 10:00:10 - 10:00:20 → 15 次 10:00:20 - 10:00:30 → 20 次 10:00:30 - 10:00:35 → 12.5 次(估算) 总计:57.5 次 < 100,通过 ``` --- #### **Java 实现(环形数组)** ```java public class SlidingWindowRateLimiter { private final int limit; // 阈值 private final int slotCount; // 槽位数量 private final long slotSizeMs; // 槽位大小(毫秒) private final int[] counters; // 计数器数组 private long lastSlotTime; // 上次槽位时间 public SlidingWindowRateLimiter(int limit, long windowSizeMs, int slotCount) { this.limit = limit; this.slotCount = slotCount; this.slotSizeMs = windowSizeMs / slotCount; this.counters = new int[slotCount]; this.lastSlotTime = System.currentTimeMillis(); } public synchronized boolean allowRequest() { long now = System.currentTimeMillis(); // 计算当前槽位索引 int currentSlot = (int) ((now / slotSizeMs) % slotCount); // 清理过期槽位 int slotsToClear = (int) ((now - lastSlotTime) / slotSizeMs); if (slotsToClear >= slotCount) { // 全部过期,清空所有槽位 Arrays.fill(counters, 0); } else { // 部分过期,清理过期槽位 for (int i = 0; i < slotsToClear; i++) { int slotToClear = (currentSlot - i + slotCount) % slotCount; counters[slotToClear] = 0; } } lastSlotTime = now; // 计算当前窗口内总请求数 int totalCount = 0; for (int count : counters) { totalCount += count; } // 检查是否超限 if (totalCount < limit) { counters[currentSlot]++; return true; } return false; } } ``` **使用示例**: ```java // 限制:每分钟 100 次请求,分为 6 个槽位(每 10 秒一个) SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(100, 60 * 1000, 6); ``` --- #### **Redis 实现(Redisson 的 RRateLimiter)** ```java @Autowired private RedissonClient redisson; public boolean allowRequest(String key, int rate, RateIntervalUnit interval) { RRateLimiter rateLimiter = redisson.getRateLimiter(key); // 初始化:每分钟 100 次 rateLimiter.trySetRate(RateType.OVERALL, rate, interval); // 尝试获取许可 return rateLimiter.tryAcquire(1); } ``` --- ### 5. 漏桶算法 #### **原理** 想象一个底部有孔的桶: - 请求像水一样流入桶 - 桶底以恒定速率漏水 - 桶满时拒绝请求 **图解**: ``` 请求流入 ↓ ┌───┐ │ ███│ ← 桶(容量 = C) │ ███│ └───┘ ↓ 恒定速率(R)流出 ``` **特点**: - **恒定速率**:无论请求多快,流出速率固定 - **平滑流量**:削峰填谷 --- #### **Java 实现** ```java public class LeakyBucketRateLimiter { private final int capacity; // 桶容量 private final double leakRate; // 漏水速率(请求/毫秒) private double currentWater; // 当前水量 private long lastLeakTime; // 上次漏水时间 public LeakyBucketRateLimiter(int capacity, double leakRatePerSec) { this.capacity = capacity; this.leakRate = leakRatePerSec / 1000.0; this.lastLeakTime = System.currentTimeMillis(); } public synchronized boolean allowRequest() { long now = System.currentTimeMillis(); // 漏水 double leaked = (now - lastLeakTime) * leakRate; currentWater = Math.max(0, currentWater - leaked); lastLeakTime = now; // 检查是否超限 if (currentWater < capacity) { currentWater += 1; return true; } return false; } } ``` **使用示例**: ```java // 容量:100,漏水速率:10 请求/秒 LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(100, 10); ``` --- #### **优缺点** **优点**: - 平滑流量,恒定速率 - 保护下游系统 **缺点**: - 无法应对突发流量 - 参数调整困难 --- ### 6. 令牌桶算法 #### **原理** 系统以恒定速率向桶中放入令牌: - 请求到达时,从桶中获取令牌 - 有令牌则通过,无令牌则拒绝 - 桶满时,令牌溢出 **图解**: ``` 恒定速率放入令牌 ↓ ┌───┐ │ ○○○│ ← 令牌桶(容量 = C) │ ○○○│ └───┘ ↓ 请求获取令牌 ``` **特点**: - **允许突发**:桶中有令牌时可突发处理 - **恒定平均速率**:长期平均速率恒定 --- #### **Java 实现** ```java public class TokenBucketRateLimiter { private final int capacity; // 桶容量 private final double refillRate; // 放入速率(令牌/毫秒) private double currentTokens; // 当前令牌数 private long lastRefillTime; // 上次放入时间 public TokenBucketRateLimiter(int capacity, double refillRatePerSec) { this.capacity = capacity; this.refillRate = refillRatePerSec / 1000.0; this.currentTokens = capacity; this.lastRefillTime = System.currentTimeMillis(); } public synchronized boolean allowRequest() { long now = System.currentTimeMillis(); // 放入令牌 double refillTokens = (now - lastRefillTime) * refillRate; currentTokens = Math.min(capacity, currentTokens + refillTokens); lastRefillTime = now; // 检查是否有令牌 if (currentTokens >= 1) { currentTokens -= 1; return true; } return false; } } ``` **使用示例**: ```java // 容量:100,放入速率:10 令牌/秒 TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100, 10); ``` --- #### **Guava RateLimiter(令牌桶实现)** ```java import com.google.common.util.concurrent.RateLimiter; // 创建限流器:每秒 100 个 permits RateLimiter rateLimiter = RateLimiter.create(100.0); // 尝试获取 permit if (rateLimiter.tryAcquire()) { // 通过 processRequest(); } else { // 被限流 rejectRequest(); } // 阻塞式获取(会等待) rateLimiter.acquire(); // 获取 1 个 permit rateLimiter.acquire(5); // 获取 5 个 permits ``` --- #### **优缺点** **优点**: - 允许突发流量 - 灵活配置 **缺点**: - 实现复杂 - 突发流量可能影响下游 --- ### 7. 漏桶 vs 令牌桶 | 特性 | 漏桶 | 令牌桶 | |------|------|--------| | **速率** | 恒定流出 | 恒定放入 | | **突发** | 不允许突发 | 允许突发 | | **适用** | 保护下游系统 | 通用场景 | | **平滑性** | 高 | 中 | **选择建议**: - 保护数据库等脆弱系统 → **漏桶** - API 接口限流 → **令牌桶** --- ### 8. 分布式限流 #### **基于 Redis(滑动窗口)** ```java @Service public class RedisRateLimiter { @Autowired private StringRedisTemplate redisTemplate; public boolean allowRequest(String key, int limit, int windowSizeSec) { long now = System.currentTimeMillis(); long windowStart = now - windowSizeSec * 1000; // Lua 脚本(原子操作) String luaScript = "local key = KEYS[1]\n" + "local now = tonumber(ARGV[1])\n" + "local windowStart = tonumber(ARGV[2])\n" + "local limit = tonumber(ARGV[3])\n" + // 删除过期记录 "redis.call('zremrangebyscore', key, '-inf', windowStart)\n" + // 获取当前窗口内计数 "local count = redis.call('zcard', key)\n" + // 检查是否超限 "if count < limit then\n" + " redis.call('zadd', key, now, now)\n" + " redis.call('expire', key, windowStart)\n" + " return 1\n" + "else\n" + " return 0\n" + "end"; // 执行 Lua 脚本 DefaultRedisScript script = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(key), String.valueOf(now), String.valueOf(windowStart), String.valueOf(limit)); return result == 1; } } ``` **使用示例**: ```java // 限制:每个 IP 每分钟 100 次请求 boolean allowed = redisRateLimiter.allowRequest("rate:limit:ip:" + ip, 100, 60); ``` --- #### **基于 Sentinel(阿里巴巴)** **引入依赖**: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` **配置限流规则**: ```java @Configuration public class SentinelConfig { @PostConstruct public void initFlowRules() { List rules = new ArrayList<>(); // 定义规则:QPS 限制 1000 FlowRule rule = new FlowRule(); rule.setResource("api"); // 资源名 rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 限流阈值类型 rule.setCount(1000); // 阈值 rule.setStrategy(RuleConstant.STRATEGY_DIRECT); // 流控策略 rules.add(rule); FlowRuleManager.loadRules(rules); } } ``` **使用注解**: ```java @RestController public class ApiController { @GetMapping("/api") @SentinelResource(value = "api", blockHandler = "handleBlock") public String api() { return "success"; } // 限流降级 public String handleBlock(BlockException ex) { return "Too many requests"; } } ``` **配置文件(动态规则)**: ```yaml # application.yml spring: cloud: sentinel: transport: dashboard: localhost:8080 # Sentinel Dashboard datasource: flow: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-flow-rules rule-type: flow ``` --- ### 9. 实际项目应用 #### **多级限流策略** ``` 用户级限流(单用户 QPS = 10) ↓ 接口级限流(总 QPS = 10000) ↓ 应用级限流(CPU < 80%) ↓ 数据库级限流(连接数 < 500) ``` --- #### **用户级限流(防刷)** ```java @Aspect @Component public class RateLimitAspect { @Autowired private RedisTemplate redisTemplate; @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = "rate:limit:user:" + getCurrentUserId(); int limit = rateLimit.limit(); int duration = rateLimit.duration(); // Redis + Lua 限流 boolean allowed = allowRequest(key, limit, duration); if (!allowed) { throw new RateLimitException("请求过于频繁,请稍后再试"); } return joinPoint.proceed(); } } ``` --- #### **接口级限流** ```java // Sentinel 配置不同接口的限流规则 FlowRule apiRule = new FlowRule(); apiRule.setResource("userApi"); apiRule.setCount(1000); FlowRule orderRule = new FlowRule(); orderRule.setResource("orderApi"); orderRule.setCount(500); ``` --- ### 10. 阿里 P7 加分项 **深度理解**: - 理解各种限流算法的适用场景和权衡 - 理解分布式限流的一致性问题 **实战经验**: - 有处理线上突发流量导致系统崩溃的经验 - 有设计多级限流策略的经验 - 有限流参数调优的经验(如何确定限流阈值) **架构能力**: - 能设计支持动态调整的限流系统 - 能设计限流的监控和告警体系 - 有灰度发布和降级预案 **技术选型**: - 了解 Sentinel、Hystrix、Resilience4j 等框架 - 有自研限流组件的经验 - 能根据业务特点选择合适的限流算法