- 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>
16 KiB
16 KiB
限流策略与算法
问题
- 为什么需要限流?常见的限流场景有哪些?
- 有哪些常见的限流算法?各自的原理和优缺点是什么?
- 固定窗口算法有什么问题?如何优化?
- 滑动窗口算法是如何实现的?
- 令牌桶和漏桶算法的区别是什么?
- 分布式限流如何实现?(Redis、Sentinel)
- 在实际项目中,你是如何设计限流策略的?
标准答案
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 实现
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;
}
}
使用示例:
// 限制:每分钟 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 实现(环形数组)
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;
}
}
使用示例:
// 限制:每分钟 100 次请求,分为 6 个槽位(每 10 秒一个)
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(100, 60 * 1000, 6);
Redis 实现(Redisson 的 RRateLimiter)
@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 实现
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;
}
}
使用示例:
// 容量:100,漏水速率:10 请求/秒
LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(100, 10);
优缺点
优点:
- 平滑流量,恒定速率
- 保护下游系统
缺点:
- 无法应对突发流量
- 参数调整困难
6. 令牌桶算法
原理
系统以恒定速率向桶中放入令牌:
- 请求到达时,从桶中获取令牌
- 有令牌则通过,无令牌则拒绝
- 桶满时,令牌溢出
图解:
恒定速率放入令牌
↓
┌───┐
│ ○○○│ ← 令牌桶(容量 = C)
│ ○○○│
└───┘ ↓
请求获取令牌
特点:
- 允许突发:桶中有令牌时可突发处理
- 恒定平均速率:长期平均速率恒定
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;
}
}
使用示例:
// 容量:100,放入速率:10 令牌/秒
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100, 10);
Guava RateLimiter(令牌桶实现)
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(滑动窗口)
@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;
}
}
使用示例:
// 限制:每个 IP 每分钟 100 次请求
boolean allowed = redisRateLimiter.allowRequest("rate:limit:ip:" + ip, 100, 60);
基于 Sentinel(阿里巴巴)
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置限流规则:
@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);
}
}
使用注解:
@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";
}
}
配置文件(动态规则):
# 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)
用户级限流(防刷)
@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();
}
}
接口级限流
// 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 等框架
- 有自研限流组件的经验
- 能根据业务特点选择合适的限流算法