一篇讲透:Spring Boot + Redisson + 注解 + AOP 实现接口限流(可直接落地)
前言
最近在做 AI 对话接口时,最先踩的坑不是模型能力,而是请求洪峰。
尤其是流式接口,如果不控流,后端线程、Redis、模型调用链都会被拖垮。
这篇文章我直接给出一套我在线上可用的限流方案,包含:
- 配置(Redis / Redisson)
- 注解定义(
@RateLimit) - 切面定义(AOP 拦截 + 分布式令牌桶)
目标是:复制过去就能跑,改几处参数就能用。
结论先行
- 单机
Guava RateLimiter不够,分布式场景建议直接上Redisson RRateLimiter。 - 限流一定要做成注解 + 切面,避免业务代码里到处写重复判断。
- key 设计是核心:至少支持
API、USER、IP三种维度。 - 令牌桶参数要按接口“价值”区分,别全局一个值打天下。
目录
- 依赖和配置
- 限流注解定义
- 限流类型枚举
- AOP 切面实现
- Controller 使用方式
- 常见问题和优化建议
- 总结
1. 依赖和配置
1.1 Maven 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
1.2 application.yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
1.3 Redisson 配置类
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host:localhost}")
private String redisHost;
@Value("${spring.data.redis.port:6379}")
private Integer redisPort;
@Value("${spring.data.redis.password:}")
private String redisPassword;
@Value("${spring.data.redis.database:0}")
private Integer redisDatabase;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + redisHost + ":" + redisPort;
SingleServerConfig singleServerConfig = config.useSingleServer()
.setAddress(address)
.setDatabase(redisDatabase)
.setConnectionMinimumIdleSize(1)
.setConnectionPoolSize(10)
.setIdleConnectionTimeout(30000)
.setConnectTimeout(5000)
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
if (redisPassword != null && !redisPassword.isEmpty()) {
singleServerConfig.setPassword(redisPassword);
}
return Redisson.create(config);
}
}
2. 限流注解定义
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
int rate() default 10;
int rateInterval() default 1;
RateLimitType limitType() default RateLimitType.USER;
String message() default "请求过于频繁,请稍后再试";
}
参数含义:
rate:窗口内允许的请求数rateInterval:窗口大小(秒)limitType:按 API、用户、IP 限流key:业务自定义前缀(可选)message:触发限流后返回文案
3. 限流类型枚举
public enum RateLimitType {
API,
USER,
IP
}
4. AOP 切面定义(核心)
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Resource
private RedissonClient redissonClient;
@Resource
private UserService userService;
@Before("@annotation(rateLimit)")
public void doBefore(JoinPoint point, RateLimit rateLimit) {
String key = generateRateLimitKey(point, rateLimit);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.expire(Duration.ofHours(1));
rateLimiter.trySetRate(
RateType.OVERALL,
rateLimit.rate(),
rateLimit.rateInterval(),
RateIntervalUnit.SECONDS
);
if (!rateLimiter.tryAcquire(1)) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST, rateLimit.message());
}
}
private String generateRateLimitKey(JoinPoint point, RateLimit rateLimit) {
StringBuilder keyBuilder = new StringBuilder("rate_limit:");
if (!rateLimit.key().isEmpty()) {
keyBuilder.append(rateLimit.key()).append(":");
}
if (rateLimit.limitType() == RateLimitType.API) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
keyBuilder.append("api:")
.append(method.getDeclaringClass().getSimpleName())
.append(".")
.append(method.getName());
return keyBuilder.toString();
}
if (rateLimit.limitType() == RateLimitType.USER) {
try {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
User loginUser = userService.getLoginUser(request);
keyBuilder.append("user:").append(loginUser.getId());
} else {
keyBuilder.append("ip:unknown");
}
} catch (BusinessException e) {
keyBuilder.append("ip:unknown");
}
return keyBuilder.toString();
}
if (rateLimit.limitType() == RateLimitType.IP) {
keyBuilder.append("ip:").append(getClientIP());
return keyBuilder.toString();
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的限流类型");
}
private String getClientIP() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "unknown";
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip != null ? ip : "unknown";
}
}
5. Controller 使用方式
@RateLimit(
limitType = RateLimitType.USER,
rate = 3,
rateInterval = 100,
message = "AI 对话请求过于频繁,请稍后再试"
)
@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatToGenCode(...) {
...
}
这个配置的含义是:同一个用户,100 秒内最多 3 次请求。
6. 常见问题和优化建议
-
trySetRate每次都调用会不会有问题?
通常没问题,首次设置成功后不会重复覆盖,但建议压测确认。 -
USER限流下未登录怎么办?
建议降级到IP,不要直接放过。 -
获取 IP 是否可靠?
生产环境有网关/Nginx 时,记得规范透传并校验可信来源。 -
选
OVERALL还是PER_CLIENT?
分布式统一配额建议OVERALL;客户端隔离场景可评估PER_CLIENT。 -
限流文案要不要统一?
建议统一错误码(比如 429)+ 接口自定义文案。
7. 总结
这套方案的优点:
- 对业务侵入小:方法上加注解即可。
- 分布式一致:多实例共享 Redis 限流状态。
- 易扩展:key 维度和参数都可按接口定制。
一句话:先把限流做成基础设施,再谈性能优化,不然高峰期接口一定会先崩在入口。