SpringBoot + 令牌桶 + 滑动窗口:精准限流保护核心接口,突发流量不崩溃
在高并发的互联网应用中,流量控制是一个绕不开的话题。想象一下,当某个热点事件引发流量洪峰时,如果没有有效的限流措施,你的服务器很可能瞬间被击垮,导致服务不可用。今天,我要跟大家分享两种经典的限流算法——令牌桶和滑动窗口,以及如何在SpringBoot中实现它们。
为什么需要限流?
在讲具体实现之前,我们先来看看为什么需要限流:
- 保护系统稳定性:防止突发流量压垮系统
- 保障服务质量:确保核心功能在高负载下仍能正常服务
- 资源合理分配:防止恶意用户占用过多资源
- 成本控制:避免不必要的资源消耗
令牌桶算法详解
令牌桶算法就像一个固定容量的桶,系统以恒定速率向桶中添加令牌。每当有请求到来时,需要从桶中取出一个令牌才能继续处理。如果桶中没有令牌,则请求被拒绝。
令牌桶的特点:
- 平滑突发流量:允许一定程度的突发请求
- 恒定速率:令牌按固定速率产生
- 容量限制:桶有最大容量,多余的令牌会被丢弃
令牌桶的实现:
@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接口、允许突发的场景 | 严格限流、防止攻击 |
实际应用场景
- API接口保护:对核心API接口进行限流,防止恶意调用
- 登录接口:限制登录尝试次数,防止暴力破解
- 支付接口:控制支付请求频率,防止刷单
- 短信发送:限制短信发送频率,防止骚扰
- 文件下载:控制下载频率,防止带宽被占满
最佳实践
- 合理设置参数:根据业务特点设置合适的限流阈值
- 分层限流:在不同层级(网关、服务、接口)实施限流
- 监控告警:记录限流统计信息,及时发现异常
- 优雅降级:限流时返回友好的错误信息
- 分布式限流:在集群环境下使用Redis等中间件实现分布式限流
总结
通过SpringBoot + 令牌桶 + 滑动窗口的组合,我们可以构建出灵活且强大的限流系统。令牌桶算法适合处理突发流量,滑动窗口算法适合精确控制流量。在实际应用中,我们可以根据不同场景选择合适的算法,或者两者结合使用,以达到最佳的限流效果。
记住,限流不是目的,而是手段。我们的目标是在保证服务质量的前提下,最大化系统吞吐量。只有在充分理解业务特点的基础上,才能制定出最合适的限流策略。
希望这篇文章能对你有所帮助,如果你觉得有用,欢迎关注"服务端技术精选",我会持续分享更多实用的技术干货。
标题:SpringBoot + 令牌桶 + 滑动窗口:精准限流保护核心接口,突发流量不崩溃
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/03/1769942525731.html
0 评论