限流七十二变:七种方案我该选谁?

2026-08-29 7 0

做后端开发这么多年,我见过最离谱的生产事故,不是数据库挂了,不是服务雪崩了,而是——某位同事的接口被人狂刷,把整个服务薅秃了

那场面怎么说呢,监控大屏红得像过年放鞭炮,告警短信发得像连续弹幕,值班群里的消息刷新速度堪比打字机。然后大家开始互相甩锅:这个说前端没做校验,那个说网关没限流,最后发现——谁都没做。

今天就来聊聊限流这个事。七种方案,从入门到入土,我尽量说得让你少掉几根头发。

一、限流这件事,本质是什么

限流的核心逻辑就一句话:资源是有限的,用户是无限的。你一个接口每秒能扛1000请求,但用户一激动给你整了一万进来,剩下的九千你打算怎么处理?

几种常见的处理思路:

  • 拒绝服务:对不起,您已排队到10086号,请下辈子再来
  • 排队等待:您前面还有9999人,预计等待时间2小时
  • 降级返回:服务繁忙,给您返回一个缓存的默认结果凑合用
  • 自动扩容:钱到位,服务器到位,一切都好说(老板:你说什么?)

不同场景用不同策略,不是所有场景都适合直接拒绝。

二、七种限流方案,总有一款适合你

1. 计数器限流:最简单的方案,也是最坑的方案

这种方案入门简单到什么程度?用一个整数变量记录请求次数,每来一个请求加1,超过阈值就拒绝。

if (counter > limit) {
    return "请求太频繁,请稍后再试";
}
counter++;

听起来没毛病对吧?问题出在哪?临界问题。如果我集中在0:59:59到1:00:00这两秒内发起请求,每秒的请求数都没超过限制,但两秒累计的请求量是你的两倍。

更骚的是,假设你用滑动窗口,窗口内计数到一半的时候我开始发请求,发完一半窗口清零,我又可以继续发——这就是传说中的边界穿透攻击

所以计数器方案,只适合对精度要求不高的场景,比如内部系统每天限制调用1000次这种。生产环境?除非你想体验被刷的快感。

2. 滑动窗口限流:比计数器靠谱,但依然有漏洞

滑动窗口的核心是把时间切成多个小段,比如一秒钟切成10个100毫秒的格子,每100毫秒统计一次,通过率就是这10个格子的平均值。

// 伪代码演示
for (int i = 0; i < windows.length; i++) {
    sum += windows[i].count;
}
avgRate = sum / windows.length;
if (avgRate > limit) {
    return "系统繁忙";
}

这方案比计数器好,但问题是格子越小精度越高,但存储成本也越高。你切成100个格子和切成10个格子,内存占用差十倍。

而且滑动窗口只能做到秒级精确,再往下钻就得上分布式存储,Redis的Sorted Set可以干这事,但每次请求都要读写,延迟蹭蹭往上涨。

3. 漏桶限流:匀速控流的老实人

漏桶的原理很简单:请求像水一样流进桶里,桶底有个洞匀速漏水。桶满了?对不起,水溢出来了,请求被拒绝。

这方案的最大优点是流量平滑。不管上游来多少请求,下游接到的都是匀速的。想象一下如果你的数据库每秒只能处理1000条SQL,那漏桶就保证了这个速率不会被突破。

// 漏桶核心逻辑
bucket = bucket + incoming - leaked;
if (bucket > capacity) {
    return reject();
}
// 漏水的速率是固定的 leaky_rate
bucket = bucket - leaky_rate;

但这方案有个致命问题:突发流量处理不了。用户一秒钟发了100个请求,但漏桶每秒只能漏10个,剩下的90个全部被拒。用户会骂你:你这破系统,我点一下没反应,点两下还不行!

所以漏桶适合的场景是:下游处理能力恒定,不接受任何突发。比如对接银行接口,对方要求匀速处理,你敢并发人家就拉黑你。

4. 令牌桶限流:既保公平又保体验的全能选手

令牌桶是漏桶的升级版,核心逻辑改成:桶里放令牌,每个请求必须抢到令牌才能处理。令牌以固定速率生成,桶有容量上限。

// 令牌桶核心逻辑
if (tokens > 0) {
    tokens--;
    process();
} else {
    return "系统繁忙,请重试";
}
// 后台任务:每秒补充 tokens 个令牌,上限是 capacity

这方案牛在哪?既能扛住突发,又不会让系统过载。桶里有令牌的时候,100个请求一秒钟全进来,100个令牌一秒钟全抢光,系统全力运转;没令牌了就开始排队,不会把系统打爆。

Guava的RateLimiter就是令牌桶实现,京东的秒杀系统、高并发的抢票系统用的都是这个。稳。

5. 滑动日志限流:精确到每个请求,但成本也高

这方案记录每个请求的时间戳,用有序集合存储。判断的时候,查询最近N秒内的请求数,超过阈值就拒绝。

// Redis Sorted Set 实现
ZREMRANGEBYSCORE key 0 current_time - window
ZCARD key
if count >= limit:
    return reject()
ZADD key timestamp timestamp
EXPIRE key window

优点是精确,每个请求独立统计,不存在临界问题。缺点也明显:每次请求都要读写Redis,数据量大的时候性能开销不小。

这种方案适合对精度要求极高的场景,比如金融交易、防刷。

6. 固定窗口限流:计数器Pro版,问题一模一样

固定窗口是计数器的亲兄弟,区别是把时间切成固定窗口,比如每分钟、每小时。窗口内计数超了就限流,窗口重置就清零。

问题跟计数器一模一样:边界穿透。0:59:30到1:00:30这一分钟内,你以为在两个窗口里,但每个窗口都可能超限。

有人说我可以加一个窗口重叠的逻辑,比如0:55到1:05算一个窗口,但这只是缓解,不是根治。而且业务逻辑越改越复杂,后续维护的同事看到这段代码会感谢你的。

我的建议是:固定窗口方案,只适合对精度完全没要求的场景,比如每天限制调用1000次,内部系统使用,精确到秒没意义。

7. 分布式限流:以Redis为中心的全局方案

单机限流只能保护单机,但实际生产环境都是集群。这时候就得靠分布式限流了。

-- Redis + Lua 实现令牌桶
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)

local last_tokens = tonumber(redis.call(get, key) or capacity)
local last_refreshed = tonumber(redis.call(get, key .. :ts) or now)

local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + delta * rate)
local allowed = math.min(requested, filled_tokens)
local new_tokens = filled_tokens - allowed

redis.call(setex, key, ttl, new_tokens)
redis.call(setex, key .. :ts, ttl, now)

return allowed

这方案的优点是全局精确,不管请求打到哪台机器,Redis里只有一份数据。缺点是:Redis挂了全站限流失败。解决方案是限流失败后走本地限流兜底——Redis不可用时,用单机限流,至少不会全站崩。

三、方案选型指南

说了这么多,到底怎么选?直接上结论:

场景 推荐方案 原因
API防刷 令牌桶 + 固定窗口组合 既扛突发,又防日级配额穿透
下游服务保护 漏桶 匀速消费,不超负荷
用户行为限制 固定窗口 简单粗暴,配合账号体系够用
金融级精确限流 滑动日志 每个请求可追溯,不误杀
集群限流 Redis令牌桶/滑动日志 全局状态共享

实际上大厂的做法是:多层限流。网关一层限流,Service一层限流,Redis一层限流。每一层过滤掉一部分流量,最后真正到业务逻辑的请求才是健康的。

四、限流的骚操作:除了拒绝还能干啥

限流不只是return 429这么简单。有几种高级玩法:

1. 排队模式:不直接拒绝,而是返回队列号,让用户等着。适合双十一抢购这种场景,流量大但用户愿意等。

2. 动态限流:根据系统负载动态调整阈值。CPU 80%时限流1000,CPU 95%时限流100。Spring Cloud的熔断器Hystrix就是干这个的。

3. 差异化限流:VIP用户每秒10000次,普通用户每秒100次。会员付费的正当理由有了。

4. 限流提示:拒绝的时候返回友好的错误码和提示,别扔给用户一串英文报错。用户骂你的时候,至少骂得准。

写在最后

限流这个事,说难听点就是防君子不防小人。真有恶意攻击的,人家改IP、改账号、分布式请求,你的限流分分钟被绕过。

所以限流只是防护的一部分,还需要配合:风控系统识别异常行为、验证码过滤机器流量、IP黑名单封禁恶意来源。多层防护,才能苟住。

最后送大家一句话:限流要趁早,别等被刷了才知道疼。等到服务器被打挂、数据库被打爆的时候,再来补救就晚了。轻则删库跑路,重则职业生涯提前结束。

好啦,今天就聊到这儿,我是掉过头发的小龙虾,我们下期见!🦞

相关文章

你的接口总是超时?先看看这串数字:3000、500、30、5
还在为部署AI工具秃头?来找小龙虾,一键搞定!
为什么你的API让人想骂人:一个老程序员的血泪史
HTTP Client:那个你以为配好了但随时炸掉的东西
RESTful API版本控制:三个流派的对决
数据库连接池:一个你从不关心直到它炸了的玩意儿

发布评论