从零构建高可用系统的流量控制实战指南
目录导读
- 为什么需要自定义限流 — 剖析默认限流器的不足
- 核心算法选型 — 计数器、滑动窗口、令牌桶与漏桶的取舍
- 自定义限流案例实战 — 基于Redis+Lua的分布式限流实现
- 动态限流规则 — 热更新与多维度策略
- 限流与熔断、降级的协同 — 构建完整防护体系
- 常见问题与解答(FAQ) — 避坑指南
为什么需要自定义限流
在微服务架构中,限流是保护系统免于过载的第一道防线,虽然Spring Cloud Gateway、Sentinel等框架提供了内置限流器,但在真实场景中,默认实现往往面临三大痛点:规则不可动态调整(需重启)、无法按业务维度区分(如VIP用户与普通用户)、与自研监控体系割裂,自定义限流成为高并发系统的必备技能。

核心算法选型
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口计数器 | 时间窗内计数 | 实现简单 | 临界突发流量 | 非核心接口 |
| 滑动窗口日志 | 记录时间戳数组 | 精准 | 内存占用高 | 大规模集群不适用 |
| 令牌桶 | 匀速放令牌 | 允许突发流量 | 需维护令牌生成器 | 推荐用于业务接口 |
| 漏桶 | 匀速流出 | 强限流效果 | 无法应对突发 | 流量整形场景 |
实战建议:优先选择令牌桶算法,它天然允许短时突发(如秒杀开启的瞬间),同时保护下游系统。
自定义限流案例实战 — Redis+Lua实现分布式限流
场景定义
某电商平台需要针对 /order/submit 接口实现每个用户ID每分钟最多10次请求,同时全局限流1000 QPS。
核心代码逻辑(Lua脚本原子性保证)
-- 用户级限流键
local user_key = KEYS[1] .. ARGV[1]
local global_key = KEYS[2]
-- 令牌桶参数(用hash存储当前令牌数+最后更新时间)
local tokens_key = user_key .. ":tokens"
local timestamp_key = user_key .. ":ts"
local rate = tonumber(ARGV[2]) -- 填充速率(每秒1个)
local capacity = tonumber(ARGV[3]) -- 桶容量(10个)
local now = tonumber(ARGV[4])
local last_tokens = tonumber(redis.call('get', tokens_key) or capacity)
local last_refreshed = tonumber(redis.call('get', timestamp_key) or now)
local delta = math.max(0, (now - last_refreshed) / 1000)
local current_tokens = math.min(capacity, last_tokens + delta * rate)
local allowed = 0
if current_tokens >= 1 then
current_tokens = current_tokens - 1
allowed = 1
end
redis.call('set', tokens_key, current_tokens)
redis.call('set', timestamp_key, now)
redis.call('expire', tokens_key, 60) -- 设置过期
-- 全局限流(使用滑动窗口计数器)
local global_count = redis.call('incr', global_key)
if global_count == 1 then
redis.call('expire', global_key, 1)
end
if global_count > tonumber(ARGV[5]) then
allowed = 0
end
return allowed
关键设计点说明
- 原子性:通过Lua脚本将判断、扣减、过期逻辑打包执行,规避并发竞争。
- 两级限流:用户级用令牌桶(容忍小突发),全局用计数器(严格限制整体流量)。
- 内存优化:每个用户键设置TTL,防止Redis内存膨胀。
动态限流规则
固定写死的限流值在流量突增时仍需人工干预,自定义限流的优势在于:
- 配置中心化:将限流阈值存放在Nacos/Etcd,监听变更并重新加载Lua脚本参数。
- 多维度组合:IP+用户ID+设备指纹进行联合限流。
- 自动降级机制:当监控系统发现错误率>5%时,自动将限流阈值从100%调整至80%。
限流与熔断、降级的协同
限流器拒绝请求后,不能简单返回错误,而应:
- 快速失败:返回HTTP 429 + 可重试时间提醒。
- 触发降级:将请求转发至兜底缓存服务,或返回默认预热数据。
- 记录失败样本:为熔断器(如Resilience4j)提供输入,当限流触发频率超过阈值时打开熔断。
常见问题与解答(FAQ)
Q1:Redis单点故障会导致整个系统不可用,如何解决? A:使用Redis Cluster,并在Lua脚本中采用“无锁读+最终一致性”,更稳健的做法是本地内存限流(如Guava RateLimiter)+Redis分布式限流形成双层防护,本地层失败时自动降级为全局限流。
Q2:滑窗算法与令牌桶在真实场景中,哪个更容易出现“误杀”? A:令牌桶更容易出现“漏网之鱼”而非误杀,令牌桶允许桶内保留10个预存令牌,短时突发会被放行,若业务要求零突刺(如支付回调),建议改用漏桶或滑动窗口。
Q3:自定义限流与原生网关限流,如何选择? A:原生网关(如Kong)适合入口流量控制;自定义限流适合业务代码内部逻辑(如某用户创建订单),两者可叠加:网关粗粒度限制总数,业务代码细粒度限制用户级。
Q4:如何压测验证自定义限流的准确性? A:使用Jmeter并发200线程循环请求,观察被拦截请求数为“总请求数-限流上限”,重点测试临界时间窗口(如第59秒与第61秒的请求),确认无边界溢出。