从原理到实战的全方位指南
目录导读
- 为什么需要限流?——高频访问的危害与场景分析
- 限流的核心算法:漏桶、令牌桶与滑动窗口
- 分布式环境下的限流挑战与解决方案
- 实战利器:Redis + Lua 实现高并发限流
- 常见限流误区与避坑指南
- 限流策略的演进:从静态到自适应
- 高频问答:5个你一定会问的限流问题
为什么需要限流?——高频访问的危害与场景分析
定义: 限流(Rate Limiting)是指控制单位时间内系统处理的请求数量,防止突发流量冲垮后端服务。

高频访问的典型危害
- 资源耗尽: 数据库连接池、线程池、内存被瞬间填满,导致服务器雪崩
- 用户体验下降: API 响应时间从 50ms 飙升到 5s,用户直接流失
- 经济损失: 电商平台的秒杀场景下,未限流会导致优惠券被脚本批量刷走
- 安全风险: 暴力破解密码、爬虫攻击、DDoS 攻击等均依赖高频请求
需要限流的常见场景
- API 网关: 控制外部调用频率,防止滥用
- 数据库读写: 避免慢查询击穿缓存,导致数据库崩溃
- 定时任务: 防止任务重复执行或批量执行时拖垮系统
- 第三方服务调用: 遵守服务商对 API 的调用频率限制(如 Twitter API 每分钟 300 次)
实战案例: 某金融系统未对用户登录接口限流,导致脚本以每秒 1000 次的速度尝试弱密码,20 分钟内数据库连接池耗尽,全站瘫痪。
限流的核心算法:漏桶、令牌桶与滑动窗口
1 漏桶算法(Leaky Bucket)
原理: 请求像水滴一样进入漏桶,桶以固定速率往下漏水(处理请求),桶满时拒绝新请求。
特点: 强制平滑流量,无法应对突发请求。
适用场景: 需要严格限制请求速率的场景(如视频流、日志写入)。
2 令牌桶算法(Token Bucket)
原理: 以固定速率向桶中放入令牌,请求需要拿到令牌才能执行,桶的容量决定了突发能力。
特点: 允许一定程度的突发流量(桶足够大时),但长期速率可控。
适用场景: 多数互联网业务的首选(如接口限流、用户行为限制)。
3 滑动窗口算法(Sliding Window)
原理: 将时间划分为小格子,记录每个格子中的请求数,滑动统计过去 N 秒内的总量。
特点: 精度高于固定窗口(计数器),能有效避免“窗口跳跃”问题。
适用场景: 需要精细控制的时间窗口限流(如直播弹幕、秒杀抢购)。
算法对比表
| 算法 | 突发支持 | 平滑性 | 实现复杂度 | 推荐场景 |
|---|---|---|---|---|
| 漏桶 | 不支持 | 最高 | 低 | 写入类限流 |
| 令牌桶 | 支持 | 中 | 中 | 通用 REST API |
| 滑动窗口 | 支持有限 | 中 | 高 | 秒杀/抢购 |
分布式环境下的限流挑战与解决方案
1 三大核心挑战
- 原子性问题: 多节点同时修改限流计数器,会导致计数不准确
- 一致性难题: 节点间限流阈值如何同步?
- 性能瓶颈: 限流逻辑如果过于复杂,会成为系统新瓶颈
2 通用解决方案
方案A:基于 Redis 的集中式限流
- 使用 Redis 的 INCR + EXPIRE 命令实现计数器
- 结合 Lua 脚本 保证操作的原子性
- 缺点:Redis 本身可能成为单点故障
方案B:基于 Nginx + Lua 的网关限流
- 在 Nginx 层使用
lua-resty-limit-traffic模块 - 支持全局限流与服务级限流
- 避免侵入业务代码,运维成本低
方案C:使用成熟限流中间件
- Sentinel(阿里): 支持限流、熔断、系统自适应,对接 Dubbo/Spring Cloud
- Hystrix(Netflix): 已停更维护,但仍可用于老系统
- Resilience4j(轻量): 适合非 Spring 生态的 Java 项目
选型建议: 中小团队推荐“Redis + Lua”方案;大型分布式系统建议直接使用 Sentinel 或企业级 API 网关(如 Kong、APISIX)。
实战利器:Redis + Lua 实现高并发限流
代码示例(基于令牌桶的限流脚本)
-- 限流脚本:获取令牌
local key = KEYS[1] -- 限流资源 key
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 令牌放入速率(每秒)
local now = tonumber(ARGV[3]) -- 当前时间戳
-- 1. 获取当前桶内的令牌数与最后更新时间
local bucket = redis.call("HMGET", key, "tokens", "last_time")
local current_tokens = tonumber(bucket[1] or capacity)
local last_time = tonumber(bucket[2] or now)
-- 2. 计算需要补充的令牌数
local elapsed = math.max(0, now - last_time)
local add_tokens = math.floor(elapsed * rate)
current_tokens = math.min(capacity, current_tokens + add_tokens)
-- 3. 判断是否允许请求
if current_tokens > 0 then
redis.call("HSET", key, "tokens", current_tokens - 1, "last_time", now)
return 1 -- 允许
else
return 0 -- 拒绝
end
调用示例(Java 代码)
// 使用 Jedis 调用
Jedis jedis = new Jedis("localhost", 6379);
String luaScript = "上面写的 Lua 脚本";
String sha = jedis.scriptLoad(luaScript);
Long result = (Long) jedis.evalsha(sha,
Arrays.asList("limit:api:user_123"),
Arrays.asList("10", "2", String.valueOf(System.currentTimeMillis())));
if (result == 1L) {
// 处理请求
} else {
// 返回 429 Too Many Requests
}
性能测试: 单机 Redis 可支撑每秒 8-10 万次限流判断,足以应对大部分业务场景。
常见限流误区与避坑指南
| 误区 | 错误表现 | 正确做法 |
|---|---|---|
| 限流阈值设得太死 | 正常用户也被频繁限制 | 叠加“熔断+降级”,允许临时超限 |
| 只对接口限流,忽视服务间调用 | 内部 RPC 调用导致级联故障 | 全链路限流,包括数据库、MQ |
| 忽略预热 | 系统刚启动时就被限流 | 采用“冷启动”或“慢启动”策略(如 QPS 从 10 逐步爬升到 100) |
| 限流日志不记录 | 排查问题时无从下手 | 输出结构化日志(时间、用户、资源名、拒绝原因) |
| 分布式限流用了强一致性 | 性能急剧下降 | 使用最终一致性,允许少量误差(如 5% 以内) |
限流策略的演进:从静态到自适应
第一阶段:静态限流
- 固定值(如 QPS = 1000),运维手动修改
- 缺点:无法应对动态流量变化
第二阶段:自适应限流
- 根据系统水位(CPU、内存、RT 延迟)动态调整阈值
- 常用指标:TCP backlog 长度、线程池队列大小、平均响应时间
- 实现示例:
if (systemLoadAverage > 0.8) { limits[api] *= 0.9; // 降 10% }
第三阶段:全自动弹性限流
- 结合 AI 预测模型,提前预判流量峰值
- UCloud 的“弹性 QoS”,在秒杀前 30 秒自动调高限流阈值
趋势观察: 2025 年主流云厂商已提供“自适应限流”服务,用户只需定义“最大容忍延迟”,系统自动计算最优阈值。
高频问答:5个你一定会问的限流问题
Q1:限流和熔断有什么区别?
A: 限流是“预防”,防止流量过大;熔断是“止损”,当服务不可用时快速断开,通常组合使用:先限流,限流无效则熔断,熔断后触发降级。
Q2:滑动窗口的精度如何控制?
A: 格子越小精度越高,但内存消耗越大,秒杀场景建议 100ms 一个格子,日常业务 1s 一个格子即可。
Q3:对用户限流好还是对 IP 限流好?
A: 推荐双维度:用户限流防止刷接口,IP 限流防止爬虫,注意匿名用户无法识别时,降级为 IP 限流。
Q4:被限流的请求应该返回什么状态码?
A: 官方标准为 429 Too Many Requests(HTTP 429),并建议在 Retry-After 头部告诉客户端多久后重试。
Q5:限流后用户体验怎么保障?
A: 主动降级展示:
- 显示“系统繁忙,请稍后重试”
- 提供排队机制(如“前方排队 123 人”)
- 限流但不拒绝:将请求写入消息队列异步处理
构建你的限流体系
限流不是简单的“加一个计数器”,而是一套系统工程,建议按以下步骤实施:
- 评估业务流量峰值,设定基线阈值
- 选择匹配的算法(令牌桶优先)
- 在网关层部署限流,减少业务代码侵入
- 叠加自适应策略,应对突发流量
- 建立监控告警,实时观察限流命中率
最终目标:让系统始终在安全水位运行,即使面对 10 倍流量冲击也能从容应对。