首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >接口被刷爆怎么办?限流算法选型与 Redis 实现实战

接口被刷爆怎么办?限流算法选型与 Redis 实现实战

原创
作者头像
用户5658160
发布2026-09-11 16:17:41
发布2026-09-11 16:17:41
210
举报

导读:读完本文你能拿到三样东西:①四种常见限流算法的适用场景对照表;②基于 Redis + Lua 的原子性限流脚本(可直接抄走);③一套从单机到分布式的限流方案演进清单。适合后端、中间件开发者参考,也适合排查线上接口被突增流量打满的场景。

一、为什么要限流:先想清楚限的是谁

限流不是为了"不让用户用",而是把超过系统承载能力的流量挡在入口之外,保护数据库、下游服务和第三方依赖不被拖垮。它和熔断、降级配合使用:限流管"进不来",熔断管"我不调",降级管"给兜底"。

判断要不要限流的三个信号:

  1. 接口依赖了数据库或外部服务,且没有连接池之外的兜底;
  2. 接口被脚本、爬虫或异常客户端高频调用(可用率正常但 QPS 异常偏高);
  3. 活动节点(秒杀、抢券、抽奖)有明确的峰值预期,但服务副本数固定。

注意:限流是"丢请求"的策略,任何限流都会牺牲部分请求,所以阈值要按"上游能容忍的失败率 × 单机能力"来定,而不是拍脑袋。

二、四种常见算法的原理与选型

2.1 固定窗口计数

以固定时间窗口(如 1 秒)为单位计数,窗口内超过阈值就拒绝。实现最简单,但存在临界突刺:窗口边界处各计一半,实际 2 倍流量可能在同一瞬间通过。

2.2 滑动窗口

把窗口细分为若干小格(如 1 秒窗口切成 10 个 100ms 小格),每次判断时把整个窗口内的小格计数求和。平滑了临界突刺,Redis 实现时用 ZSET 记录每个请求的时间戳即可,但内存占用随流量线性增长。

2.3 漏桶

请求先进桶,桶按固定速率出水。优点是输出速率恒定、对下游最友好;缺点是无法应对突发流量——即使系统有富余能力,也只能匀速处理。

2.4 令牌桶

以固定速率往桶里放令牌,请求取到令牌才能通过,桶容量允许一定量的突发。这是生产中使用最广的方案:既能限平均速率,又能容忍短期突发。

2.5 选型对照

算法

允许突发

实现成本

内存/存储

典型场景

固定窗口

边界突发

极低

极低

简单计数、防刷

滑动窗口

平滑

随粒度增长

精确限流

漏桶

不支持

保护下游匀速消费

令牌桶

支持

通用接口限流

建议:第一版用固定窗口即可上线;有突发流量且希望平滑时换令牌桶;需要精确到毫秒级控制时用滑动窗口。

2.6 阈值怎么定:先算容量,再定限流值

限流值不是拍脑袋定的,推荐一条计算链:

  1. 压测单机容量:用压测工具打到接口错误率超过阈值(如 1%)或 RT 超时(如 500ms),记下此时 QPS,这就是单机上限;
  2. 乘上副本数:单机上限 × 实例数 × 冗余系数(建议 0.6~0.8),得到集群上限;
  3. 留出下游余量:如果接口依赖数据库或外部服务,还要看下游能力,取"集群上限"与"下游上限"的较小值;
  4. 给峰值留缓冲:日常阈值设为上限的 70% 左右,秒杀等场景临时调高而不是长期顶着上限跑。

常见错误:不压测就写死"100 QPS",结果流量一涨就误伤正常用户,或者系统被打穿限流形同虚设。阈值必须能动态调整,并配合监控验证。

三、Redis + Lua 的原子实现

生产环境多实例部署时,单机内存计数会互相独立,必须用 Redis 做集中计数。为了防止"先判断后扣减"之间的竞态,整个限流逻辑必须放进一个 Lua 脚本原子执行

3.1 固定窗口版(Redis + Lua)

代码语言:lua
复制
-- KEYS[1] = 限流 key,如 rate:api:order:20260911
-- ARGV[1] = 窗口秒数,ARGV[2] = 窗口内最大请求数
local key   = KEYS[1]
local window = tonumber(ARGV[1])
local limit  = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')
if current >= limit then
    return 0
end
redis.call('INCR', key)
if current == 0 then
    redis.call('EXPIRE', key, window)
end
return 1

窗口用自然时间对齐:key 里带上时间戳,如 rate:api:order:20260911,每小时一个 key,天然避免跨窗口残留。

3.2 令牌桶版(Redis + Lua)

代码语言:lua
复制
-- KEYS[1] = 令牌桶 key(存 JSON:capacity/tokens/last_refill)
-- ARGV[1] = 容量,ARGV[2] = 每秒补充速率,ARGV[3] = 本次请求数
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local need = tonumber(ARGV[3])
local now = tonumber(redis.call('TIME')[1])

local data = redis.call('GET', key)
local tokens, last = capacity, now
if data then
    local t = cjson.decode(data)
    tokens = t.tokens
    last = t.last
end

tokens = math.min(capacity, tokens + (now - last) * rate)
if tokens < need then
    return 0
end

tokens = tokens - need
redis.call('SET', key, cjson.encode({tokens = tokens, last = now}), 'EX', 3600)
return 1

说明:redis.call('TIME') 取的是 Redis 服务器时间,多实例时钟一致,不依赖应用服务器时钟。

3.3 服务端调用示例(Java + RedisTemplate)

代码语言:javascript
复制
public boolean tryAcquire(String key, int capacity, int rate) {
    String lua = loadScript("token_bucket.lua");
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(lua, Long.class),
        List.of(key),
        String.valueOf(capacity),
        String.valueOf(rate),
        "1"
    );
    return result != null && result == 1L;
}

说明:Lua 脚本加载后可常驻,避免每次请求都发送脚本内容;Go、Python 等其他语言通过各自 Redis 客户端的 EVAL/SCRIPT LOAD 支持同样的原子语义,实现思路完全一致。

四、从单机到分布式的演进清单

  1. 单机版先行:Guava RateLimiter 或自研内存计数,适合单实例、对一致性要求不高的服务。
  2. Redis 集中式:多实例共用一套计数,注意 Lua 脚本保证原子性;Redis 是单线程,限流 QPS 可达每秒十万级,一般够用。
  3. 分层限流:网关层(Nginx/API 网关)做粗粒度限流 + 业务层做细粒度限流(按用户、按 IP、按接口维度)。
  4. 动态阈值:把阈值放进配置中心,根据服务健康度(错误率、RT)动态调整,避免"限流值写死、容量变化后失效"。

五、容易踩的五个坑

  1. 时钟不一致:分布式限流依赖的时钟必须统一,用 Redis TIME 而不是应用本地时间。
  2. key 不设置过期:固定窗口的 key 不带时间戳会越积越大,务必按窗口切 key 并设过期。
  3. 只限了单机:网关限流只对入口生效,业务内部直连调用同样要走限流。
  4. 阈值写死:容量变化(加副本、加索引)后阈值不变,等于没限或误伤。
  5. 限流后无响应策略:被限的请求要区分"直接拒绝(429)"和"排队等待",给客户端明确的退避信号,而不是静默失败。

六、被限流之后:客户端与网关怎么配合

限流不是"挡住就完事",后面还得有配套:

  1. 明确的状态码与提示:被限流返回 429 Too Many Requests 并带 Retry-After 头,客户端据此退避,而不是一律 500 让监控误报;
  2. 客户端指数退避:收到 429 后按"1s、2s、4s"递增重试,最多 N 次,避免退避重试本身变成新的流量放大;
  3. 网关层兜底:入口网关做全局粗粒度限流(按 IP、按路径),业务层做细粒度限流(按用户、按接口),两层各司其职;
  4. 限流命中监控:把"被限次数/总请求"接入监控看板,命中率长期高于 5% 说明阈值偏低或存在刷量,需要人工评估;
  5. 排队替代拒绝:对可等待的请求(如报表导出、批量任务),用队列 + 匀速消费替代直接拒绝,提升体验。

一个容易忽略的点:限流、熔断、降级要联动。限流只挡入口流量,如果依赖的下游已经故障,还要靠熔断快速失败,否则限流放进来的请求依然会拖垮整个调用链。

七、上线自检清单

  • 限流 key 是否按窗口切分并设置过期时间
  • Lua 脚本是否通过 Redis EVAL 在测试环境验证过
  • 是否区分了接口维度与用户维度,避免单用户刷爆全局额度
  • 被限请求的 HTTP 状态码与错误提示是否明确
  • 阈值是否可从配置中心动态调整
  • 是否对限流命中率做了监控(命中率突增说明可能有异常流量)

结语

限流是接口稳定性的第一道闸门,选型不必复杂:没有突发需求用固定窗口,有突发需求用令牌桶,要求精确平滑用滑动窗口。实现上记住一条铁律——判断和扣减必须原子,生产环境用 Redis + Lua 是成本最低的可靠方案。功能与实现细节以官方文档与实时版本为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么要限流:先想清楚限的是谁
  • 二、四种常见算法的原理与选型
    • 2.1 固定窗口计数
    • 2.2 滑动窗口
    • 2.3 漏桶
    • 2.4 令牌桶
    • 2.5 选型对照
    • 2.6 阈值怎么定:先算容量,再定限流值
  • 三、Redis + Lua 的原子实现
    • 3.1 固定窗口版(Redis + Lua)
    • 3.2 令牌桶版(Redis + Lua)
    • 3.3 服务端调用示例(Java + RedisTemplate)
  • 四、从单机到分布式的演进清单
  • 五、容易踩的五个坑
  • 六、被限流之后:客户端与网关怎么配合
  • 七、上线自检清单
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档