Spring Boot 接口限流实战:基于 Guava 令牌桶与 AOP 的实现方案
在单体 Spring Boot 项目中,若需对接口进行流量管控,无需引入网关组件,可借助 Guava 的 RateLimiter 结合 AOP 切面编程快速实现。以下为完整的技术方案。
依赖引入
在 pom.xml 中添加 Guava 与 AOP 支持:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义限流注解
定义 @RateLimit 注解,用于标注需要限流的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流标识,需全局唯一
*/
String resourceKey();
/**
* 每秒生成的令牌数量
*/
double qps();
/**
* 获取令牌的最大等待时长
*/
long waitTime() default 0;
/**
* 等待时长的单位
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
/**
* 触发限流时的提示信息
*/
String fallbackMessage() default "请求过于频繁,请稍后重试";
}
切面逻辑实现
通过 AOP 拦截注解,利用 ConcurrentHashMap 维护各资源对应的限流器实例:
@Slf4j
@Aspect
@Component
public class RateLimitInterceptor {
private final Map<String, RateLimiter> rateLimiterCache = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object intercept(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
String resource = rateLimit.resourceKey();
RateLimiter limiter = rateLimiterCache.computeIfAbsent(resource, k ->
RateLimiter.create(rateLimit.qps())
);
boolean permitted = limiter.tryAcquire(rateLimit.waitTime(), rateLimit.unit());
if (!permitted) {
return buildErrorResponse(rateLimit.fallbackMessage());
}
return point.proceed();
}
private Object buildErrorResponse(String message) {
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
try (PrintWriter writer = response.getWriter()) {
writer.write("{\"error\":\"" + message + "\"}");
} catch (IOException ex) {
log.error("限流响应写入失败", ex);
}
return null;
}
}
接口应用示例
在 Controller 方法上添加注解,实现每 500 毫秒仅允许 1 次请求:
@RestController
@RequestMapping("/api")
public class DataSyncController {
@PostMapping("/fetch")
@RateLimit(resourceKey = "data-fetch", qps = 2.0, waitTime = 500, unit = TimeUnit.MILLISECONDS)
public ResponseEntity<String> fetchExternalData() {
return ResponseEntity.ok("数据处理完成");
}
}
关键注意点
- 资源标识唯一性:
resourceKey需保证全局唯一,相同 key 共享同一限流器 - QPS 计算方式:
RateLimiter.create(2.0)表示每秒生成 2 个令牌,即每 500ms 1 个 - 异常处理:建议将限流响应封装为统一 Result 对象,便于前端处理
- 内存管理:当前实现基于内存缓存,重启后限流器状态丢失,生产环境可考虑 Redis 持久化方案