限流是后端开发中保护服务的最后一道防线。接口被爬虫扫、营销活动流量洪峰、下游服务变慢导致线程池打满,这些场景下没有限流的接口会直接拖垮整个应用。API接口限流的核心是把流量控制在系统容量之内,本文给出从单机令牌桶到分布式限流的完整落地方案。
限流算法选型:固定窗口、滑动窗口与令牌桶
三种主流算法各有适用场景。固定窗口实现最简单,按分钟计数,临界点有突刺问题——窗口结束前1秒和下个窗口开始后1秒可以打进2倍流量;滑动窗口把大窗口切成小格子滚动统计,精度高但内存开销大;令牌桶以恒定速率往桶里放令牌,请求拿到令牌才放行,允许一定突发流量,是业务接口最均衡的选择。
选型结论:网关层全局防护用滑动窗口(配合Nginx limit_req);业务接口用令牌桶(Guava RateLimiter或Redis实现);削峰场景如秒杀下单用漏桶思想排队。单机令牌桶用Guava最快落地:
import com.google.common.util.concurrent.RateLimiter;
public class OrderService {
// 每秒放行200个请求,突发容量约1秒量
private final RateLimiter limiter = RateLimiter.create(200.0);
public Result createOrder(OrderRequest req) {
// tryTimeout超时直接拒绝,不让线程排队堆积
if (!limiter.tryAcquire(50, TimeUnit.MILLISECONDS)) {
return Result.fail(429, "请求过于频繁,请稍后重试");
}
return doCreate(req);
}
}
单机限流的问题:集群N台机器,流量按权重分摊后,单机限流阈值无法保证集群总量。实例扩缩容时限流阈值要跟着改,配置漂移极易出错。
分布式限流:Redis+Lua保证原子性
集群级限流把计数器放到Redis,用Lua脚本保证”读取-判断-扣减”原子执行,避免并发超卖。令牌桶的Redis实现核心是惰性补偿:不依赖定时任务真实放令牌,而是在每次请求时按时间差补算应发令牌数。
-- token_bucket.lua:KEYS[1]=限流键 ARGV=rate,capacity,now,requested
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 bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now
-- 按时间差补偿令牌(惰性计算,无需定时任务)
local delta = math.max(0, (now - last_time) / 1000.0)
tokens = math.min(capacity, tokens + delta * rate)
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'last_time', now)
redis.call('PEXPIRE', key, math.ceil(capacity / rate * 1000) + 1000)
return allowed
Java侧封装调用,注意Lua脚本的KEYS/ARGV顺序不可颠倒:
@Component
public class RedisRateLimiter {
private static final DefaultRedisScript<Long> SCRIPT;
static {
SCRIPT = new DefaultRedisScript<>();
SCRIPT.setLocation(new ClassPathResource("lua/token_bucket.lua"));
SCRIPT.setResultType(Long.class);
}
@Resource private StringRedisTemplate redisTemplate;
public boolean tryAcquire(String key, int rate, int capacity, int requested) {
Long r = redisTemplate.execute(SCRIPT,
Collections.singletonList(key),
String.valueOf(rate), String.valueOf(capacity),
String.valueOf(System.currentTimeMillis()),
String.valueOf(requested));
return r != null && r == 1L;
}
}
限流层级设计与阈值计算方法
生产环境限流不是单点配置,而是分层防护体系:
1. 接入层(Nginx/网关):按IP限流,挡住单源异常流量,limit_req_zone配置每秒10个请求的突发桶;
2. 服务层(应用内+Redis):按接口+用户维度限流,阈值按压测容量除以安全系数0.7计算,压测单接口承载800QPS则限流阈值设560;
3. 依赖层(下游保护):调用第三方接口按对方配额限流,防止把下游打挂后连锁故障。
阈值计算的常见误区是拿CPU使用率当容量指标,正确的容量基线是压测出的”错误率拐点”——QPS上升过程中错误率从0.01%跳到1%的位置就是系统真实容量,留30%余量作为限流线。
被限流请求的处理策略
拒绝不是唯一选项。按业务重要性分级处理:
1. 直接拒绝返回429,响应头带Retry-After告知重试时间,适用普通查询接口;
2. 排队等待(漏桶),适用写操作,队列长度要有限防止内存溢出;
3. 降级兜底,读接口返回缓存快照或默认数据,牺牲新鲜度保可用性。
@GetMapping("/hot/products")
public Result listHotProducts() {
if (!userLimiter.tryAcquire("hot:list:" + currentUserId(), 5, 10, 1)) {
// 限流后走本地缓存兜底,不返回错误页
return Result.ok(localCache.getSnapshot("hot_products"));
}
return Result.ok(productService.listHot());
}
监控上把限流触发次数接入Prometheus,counter按接口维度打标,触发速率突增说明要么有异常流量、要么容量需要扩容,两个方向都要有人跟进。
常见踩坑与验证清单
1. Redis限流的热键问题:全部流量打到一个分片,用key加哈希槽分散或客户端本地预扣减+异步同步;
2. 时钟回拨导致令牌补偿异常,Lua内用Redis的TIME命令取时间而非业务服务器传参;
3. 限流粒度遗漏:只按用户限流,攻击者换一批账号照样打穿,关键接口叠加IP维度。
上线验证三条:单机压测确认阈值生效、Redis故障时限流器fail-open或fail-close的演练、限流返回的错误码与前端重试逻辑对齐。限流不是配置完就结束的功能,容量随业务变化,每季度复查一次阈值是必要的运维动作。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/api-jie-kou-xian-liu-shi-zhan-ling-pai-tong-suan-fa-shi/