Files
interview/questions/rate-limiting.md
yasinshaw 71e3497bfd feat: add comprehensive system design interview questions
- design-seckill.md: 秒杀系统设计
- design-shorturl.md: 短链接系统设计
- design-lbs.md: LBS附近的人系统设计
- design-im.md: 即时通讯系统设计
- design-feed.md: 社交信息流系统设计

Each document includes:
- Requirements analysis and data volume assessment
- Technical challenges
- System architecture design
- Database design
- Caching strategies
- Scalability considerations
- Practical project experience
- Alibaba P7 level additional points

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 23:43:36 +08:00

668 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 限流策略与算法
## 问题
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<Long> 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
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
```
**配置限流规则**
```java
@Configuration
public class SentinelConfig {
@PostConstruct
public void initFlowRules() {
List<FlowRule> 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 等框架
- 有自研限流组件的经验
- 能根据业务特点选择合适的限流算法