限流算法:当你的系统变成“网红景点”,如何避免被游客挤垮?

15 阅读10分钟

限流算法:当你的系统变成“网红景点”,如何避免被游客挤垮?🏞️🚧

一个曾因没做限流,让公司系统在促销日变成“万人排队进厕所”的倒霉蛋。现在,我是限流算法的“景区管理员”。👮♂️

朋友们,想象一下这个令人窒息的场景:

你们公司新上线了一个“秒杀茅台”活动,原价1499,现价999!消息一出,全国黄牛倾巢而出。0点一到,瞬间涌入100万请求,而你的系统最多只能处理1万请求……

结果就是:服务器CPU飙到100%,内存溢出,数据库连接池爆满,最后——系统直接躺平,显示“502 Bad Gateway” 。用户骂娘,老板骂你,运维小哥提着刀在来的路上。🔪

这就是限流算法要解决的“生存还是毁灭”问题:在系统资源有限的情况下,如何优雅地拒绝过多的请求,保护系统不被流量冲垮?

一、限流是啥?系统的“景区承载量管理” 🎫

简单说,限流就是控制单位时间内进入系统的请求数量,保证系统在可承受的范围内稳定运行。

用“网红景点”来比喻:

  • 你的系统​ = 一个热门景区
  • 用户请求​ = 想进景区的游客
  • 系统处理能力​ = 景区的最大承载量
  • 限流算法​ = 景区门口的售票处和排队栏杆

核心目标:宁可让1000个人在门口有序排队(甚至拒绝一部分人),也绝不让2000人挤进去把景区踩塌!🏰

二、为什么需要限流?因为服务器不是“海绵宝宝”🧽

你可能会想:我多加点服务器不就行了?但现实是:

  1. 资源有上限:数据库连接数、线程池大小、CPU、内存、网络带宽... 都不是无限的
  2. 成本有限制:老板不可能让你无限扩容
  3. 突发流量不可预测:谁知道哪个网红明天就打卡你的系统了?

不加限流的惨痛教训

00:00:00 - 100万请求涌入
00:00:01 - CPU 100%,内存 90%
00:00:03 - 数据库连接池耗尽
00:00:05 - 服务完全不可用
00:00:10 - 隔壁服务被拖垮(雪崩效应)
00:00:30 - 全站瘫痪
00:10:00 - 你开始更新简历 💼

三、四大限流算法:景区管理的“四大门派” 🥋

1. 计数器算法:死板的“售票员” 🎟️

原理:在固定时间窗口内计数,超过阈值就拒绝

这个小时只卖1000张票,第1001个人来,就说“明日请早”

代码实现(简单版):

public class CounterLimiter {
    private long timeWindow = 1000; // 1秒
    private int limit = 10; // 1秒最多10次
    private long lastTime = System.currentTimeMillis();
    private int counter = 0;
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - lastTime > timeWindow) {
            // 新窗口,重置
            counter = 0;
            lastTime = now;
        }
        if (counter < limit) {
            counter++;
            return true; // 允许通过
        }
        return false; // 拒绝
    }
}

优点:简单,容易理解

缺点临界问题严重!比如限制1分钟100次,在59秒来了100次,1分01秒又来了100次,实际上2秒内处理了200次!

适用:对精度要求不高的简单场景

2. 滑动窗口算法:聪明的“检票员” 🎪

原理:把固定窗口细分,按小窗口滑动计数

把1小时分成60个1分钟的小窗口,实时滑动,更精确控制

代码思路

// 使用环形队列存储小窗口计数
class SlidingWindow {
    private long[] windows; // 小窗口计数数组
    private int windowSize; // 小窗口数量
    private long windowTime; // 小窗口时间(毫秒)
    private int limit; // 总限制
    
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        // 1. 清理过期的小窗口
        // 2. 计算当前所有小窗口的总计数
        // 3. 如果小于限制,当前小窗口计数+1,返回true
        // 4. 否则返回false
    }
}

优点:解决了计数器算法的临界问题,更平滑

缺点:实现稍复杂,小窗口划分影响精度

适用:大多数API限流场景

3. 漏桶算法:匀速的“接水工” 🪣

原理:请求像水一样流入漏桶,桶以固定速率出水(处理请求),桶满则溢出(拒绝请求)

想象一个底部有洞的桶:
- 水(请求)以任意速率流入
- 桶以固定速率从底部流出(处理)
- 桶满了,多余的水溢出(拒绝)

代码示例

public class LeakyBucketLimiter {
    private long capacity; // 桶容量
    private long rate; // 流出速率(请求/毫秒)
    private long water; // 当前水量
    private long lastLeakTime; // 上次漏水时间
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        // 先漏水:计算从上一次到现在漏了多少
        long leakAmount = (now - lastLeakTime) * rate;
        water = Math.max(0, water - leakAmount);
        lastLeakTime = now;
        
        // 再加水:如果桶没满,允许进入
        if (water < capacity) {
            water++;
            return true;
        }
        return false;
    }
}

优点绝对平滑,输出速率恒定,保护下游系统

缺点:无法应对突发流量(即使系统有能力处理,也被限死了)

适用:需要恒定速率处理的场景,如短信发送

4. 令牌桶算法:灵活的“售票机” 🎫🤖

原理:系统以固定速率往桶里放令牌,请求来时取走一个令牌,取到才能通过,桶空则拒绝

像游乐园的快速通行证发放机:
- 机器以固定速率产生通行证(令牌)
- 游客来了就取一张通行证
- 没通行证了就得排队等
- 但机器里可以攒一些通行证,应对突然来的旅行团

代码实现(Guava RateLimiter原理简化):

public class TokenBucketLimiter {
    private long capacity; // 桶容量
    private long tokens; // 当前令牌数
    private long rate; // 令牌产生速率(令牌/毫秒)
    private long lastRefillTime; // 上次补充时间
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        // 先补充令牌
        long tokensToAdd = (now - lastRefillTime) * rate;
        tokens = Math.min(capacity, tokens + tokensToAdd);
        lastRefillTime = now;
        
        // 再消费令牌
        if (tokens >= 1) {
            tokens--;
            return true;
        }
        return false;
    }
}

优点允许突发流量(桶里有令牌就能用),灵活实用

缺点:实现相对复杂

适用绝大多数场景,特别是需要应对突发流量的API

四、四大算法对比:谁是你的“真命天子”?👑

算法比喻优点缺点适用场景
计数器死板售票员简单粗暴临界问题简单限流,如短信验证码
滑动窗口智能检票员解决临界问题实现稍复杂API限流,Web应用
漏桶匀速接水工输出绝对平滑无法应对突发消息队列,恒定速率处理
令牌桶灵活售票机允许突发,灵活实现复杂绝大多数API限流

选择口诀

  • 要简单,用计数器
  • 要平滑,用漏桶
  • 要灵活,用令牌桶
  • 要折中,用滑动窗口

五、工作中的“防坑”指南:别让限流变成“自残”🔪

1. 阈值不是拍脑袋定的!

错误做法

// 随便写个数字
rateLimiter.setRate(100); // 为什么是100?不知道!

正确做法

  1. 压测:先知道系统最大处理能力(如单机1000 QPS)
  2. 留余量:设置为最大能力的70%-80%(如700-800 QPS)
  3. 动态调整:根据监控数据动态调整

2. 别只限流,要给用户“体面的拒绝”😇

// ❌ 糟糕:直接返回错误
return Response.error("系统繁忙");

// ✅ 优秀:友好提示 + 建议
return Response.error("当前排队人数较多,建议您稍后再试")
        .setRetryAfter(30); // 告诉客户端30秒后重试

// ✅ 更优秀:返回排队信息
{
    "code": 429,
    "msg": "当前排队人数:1523,预计等待时间:45秒",
    "suggest": "您可以先逛逛其他商品"
}

3. 分布式限流的陷阱

单机限流简单,但集群呢?

// ❌ 错误:每个实例各自为政
// 实例A限100,实例B限100,负载均衡下,总共可能收到200请求

// ✅ 正确:使用Redis等集中式存储
// 所有实例共享一个计数器
String key = "rate_limit:" + userId;
Long count = redis.incr(key);
if (count == 1) {
    redis.expire(key, 60); // 设置过期
}
if (count > 100) {
    return false; // 限流
}

4. 不同用户区别对待

别把VIP和普通用户一视同仁

// 根据用户等级设置不同限流阈值
int limit = 100; // 默认
if (user.isVip()) {
    limit = 1000; // VIP有特权
}
if (user.isBlacklist()) {
    limit = 1; // 黑名单严格限制
}

5. 监控和动态调整

没有监控的限流是“瞎子摸象”:

// 关键监控指标
1. 请求总量
2. 通过量 vs 拒绝量
3. 响应时间(限流后应该稳定)
4. 系统负载(CPU、内存、线程池)

// 动态调整:根据监控自动调整阈值
if (cpu > 80%) {
    rateLimiter.adjustRate(0.8); // 下调20%
} else if (cpu < 30% && queueSize > 0) {
    rateLimiter.adjustRate(1.2); // 上调20%
}

六、实战代码:用Guava RateLimiter优雅限流 🛡️

Google Guava的RateLimiter是令牌桶算法的工业级实现,简单又好用:

// 1. 引入依赖
// <dependency>
//     <groupId>com.google.guava</groupId>
//     <artifactId>guava</artifactId>
//     <version>31.0.1-jre</version>
// </dependency>

// 2. 创建限流器
private RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10个令牌

// 3. 在方法中使用
@GetMapping("/api/茅台秒杀")
public Response seckill(@RequestParam Long userId) {
    // 尝试获取令牌,非阻塞
    if (!rateLimiter.tryAcquire()) {
        return Response.error("手速太快了,稍后再试哦~");
    }
    
    // 或者阻塞等待(不超过指定时间)
    if (!rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) {
        return Response.error("排队超时,请重试");
    }
    
    // 执行业务逻辑
    return doSeckill(userId);
}

// 4. 预热模式(应对突发流量)
private RateLimiter warmupLimiter = RateLimiter.create(10, 3, TimeUnit.SECONDS);
// 每秒10个令牌,但有3秒预热期,从慢速逐渐达到全速

七、限流的最佳实践:做聪明的“景区管理员” 🧠

1. 分层限流:从外到内层层过滤

用户请求 → CDN限流 → 网关限流 → 应用限流 → 数据库限流

每一层都做限流,避免流量击穿到最脆弱的数据库。

2. 多维度限流:多角度识别“危险分子”

// 不只用IP,多维度组合
String key = String.format("limit:%s:%s:%s", 
    userId, 
    apiPath, 
    TimeUnit.MINUTES.toSeconds(System.currentTimeMillis() / 60000)
);
// 按用户+接口+分钟限流,更精准

3. 热点数据特殊处理

// 识别热点商品
if (isHotProduct(productId)) {
    // 热点商品用更严格的限流
    return hotProductRateLimiter.tryAcquire();
} else {
    return normalRateLimiter.tryAcquire();
}

4. 失败重试的退避策略

// 被限流后,别让客户端立即重试(避免雪崩)
@ControllerAdvice
public class RateLimitHandler {
    @ExceptionHandler(RateLimitException.class)
    public Response handleRateLimit(RateLimitException e) {
        // 返回Retry-After头,告诉客户端多久后重试
        response.setHeader("Retry-After", "30");
        return Response.error("太火爆了,请30秒后重试");
    }
}

八、灵魂拷问:你真的需要限流吗?🤔

在实现限流前,先问自己:

  1. 能不能扩容?加机器能解决就不需要复杂限流
  2. 能不能降级?关闭非核心功能,保核心功能
  3. 能不能缓存?用缓存扛住大部分读请求
  4. 能不能排队?用消息队列异步处理,避免同步阻塞

限流是最后一道防线,不是第一选择!

结语:限流是门艺术,不是暴力拒绝 🎨

一个优秀的限流系统,应该像迪士尼的快速通行证系统:

  • VIP有特权:不同用户区别对待
  • 排队有预期:告诉用户要等多久
  • 系统不崩溃:保证核心体验
  • 体验不降级:即使被限流,也让用户觉得“合理”

记住限流的终极目标:不是拒绝用户,而是在系统能力范围内,服务尽可能多的用户

现在,带上这些限流算法和最佳实践,去守护你的系统吧!让它既能享受“网红景点”的流量红利,又不会在流量洪水中“翻船”。🚤🌊

(当你的系统再次面临流量冲击时,希望你能优雅地说:“别挤别挤,排队进场,人人有份!”)😉