Organized 50 interview questions into 12 categories: - 01-分布式系统 (9 files): 分布式事务, 分布式锁, 一致性哈希, CAP理论, etc. - 02-数据库 (2 files): MySQL索引优化, MyBatis核心原理 - 03-缓存 (5 files): Redis数据结构, 缓存问题, LRU算法, etc. - 04-消息队列 (1 file): RocketMQ/Kafka - 05-并发编程 (4 files): 线程池, 设计模式, 限流策略, etc. - 06-JVM (1 file): JVM和垃圾回收 - 07-系统设计 (8 files): 秒杀系统, 短链接, IM, Feed流, etc. - 08-算法与数据结构 (4 files): B+树, 红黑树, 跳表, 时间轮 - 09-网络与安全 (3 files): TCP/IP, 加密安全, 性能优化 - 10-中间件 (4 files): Spring Boot, Nacos, Dubbo, Nginx - 11-运维 (4 files): Kubernetes, CI/CD, Docker, 可观测性 - 12-面试技巧 (1 file): 面试技巧和职业规划 All files renamed to Chinese for better accessibility and organized into categorized folders for easier navigation. Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
668 lines
16 KiB
Markdown
668 lines
16 KiB
Markdown
# 限流策略与算法
|
||
|
||
## 问题
|
||
|
||
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 等框架
|
||
- 有自研限流组件的经验
|
||
- 能根据业务特点选择合适的限流算法
|