手写限流器:令牌桶与漏桶算法🚰

52 阅读3分钟

面对突发流量,如何保护系统不被打垮?限流器是第一道防线!

一、四大限流算法

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为什么不能真正取消任务?🛑