面对突发流量,如何保护系统不被打垮?限流器是第一道防线!
一、四大限流算法
1. 固定窗口计数器(最简单)
public class FixedWindowRateLimiter {
private final int limit; // 限流阈值
private final AtomicInteger counter = new AtomicInteger(0);
private long startTime = System.currentTimeMillis();
public FixedWindowRateLimiter(int limit) {
this.limit = limit;
}
public boolean tryAcquire() {
long now = System.currentTimeMillis();
// 窗口重置(每秒)
if (now - startTime >= 1000) {
counter.set(0);
startTime = now;
}
// 检查是否超限
return counter.incrementAndGet() <= limit;
}
}
缺点: 临界问题
窗口1(0-1秒):100个请求(限制100)
窗口2(1-2秒):100个请求
但在0.9秒-1.1秒之间有200个请求!超过限制!
2. 滑动窗口(改进)
public class SlidingWindowRateLimiter {
private final int limit;
private final Queue<Long> timestamps = new ConcurrentLinkedQueue<>();
public SlidingWindowRateLimiter(int limit) {
this.limit = limit;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 移除1秒前的记录
while (!timestamps.isEmpty() && now - timestamps.peek() >= 1000) {
timestamps.poll();
}
// 检查是否超限
if (timestamps.size() < limit) {
timestamps.offer(now);
return true;
}
return false;
}
}
3. 令牌桶(Token Bucket)⭐推荐
public class TokenBucketRateLimiter {
private final int capacity; // 桶容量
private final int refillRate; // 每秒补充速率
private int tokens; // 当前令牌数
private long lastRefillTime; // 上次补充时间
public TokenBucketRateLimiter(int capacity, int refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
refill(); // 补充令牌
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long elapsedTime = now - lastRefillTime;
// 计算应该补充的令牌数
int tokensToAdd = (int) (elapsedTime * refillRate / 1000);
if (tokensToAdd > 0) {
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
}
}
特点:
- ✅ 支持突发流量(桶内有令牌就放行)
- ✅ 平均速率控制
4. 漏桶(Leaky Bucket)
public class LeakyBucketRateLimiter {
private final int capacity; // 桶容量
private final int leakRate; // 漏出速率
private int water; // 当前水量
private long lastLeakTime;
public LeakyBucketRateLimiter(int capacity, int leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
this.water = 0;
this.lastLeakTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
leak(); // 漏水
if (water < capacity) {
water++;
return true;
}
return false;
}
private void leak() {
long now = System.currentTimeMillis();
long elapsedTime = now - lastLeakTime;
// 计算漏出的水量
int leaked = (int) (elapsedTime * leakRate / 1000);
if (leaked > 0) {
water = Math.max(0, water - leaked);
lastLeakTime = now;
}
}
}
特点:
- ✅ 固定速率输出(平滑)
- ❌ 不支持突发流量
二、令牌桶 vs 漏桶
| 特性 | 令牌桶 | 漏桶 |
|---|---|---|
| 突发流量 | ✅ 支持 | ❌ 不支持 |
| 输出速率 | 不固定 | 固定 |
| 应用场景 | API限流 | 流量整形 |
| 实现 | Guava RateLimiter | 队列 |
三、Guava RateLimiter(生产级)
// 创建限流器:每秒10个请求
RateLimiter limiter = RateLimiter.create(10);
// 阻塞获取
limiter.acquire(); // 阻塞直到获得令牌
// 尝试获取(非阻塞)
boolean success = limiter.tryAcquire();
// 超时获取
boolean success = limiter.tryAcquire(1, TimeUnit.SECONDS);
// 批量获取
limiter.acquire(5); // 获取5个令牌
四、分布式限流(Redis)
public class RedisRateLimiter {
private final RedisTemplate<String, String> redis;
private final String luaScript =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local current = tonumber(redis.call('get', key) or '0') " +
"if current + 1 > limit then " +
" return 0 " +
"else " +
" redis.call('incr', key) " +
" redis.call('expire', key, 1) " +
" return 1 " +
"end";
public boolean tryAcquire(String key, int limit) {
Long result = redis.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(limit)
);
return result != null && result == 1;
}
}
五、实战:接口限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int value() default 100; // 每秒请求数
}
@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = pjp.getSignature().toString();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.value())
);
if (!limiter.tryAcquire()) {
throw new RuntimeException("请求过于频繁");
}
return pjp.proceed();
}
}
// 使用
@RateLimit(10) // 每秒10个请求
public String api() {
return "success";
}
六、面试高频问答💯
Q: 令牌桶和漏桶的区别?
A:
- 令牌桶:支持突发流量,桶满后多余令牌丢弃
- 漏桶:固定速率输出,超出容量的请求被拒绝
Q: 如何实现分布式限流?
A:
- Redis + Lua脚本(原子性)
- Redis + Sorted Set(滑动窗口)
- Nginx限流模块
Q: 限流器QPS如何设置?
A:
- 压测得出系统最大QPS
- 留20-30%余量
- 根据业务重要性分配
下一篇→ Future.cancel为什么不能真正取消任务?🛑