3. 限流降级核心策略详解
限流和降级,说白了就是给系统装个「安全阀」。我做了这么多年高并发系统,见过太多因为没装这个阀而崩掉的案例。今天咱们就把这块掰开揉碎了讲清楚。
3.1 限流策略:控制流量入口
限流的核心思想很简单——控制单位时间内的请求量。但实现方式各有千秋,我挑几个最常用的说说。
3.1.1 计数器法
这是最直观的方式。维护一个计数器,每秒重置一次。
// 伪代码示例
public class CounterLimiter {
private long currentTime = System.currentTimeMillis();
private int count = 0;
private final int limit = 100; // 每秒最多100个请求
public boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - currentTime > 1000) {
count = 0;
currentTime = now;
}
count++;
return count <= limit;
}
}
注意:计数器法有个经典问题——临界突变。比如每秒限100个请求,用户可以在第1秒的最后一毫秒发100个,第2秒的第一毫秒再发100个。实际200ms内就打了200个请求,系统照样扛不住。我曾经在一个抢购项目里就踩过这个坑,后来才换成了滑动窗口。
3.1.2 滑动窗口算法
滑动窗口把时间切得更细。比如把1秒切成10个格子,每个格子100ms。窗口滑动时,统计的是当前时间往前推1秒内的所有格子数据。
| 特性 | 计数器法 | 滑动窗口 |
|---|---|---|
| 精度 | 低(秒级突变) | 高(可到毫秒级) |
| 内存占用 | 低(1个计数器) | 中(N个格子) |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 低精度限流 | 大部分生产环境 |
我个人习惯在生产环境用滑动窗口。虽然多占点内存,但精度够用,实现也不复杂。
3.1.3 漏桶算法
漏桶像个水桶——请求先进来存着,然后以固定速率漏出去。桶满了就拒绝新请求。
// 漏桶核心逻辑
public class LeakyBucket {
private long lastLeakTime = System.currentTimeMillis();
private int water = 0;
private final int capacity = 100; // 桶容量
private final int rate = 10; // 每秒漏10个
public boolean tryAcquire() {
long now = System.currentTimeMillis();
// 先漏水
water = Math.max(0, water - (int)((now - lastLeakTime) / 1000 * rate));
lastLeakTime = now;
if (water < capacity) {
water++;
return true;
}
return false; // 桶满了,拒绝
}
}
我的经验:漏桶适合需要「平滑流量」的场景,比如数据库写入。但缺点也很明显——无法应对突发流量。你想想看,如果突然来一波大流量,漏桶只会按固定速率处理,后面的请求全得排队等,延迟会飙升。
3.1.4 令牌桶算法
令牌桶是我最常用的限流算法。它允许一定程度的突发流量,同时又能控制平均速率。
工作原理:
- 系统以固定速率往桶里放令牌
- 请求来了,先拿令牌,拿到就处理,拿不到就等或拒绝
- 桶有上限,令牌满了就不再增加
// 令牌桶实现
public class TokenBucket {
private long lastRefillTime = System.currentTimeMillis();
private int tokens;
private final int capacity = 100; // 桶容量
private final int rate = 10; // 每秒放10个令牌
public boolean tryAcquire(int needTokens) {
long now = System.currentTimeMillis();
// 补充令牌
tokens = Math.min(capacity, tokens + (int)((now - lastRefillTime) / 1000 * rate));
lastRefillTime = now;
if (tokens >= needTokens) {
tokens -= needTokens;
return true;
}
return false;
}
}
核心优势:令牌桶允许突发。比如桶里有100个令牌,突然来100个请求,可以一次性全部处理。这很适合电商秒杀、热点新闻等场景。我之前的项目用令牌桶,把QPS从5000平滑提升到了12000,效果很明显。
3.2 降级策略:有舍才有得
限流是「挡」,降级是「舍」。系统快扛不住时,主动放弃一些非核心功能,保证核心业务可用。
3.2.1 降级分类
| 降级类型 | 说明 | 典型场景 |
|---|---|---|
| 读降级 | 直接返回缓存或默认值 | 商品详情页降级为缓存数据 |
| 写降级 | 异步写入或丢弃写请求 | 日志收集降级为本地文件 |
| 功能降级 | 关闭非核心功能 | 评论、推荐等模块暂时关闭 |
| 服务降级 | 熔断下游服务调用 | 调用第三方API超时直接返回兜底 |
3.2.2 降级触发条件
降级不能乱降,得有明确的触发条件。我一般设置三层阈值:
- 警告线(70%负载):记录日志,准备降级预案
- 降级线(85%负载):自动触发非核心功能降级
- 熔断线(95%负载):强制熔断,只保留核心入口
我曾经踩过的坑:有一次降级条件设得太敏感,系统刚有点波动就降级了,结果用户反馈说「评论功能怎么没了」。后来我加了「持续观察期」——必须连续5秒都超过阈值才触发降级,避免毛刺导致误降。
3.3 限流降级策略对比
为了让你看得更清楚,我把几种策略放在一起对比:
| 策略 | 核心思想 | 突发处理 | 实现难度 | 推荐场景 |
|---|---|---|---|---|
| 计数器 | 固定窗口计数 | 差 | 低 | 简单限流,不推荐生产 |
| 滑动窗口 | 细粒度时间窗口 | 中 | 中 | 通用限流场景 |
| 漏桶 | 固定速率流出 | 差 | 中 | 需要平滑流量的场景 |
| 令牌桶 | 固定速率放令牌 | 好 | 中 | 允许突发流量的场景 |
| 降级 | 主动舍弃非核心 | — | 高 | 系统过载时的保命手段 |
3.4 策略选择指南
说了这么多,到底怎么选?我个人的经验是:
- API网关层:用令牌桶,允许突发但控制总量
- 数据库写入:用漏桶,平滑写入避免打爆DB
- 核心业务:限流+降级组合使用,限流挡外部流量,降级保内部核心
- 第三方调用:必须加熔断降级,防止下游故障拖垮自己
小建议:别想着一步到位。先上线最简单的限流策略,然后根据监控数据逐步优化。我见过太多团队一开始就搞复杂的限流方案,结果上线后问题一堆。说白了,先跑起来,再优化。
3.5 本章知识体系
下面这张图把本章的核心策略和关系梳理清楚了:
嗯,这张图把本章的核心内容都串起来了。左边是限流,右边是降级,两者配合才能构建一个健壮的高并发系统。记住一句话:限流是防守,降级是取舍。