悠悠楠杉
Java请求频率限制实战:从单机到分布式的高效流控方案
正文:
在电商大促的凌晨,技术总监老王盯着监控大屏突然脸色发青——核心API的QPS曲线像坐了火箭般垂直飙升。"快!启动流控熔断!" 随着他嘶哑的指令,整个运维团队手忙脚乱地修改Nginx配置。这种被动救火的场景,正是由于缺乏精准的请求频率控制导致的系统过载。今天我们将深入探讨,如何用Java构建自动化的流量防御工事。
一、为什么需要请求频率限制?
当某用户每秒发起1000次登录请求,或是爬虫疯狂抓取商品数据时,系统会陷入灾难性境地。去年某物流公司就因未做API限流,被脚本刷单导致数据库CPU飚至100%,直接损失数百万订单。精准的流控能实现:
- 防止资源枯竭
- 保障VIP用户体验
- 规避恶意攻击
- 平滑流量洪峰
二、单机流控的五种兵器谱
- 粗暴计数器法
最直白的实现,但存在致命的时间窗口漂移问题:
java
public class SimpleCounterLimiter {
private final AtomicInteger counter = new AtomicInteger(0);
private final long intervalNanos;
private final int limit;
private volatile long startTime = System.nanoTime();
public SimpleCounterLimiter(int limit, TimeUnit perUnit) {
this.limit = limit;
this.intervalNanos = perUnit.toNanos(1);
}
public boolean tryAcquire() {
long now = System.nanoTime();
// 时间窗口漂移问题在此暴露
if (now - startTime > intervalNanos) {
startTime = now;
counter.set(0);
}
return counter.incrementAndGet() <= limit;
}
}
- 滑动时间窗口法
通过队列记录每次请求时间戳,解决窗口漂移:
java
public class SlidingWindowLimiter {
private final Queue
private final int maxRequests;
private final long windowMillis;
public SlidingWindowLimiter(int maxRequests, long windowMillis) {
this.maxRequests = maxRequests;
this.windowMillis = windowMillis;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 移除过期请求
while (!timestamps.isEmpty() && now - timestamps.peek() > windowMillis) {
timestamps.poll();
}
if (timestamps.size() < maxRequests) {
timestamps.offer(now);
return true;
}
return false;
}
}
- 漏桶算法(Leaky Bucket)
恒定速率输出的经典模型,适合流量整形:
java
public class LeakyBucketLimiter {
private final long capacity;
private final long leakRate; // 纳秒/次
private long waterLevel;
private long lastLeakTime;
public LeakyBucketLimiter(int capacity, int permitsPerSecond) {
this.capacity = capacity;
this.leakRate = TimeUnit.SECONDS.toNanos(1) / permitsPerSecond;
}
public synchronized boolean tryAcquire() {
long now = System.nanoTime();
// 漏出水量
if (waterLevel > 0) {
long leaked = (now - lastLeakTime) / leakRate;
waterLevel = Math.max(0, waterLevel - leaked);
}
lastLeakTime = now;
if (waterLevel < capacity) {
waterLevel++;
return true;
}
return false;
}
}
- 令牌桶算法(Token Bucket)
Guava的RateLimiter正是此算法的工业级实现:
java
// 使用Guava的优雅实现
public class TokenBucketDemo {
public static void main(String[] args) {
// 每秒生成2个令牌
RateLimiter limiter = RateLimiter.create(2.0);
// 尝试获取令牌(非阻塞)
if (limiter.tryAcquire()) {
processRequest();
} else {
throw new TooManyRequestsException();
}
// 预热模式:系统启动初期逐步提升速率
RateLimiter warmupLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS);
}
}
三、分布式场景下的高并发流控
当服务部署在多个实例时,需要借助外部存储实现集群限流。以下是基于Redis+Lua的原子计数器方案:
lua
-- KEYS[1]: 限流key
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 最大请求数
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- 清除过期数据
redis.call('zremrangebyscore', key, 0, now - window)
-- 获取当前请求数
local current = redis.call('zcard', key)
if current < limit then
-- 添加当前请求
redis.call('zadd', key, now, now)
redis.call('expire', key, window)
return 1
end
return 0
Java调用示例:
java
public class RedisRateLimiter {
private final JedisPool jedisPool;
private final String scriptSha;
public boolean tryAcquire(String key, int limit, int windowSec) {
try (Jedis jedis = jedisPool.getResource()) {
Object result = jedis.evalsha(scriptSha,
1, key,
String.valueOf(System.currentTimeMillis() / 1000),
String.valueOf(windowSec),
String.valueOf(limit));
return "1".equals(result.toString());
}
}
}
四、流量整形的实战经验
在某金融系统项目中,我们通过分级流控实现了关键保障:
1. 用户维度:每个账号每秒不超过5次
2. IP维度:每个IP每分钟不超过100次
3. 业务维度:支付接口集群总QPS≤3000
配置优先级规则:
yaml
flow-control:
rules:
- key: userId
limit: 5
period: 1s
fallback: wait # 等待模式
- key: ip
limit: 100
period: 1m
fallback: reject # 直接拒绝
五、终极方案选型指南
- 中小型系统:Guava RateLimiter(单机)/ Redis计数器(集群)
- 高精度要求:滑动时间窗口算法
- 需要平滑突发:令牌桶算法(支持突发)
- 严格匀速输出:漏桶算法
最后提醒:在网关层(如Spring Cloud Gateway)做全局限流才是治本之道。某跨境电商在接入网关级流控后,API稳定性从99.3%提升至99.98%,运维告警减少了72%——这或许就是技术架构的艺术价值所在。
