SpringBoot + 令牌桶 + 滑动窗口:精准限流保护核心接口,突发流量不崩溃

在高并发的互联网应用中,流量控制是一个绕不开的话题。想象一下,当某个热点事件引发流量洪峰时,如果没有有效的限流措施,你的服务器很可能瞬间被击垮,导致服务不可用。今天,我要跟大家分享两种经典的限流算法——令牌桶和滑动窗口,以及如何在SpringBoot中实现它们。

为什么需要限流?

在讲具体实现之前,我们先来看看为什么需要限流:

  1. 保护系统稳定性:防止突发流量压垮系统
  2. 保障服务质量:确保核心功能在高负载下仍能正常服务
  3. 资源合理分配:防止恶意用户占用过多资源
  4. 成本控制:避免不必要的资源消耗

令牌桶算法详解

令牌桶算法就像一个固定容量的桶,系统以恒定速率向桶中添加令牌。每当有请求到来时,需要从桶中取出一个令牌才能继续处理。如果桶中没有令牌,则请求被拒绝。

令牌桶的特点:

  • 平滑突发流量:允许一定程度的突发请求
  • 恒定速率:令牌按固定速率产生
  • 容量限制:桶有最大容量,多余的令牌会被丢弃

令牌桶的实现:

@Service
public class TokenBucketRateLimiter {
    
    // 令牌桶缓存
    private final Cache<String, TokenBucket> tokenBucketCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    
    public synchronized boolean tryAcquire(String key, int limit, int window) {
        try {
            TokenBucket bucket = tokenBucketCache.get(key, () -> new TokenBucket(limit, window));
            return bucket.tryConsume();
        } catch (Exception e) {
            // 发生异常时,默认允许请求通过
            return true;
        }
    }
    
    private static class TokenBucket {
        private final int capacity;           // 桶的容量
        private final int refillRate;        // 令牌填充速率
        private final ReentrantLock lock = new ReentrantLock();
        private volatile int tokens;         // 当前令牌数量
        private volatile long lastRefillTime; // 上次填充时间
        
        public TokenBucket(int capacity, int window) {
            this.capacity = capacity;
            this.refillRate = capacity / Math.max(window, 1);
            this.tokens = capacity; // 初始化时桶满
            this.lastRefillTime = System.currentTimeMillis();
        }
        
        public boolean tryConsume() {
            lock.lock();
            try {
                refillTokens();  // 补充令牌
                
                if (tokens > 0) {
                    tokens--;  // 消费一个令牌
                    return true;
                }
                
                return false;
            } finally {
                lock.unlock();
            }
        }
        
        private void refillTokens() {
            long now = System.currentTimeMillis();
            long timePassed = now - lastRefillTime;
            
            // 计算应该补充的令牌数
            int tokensToAdd = (int) ((timePassed / 1000.0) * refillRate);
            
            if (tokensToAdd > 0) {
                tokens = Math.min(capacity, tokens + tokensToAdd);
                lastRefillTime = now;
            }
        }
    }
}

滑动窗口算法详解

滑动窗口算法通过维护一个固定时间窗口内的请求计数来实现限流。每当有请求到来时,系统会检查当前时间窗口内的请求数是否超过了设定的阈值。

滑动窗口的特点:

  • 精确控制:严格控制时间窗口内的请求数量
  • 防止突发:不允许突发流量
  • 实时统计:动态跟踪请求情况

滑动窗口的实现:

@Service
public class SlidingWindowRateLimiter {
    
    // 滑动窗口缓存
    private final Cache<String, WindowCounter> windowCounterCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    
    public synchronized boolean tryAcquire(String key, int limit, int window) {
        try {
            WindowCounter counter = windowCounterCache.get(key, () -> new WindowCounter(window));
            return counter.tryIncrement(limit);
        } catch (Exception e) {
            // 发生异常时,默认允许请求通过
            return true;
        }
    }
    
    private static class WindowCounter {
        private final int windowSize;              // 窗口大小(毫秒)
        private final AtomicInteger requestCount;  // 请求计数
        private volatile long windowStart;         // 窗口开始时间
        
        public WindowCounter(int windowSizeSeconds) {
            this.windowSize = windowSizeSeconds * 1000;
            this.requestCount = new AtomicInteger(0);
            this.windowStart = System.currentTimeMillis();
        }
        
        public boolean tryIncrement(int maxRequests) {
            long now = System.currentTimeMillis();
            
            // 检查是否需要重置窗口
            if (now - windowStart >= windowSize) {
                synchronized (this) {
                    if (now - windowStart >= windowSize) {
                        requestCount.set(0);
                        windowStart = now;
                    }
                }
            }
            
            // 检查当前窗口内的请求数是否超过限制
            int currentCount = requestCount.get();
            if (currentCount >= maxRequests) {
                return false; // 拒绝请求
            }
            
            // 原子性地增加计数
            return requestCount.incrementAndGet() <= maxRequests;
        }
    }
}

SpringBoot集成实现

为了让限流更加易用,我们可以结合Spring AOP实现注解驱动的限流:

1. 创建限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    
    String key() default "";
    
    Type type() default Type.TOKEN_BUCKET;
    
    int window() default 60;
    
    int limit() default 100;
    
    String message() default "请求过于频繁,请稍后再试";
    
    enum Type {
        TOKEN_BUCKET,   // 令牌桶算法
        SLIDING_WINDOW  // 滑动窗口算法
    }
}

2. 实现限流切面

@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
    
    private final TokenBucketRateLimiter tokenBucketRateLimiter;
    private final SlidingWindowRateLimiter slidingWindowRateLimiter;
    
    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String key = generateKey(joinPoint, rateLimit);
        int limit = rateLimit.limit();
        int window = rateLimit.window();
        
        boolean allowed = false;
        
        switch (rateLimit.type()) {
            case TOKEN_BUCKET:
                allowed = tokenBucketRateLimiter.tryAcquire(key, limit, window);
                break;
            case SLIDING_WINDOW:
                allowed = slidingWindowRateLimiter.tryAcquire(key, limit, window);
                break;
        }
        
        if (!allowed) {
            throw new RuntimeException(rateLimit.message());
        }
        
        return joinPoint.proceed();
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
        if (!rateLimit.key().isEmpty()) {
            return rateLimit.key();
        }
        
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        return className + ":" + methodName;
    }
}

3. 使用示例

@RestController
@RequestMapping("/api")
public class ApiController {
    
    // 使用令牌桶算法,每分钟最多10个请求
    @RateLimit(
        type = RateLimit.Type.TOKEN_BUCKET,
        limit = 10,
        window = 60,
        message = "请求过于频繁,请稍后再试"
    )
    @GetMapping("/data")
    public ResponseEntity<?> getData() {
        return ResponseEntity.ok("数据获取成功");
    }
    
    // 使用滑动窗口算法,每分钟最多5个请求
    @RateLimit(
        type = RateLimit.Type.SLIDING_WINDOW,
        limit = 5,
        window = 60,
        message = "请求过于频繁,请稍后再试"
    )
    @PostMapping("/order")
    public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
        return ResponseEntity.ok("订单创建成功");
    }
}

两种算法对比

特性令牌桶算法滑动窗口算法
突发流量处理支持一定程度的突发严格限制,不支持突发
实现复杂度中等简单
内存占用较低较低
精确性相对宽松非常精确
适用场景API接口、允许突发的场景严格限流、防止攻击

实际应用场景

  1. API接口保护:对核心API接口进行限流,防止恶意调用
  2. 登录接口:限制登录尝试次数,防止暴力破解
  3. 支付接口:控制支付请求频率,防止刷单
  4. 短信发送:限制短信发送频率,防止骚扰
  5. 文件下载:控制下载频率,防止带宽被占满

最佳实践

  1. 合理设置参数:根据业务特点设置合适的限流阈值
  2. 分层限流:在不同层级(网关、服务、接口)实施限流
  3. 监控告警:记录限流统计信息,及时发现异常
  4. 优雅降级:限流时返回友好的错误信息
  5. 分布式限流:在集群环境下使用Redis等中间件实现分布式限流

总结

通过SpringBoot + 令牌桶 + 滑动窗口的组合,我们可以构建出灵活且强大的限流系统。令牌桶算法适合处理突发流量,滑动窗口算法适合精确控制流量。在实际应用中,我们可以根据不同场景选择合适的算法,或者两者结合使用,以达到最佳的限流效果。

记住,限流不是目的,而是手段。我们的目标是在保证服务质量的前提下,最大化系统吞吐量。只有在充分理解业务特点的基础上,才能制定出最合适的限流策略。

希望这篇文章能对你有所帮助,如果你觉得有用,欢迎关注"服务端技术精选",我会持续分享更多实用的技术干货。


标题:SpringBoot + 令牌桶 + 滑动窗口:精准限流保护核心接口,突发流量不崩溃
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/03/1769942525731.html

    0 评论
avatar