背景
项目中已有基于 @RateLimit 注解的方法级限流,通过 Redis Lua 滑动窗口实现。但它存在两个问题:
- 入口即 Controller:限流在 AOP 切面触发,恶意请求已经走完了 Filter Chain、Security 上下文构建等流程
- 不支持突发流量:滑动窗口是严格的计数限流,超了就拒,无法应对合理突发
因此引入网关层令牌桶作为第一道防线,与方法层滑动窗口形成二级分层限流。
架构设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 请求进入 │ ▼ ┌─────────────────────────────────────────┐ │ ① TokenBucketInterceptor (网关层) │ Bucket4j 分布式令牌桶 │ 维度:IP + URI │ 粗粒度防刷,支持突发 │ 存储:Redis (Redisson CAS) │ │ 顺序:order(0) 最先执行 │ └───────────────┬─────────────────────────┘ │ 放行 ▼ ┌─────────────────────────────────────────┐ │ ② RateLimitAspect (方法层) │ Redis Lua 滑动窗口 │ 维度:GLOBAL / IP / USER │ 细粒度业务限流 │ 注解:@RateLimit │ └───────────────┬─────────────────────────┘ │ 放行 ▼ Controller
|
为什么是令牌桶而非漏桶?
| 算法 |
突发流量 |
平滑性 |
适用场景 |
| 计数器(滑动窗口) |
❌ 不友好 |
中 |
接口级精确限流 |
| 漏桶 |
❌ 严格平滑 |
高 |
网络流量整形 |
| 令牌桶 |
✅ 桶容量内允许突发 |
高 |
网关限流 |
令牌桶可以设置 capacity=30, refill=20/min,意味着稳态每秒约 0.33 个请求,但短时间内可以突发 30 个请求——符合真实用户行为。
技术选型
| 组件 |
版本 |
作用 |
| Bucket4j |
8.10.1 |
令牌桶算法核心库 |
| bucket4j-redis |
8.10.1 |
Redis 分布式代理 |
| Redisson |
3.29.0 |
Redis 客户端,提供 CAS 操作 |
| Caffeine |
Spring Boot 内置 |
本地缓存桶代理引用 |
关键依赖:
1 2 3 4 5
| <dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-redis</artifactId> <version>8.10.1</version> </dependency>
|
注意:Bucket4j 8.x 的 groupId 已从 com.github.vladimir-bukhtoyarov 迁移至 com.bucket4j。
核心实现
1. 配置属性类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Data @Component @ConfigurationProperties(prefix = "rate-limit") public class BucketConfig {
private Map<String, Rule> rules = new HashMap<>();
private Rule anonymousDefault = new Rule(30, 20);
private Rule authenticatedDefault = new Rule(60, 40);
@Data public static class Rule { @Min(1) private int capacity; @Min(1) private long refillPerMinute; } }
|
配置文件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| rate-limit: anonymous-default: capacity: 30 refill-per-minute: 20 authenticated-default: capacity: 60 refill-per-minute: 40 rules: /api/homework/submit: capacity: 5 refill-per-minute: 3 /api/dashboard/: capacity: 60 refill-per-minute: 30 /api/rag/: capacity: 30 refill-per-minute: 15
|
2. 分布式代理管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Configuration public class Bucket4jRedisConfig {
@Bean public ProxyManager<String> bucketProxyManager( CommandAsyncExecutor commandAsyncExecutor) {
return RedissonBasedProxyManager .<String>builderFor(commandAsyncExecutor) .withExpirationStrategy( ExpirationAfterWriteStrategy .basedOnTimeForRefillingBucketUpToMax( Duration.ofMinutes(60))) .build(); }
@Bean("bucketLocalCache") public Cache<String, Bucket> bucketLocalCache() { return Caffeine.newBuilder() .maximumSize(10_000) .expireAfterAccess(Duration.ofMinutes(10)) .recordStats() .build(); } }
|
这里有两个 Bean:
- ProxyManager:Bucket4j 的 Redis 代理,底层通过 Redisson CAS 实现原子性;桶状态持久化在 Redis,多实例共享
- Caffeine Cache:本地缓存桶代理引用,避免每次请求都重新构建代理对象,但令牌状态仍在 Redis 端维护
3. 限流器核心组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Component public class DistributedRateLimiter {
private final ProxyManager<String> proxyManager; private final Cache<String, Bucket> localCache;
public boolean tryConsume(String key, int capacity, long refillPerMinute) { Bucket bucket = resolveBucket(key, capacity, refillPerMinute); return bucket.tryConsume(1); }
private Bucket resolveBucket(String key, int capacity, long refillPerMinute) { String cacheKey = key + "|" + capacity + "|" + refillPerMinute; return localCache.get(cacheKey, k -> buildDistributedBucket(key, capacity, refillPerMinute)); }
private Bucket buildDistributedBucket(String key, int capacity, long refillPerMinute) { Refill refill = Refill.greedy(refillPerMinute, Duration.ofMinutes(1)); Bandwidth limit = Bandwidth.classic(capacity, refill); BucketConfiguration configuration = BucketConfiguration.builder() .addLimit(limit) .build();
return proxyManager.builder() .build(key, configuration); } }
|
关键设计:
- Refill.greedy:令牌连续平滑补充,而非
intervally 的梯级补充,更符合真实流量模型
- Cache Key 编码参数:当配置热更新时(capacity/refillRate 变化),自动重建桶
- ProxyManager.builder().build(key, config):返回
BucketProxy,其 tryConsume 通过 Redis Lua 脚本原子执行
4. 网关拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Slf4j @RequiredArgsConstructor public class TokenBucketInterceptor implements HandlerInterceptor {
private final DistributedRateLimiter rateLimiter; private final BucketConfig bucketConfig; private final ObjectMapper objectMapper;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String clientIp = getClientIp(request); String uri = request.getRequestURI(); String bucketKey = "bucket:{" + clientIp + "}:" + uri;
BucketConfig.Rule rule = resolveRule(uri, request);
if (rateLimiter.tryConsume(bucketKey, rule.getCapacity(), rule.getRefillPerMinute())) { return true; }
response.setStatus(429); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(Map.of( "code", 429, "message", "请求过于频繁,请稍后重试" ))); return false; } }
|
规则匹配策略:
- 精确匹配
/api/homework/submit
- 前缀匹配
/api/dashboard/ → 匹配 /api/dashboard/metrics
- 默认回退 根据是否认证选择
anonymousDefault / authenticatedDefault
5. 注册拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Configuration public class WebMvcConfig implements WebMvcConfigurer {
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenBucketInterceptor) .addPathPatterns("/api/**") .excludePathPatterns( "/api/auth/**", "/actuator/**", "/error" ) .order(0); } }
|
6. Redis 配置调整
原有 RedisConfig 需额外暴露 CommandAsyncExecutor Bean(Bucket4j 8.x 的 Redisson 集成需要):
1 2 3 4 5 6
| private Redisson redissonInstance;
@Bean public CommandAsyncExecutor commandAsyncExecutor() { return redissonInstance.getCommandExecutor(); }
|
Redis Key 设计
网关层令牌桶(Bucket4j)
1 2
| bucket:{192.168.1.100}:/api/homework/submit bucket:{192.168.1.100}:/api/dashboard/metrics
|
{ip} 包裹保证同一 IP 的 key 落在相同 Redis hash slot,兼容 Cluster 模式
- 桶状态包含:当前令牌数、最后补充时间、配置信息
- 超期 60 分钟未访问自动清理
方法层滑动窗口(保持不动)
1 2 3
| ratelimit:{ClassName:methodName}:global ratelimit:{ClassName:methodName}:ip:192.168.1.100 ratelimit:{ClassName:methodName}:user:12345
|
错误码体系
| 层级 |
HTTP 状态码 |
响应格式 |
| 网关层 |
429 Too Many Requests |
{"code":429,"message":"请求过于频繁,请稍后重试"} |
| 方法层 |
429 Too Many Requests |
RateLimitExceededException → GlobalExceptionHandler |
两层都返回 429,前端可以统一处理。通过日志区分触发层级。
监控集成建议
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/actuator") public class RateLimitMetricsEndpoint {
@GetMapping("/ratelimit/{uri}") public Map<String, Object> getMetrics(@PathVariable String uri, HttpServletRequest request) { long tokens = rateLimiter.getAvailableTokens( "bucket:{" + request.getRemoteAddr() + "}:" + uri, 30, 20); return Map.of("availableTokens", tokens); } }
|
配合 Prometheus + Grafana 可展示:
- 各接口限流触发次数
- 桶令牌余量趋势
- Caffeine 缓存命中率(
recordStats() 已开启)
总结
| 维度 |
改动前 |
改动后 |
| 限流层级 |
仅方法层 |
网关层 + 方法层 双层 |
| 算法 |
滑动窗口(一种) |
令牌桶 + 滑动窗口(两种) |
| 突发流量 |
不支持 |
桶容量内支持 |
| 分布式 |
Redis Lua(单机也可) |
Bucket4j + Redisson CAS |
| 配置灵活性 |
注解硬编码 |
配置文件 + 注解 |
| 多实例 |
已支持 |
已支持 |
面试时一句话表述:
“构建了网关级令牌桶 + 方法级滑动窗口的分层限流体系,网关层基于 Bucket4j + Redisson CAS 实现分布式令牌桶,支持按接口路径差异化配置和突发流量;方法层基于 Redis Lua 滑动窗口实现全局/IP/用户多维度精确限流。两层均兼容 Redis Cluster 模式。”
参考