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 降级触发条件

降级不能乱降,得有明确的触发条件。我一般设置三层阈值:

  1. 警告线(70%负载):记录日志,准备降级预案
  2. 降级线(85%负载):自动触发非核心功能降级
  3. 熔断线(95%负载):强制熔断,只保留核心入口

我曾经踩过的坑:有一次降级条件设得太敏感,系统刚有点波动就降级了,结果用户反馈说「评论功能怎么没了」。后来我加了「持续观察期」——必须连续5秒都超过阈值才触发降级,避免毛刺导致误降。

3.3 限流降级策略对比

为了让你看得更清楚,我把几种策略放在一起对比:

策略 核心思想 突发处理 实现难度 推荐场景
计数器 固定窗口计数 差 低 简单限流,不推荐生产
滑动窗口 细粒度时间窗口 中 中 通用限流场景
漏桶 固定速率流出 差 中 需要平滑流量的场景
令牌桶 固定速率放令牌 好 中 允许突发流量的场景
降级 主动舍弃非核心 — 高 系统过载时的保命手段

3.4 策略选择指南

说了这么多,到底怎么选?我个人的经验是:

  • API网关层:用令牌桶,允许突发但控制总量
  • 数据库写入:用漏桶,平滑写入避免打爆DB
  • 核心业务:限流+降级组合使用,限流挡外部流量,降级保内部核心
  • 第三方调用:必须加熔断降级,防止下游故障拖垮自己

小建议:别想着一步到位。先上线最简单的限流策略,然后根据监控数据逐步优化。我见过太多团队一开始就搞复杂的限流方案,结果上线后问题一堆。说白了,先跑起来,再优化。

3.5 本章知识体系

下面这张图把本章的核心策略和关系梳理清楚了:

限流降级核心策略体系 高并发防护策略 限流策略 降级策略 计数器 滑动窗口 令牌桶 读降级 写降级 功能降级 核心原则:限流挡外部流量,降级保内部核心 实际生产中通常组合使用,根据系统负载动态调整策略

嗯,这张图把本章的核心内容都串起来了。左边是限流,右边是降级,两者配合才能构建一个健壮的高并发系统。记住一句话:限流是防守,降级是取舍。