瞧瞧别人家的限流,那叫一个优雅!

前言

去年夏天某个凌晨,我接到某金融平台报警:支付接口错误率飙升至35%。

赶到机房时,发现数据库连接池耗尽,大量请求堆积成山------这就是典型的未做限流防护的灾难现场。

就像高速公路不设收费站,高峰期必然堵成停车场。

限流的本质 不是拒绝服务,而是用可控的牺牲保护核心链路

某电商大促时,他们用令牌桶算法将秒杀接口QPS限制在5万,虽然流失了20%的突发流量,但保住了99%的核心交易成功率。

1 常用限流方案

1.1 固定窗口计数器

核心原理:

以固定时间窗口(如1秒)为周期,统计周期内请求数,超过阈值则拒绝后续请求。

具体代码实现如下:

java 复制代码
// 线程安全实现(AtomicLong优化版)
public class FixedWindowCounter {
    private final AtomicLong counter = new AtomicLong(0);
    private volatile long windowStart = System.currentTimeMillis();
    private final int maxRequests;
    private final long windowMillis;

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - windowStart > windowMillis) {
            if (counter.compareAndSet(counter.get(), 0)) {
                windowStart = now;
            }
        }
        return counter.incrementAndGet() <= maxRequests;
    }
}

致命缺陷:

假设设置1秒100次限制,0.9秒时突发100次请求,下一秒0.1秒又放行100次,实际两秒内通过200次。

就像红绿灯切换时车辆抢行,容易引发"临界点突刺"。

适用场景:

日志采集、非关键性接口的粗粒度限流

1.2 滑动窗口

核心原理:

将时间窗口细分为更小的时间片(如10秒),统计最近N个时间片的请求总和。

基于Redis的Lua脚本如下:

java 复制代码
// Redis Lua实现滑动窗口(精确到毫秒)
String lua = """
    local now = tonumber(ARGV
    local window = tonumber(ARGV
    local key = KEYS[1]
    
    redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
    local count = redis.call('ZCARD', key)
    
    if count < tonumber(ARGV then
        redis.call('ZADD', key, now, now)
        redis.call('EXPIRE', key, window/1000)
        return 1
    end
    return 0
    """;

技术亮点:

某证券交易系统采用滑动窗口后,将API异常率从5%压降至0.3%。

通过Redis ZSET实现时间切片,误差控制在±10ms内。

优势对比

指标 固定窗口 滑动窗口
时间精度 1秒 100ms
临界突刺问题 存在 消除
实现复杂度 简单 中等

2.3 漏桶算法

核心原理:

请求像水流一样进入漏桶,系统以固定速率处理请求。

桶满时新请求被丢弃。

具体实现如下:

java 复制代码
// 漏桶动态实现(Semaphore优化版)
public class LeakyBucket {
    private final Semaphore permits;
    private final ScheduledExecutorService scheduler;

    public LeakyBucket(int rate) {
        this.permits = new Semaphore(rate);
        this.scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> permits.release(rate), 1, 1, TimeUnit.SECONDS);
    }

    public boolean tryAcquire() {
        return permits.tryAcquire();
    }
}

技术痛点:

某智能家居平台用此方案,确保即使10万台设备同时上报数据,系统仍按500条/秒的速率稳定处理。

但突发流量会导致队列积压,就像用漏斗倒奶茶------珍珠容易卡住。

适用场景:

IoT设备控制指令下发、支付渠道限额等需要严格恒定速率的场景

1.4 令牌桶算法

核心原理:

以固定速率生成令牌,请求需获取令牌才能执行。

突发流量可消耗桶内积攒的令牌。

具体实现如下:

java 复制代码
// Guava RateLimiter高级用法
RateLimiter limiter = RateLimiter.create(10.0, 1, TimeUnit.SECONDS); // 初始预热
limiter.acquire(5); // 尝试获取5个令牌

// 动态调整速率(需反射实现)
Field field = RateLimiter.class.getDeclaredField("tokens");
field.setAccessible(true);
AtomicDouble tokens = (AtomicDouble) field.get(limiter);
tokens.set(20); // 突发时注入20个令牌

实战案例:

某视频平台用此方案应对热点事件:平时限制10万QPS,突发时允许3秒内超限50%,既防雪崩又保用户体验。

动态特性

  • 正常时限制QPS
  • 突发时允许透支
  • 持续突发会耗尽令牌

我的技术网站,海量八股文、项目实战这里都有:www.susan.net.cn,欢迎大家访问。

2 生产环境实战

2.1 网关层分布式限流

某电商双11方案:通过Redis+Lua实现分布式计数,配合Nginx本地缓存,在网关层拦截了83%的恶意请求。

2.2 自适应熔断机制

我们还需要自适应熔断机制。

某社交平台用此方案,在突发流量时自动将限流阈值从5万降到3万,等系统恢复后再逐步提升。

3 避坑指南与性能优化

3.1 致命误区

在数据库连接池前做限流!

某公司曾因此导致连接泄漏,最终撑爆数据库。

正确做法应遵循熔断三原则

  1. 快速失败:在入口层拦截无效请求
  2. 动态降级:核心服务保留最小资源
  3. 自动恢复:熔断后渐进式放量

3.2 性能优化

某金融系统通过JMH测试发现,使用LongAdder替代AtomicLong,限流吞吐量提升220%。

性能优化手段:减少CAS竞争 和 分段锁基座。

总结

上面列举了工作中最常用的4种限流方案。

对于不同的业务场景,我们需要选择不同的限流方案。

限流的黄金法则如下:

记住:好的限流方案就像高铁闸机------既保证通行效率,又守住安全底线。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。