一篇讲透:Spring Boot + Redisson + 注解 + AOP 实现接口限流(可直接落地)

7 阅读3分钟

一篇讲透:Spring Boot + Redisson + 注解 + AOP 实现接口限流(可直接落地)

前言

最近在做 AI 对话接口时,最先踩的坑不是模型能力,而是请求洪峰。
尤其是流式接口,如果不控流,后端线程、Redis、模型调用链都会被拖垮。

这篇文章我直接给出一套我在线上可用的限流方案,包含:

  1. 配置(Redis / Redisson)
  2. 注解定义(@RateLimit
  3. 切面定义(AOP 拦截 + 分布式令牌桶)

目标是:复制过去就能跑,改几处参数就能用。


结论先行

  1. 单机 Guava RateLimiter 不够,分布式场景建议直接上 Redisson RRateLimiter
  2. 限流一定要做成注解 + 切面,避免业务代码里到处写重复判断。
  3. key 设计是核心:至少支持 APIUSERIP 三种维度。
  4. 令牌桶参数要按接口“价值”区分,别全局一个值打天下。

目录

  1. 依赖和配置
  2. 限流注解定义
  3. 限流类型枚举
  4. AOP 切面实现
  5. Controller 使用方式
  6. 常见问题和优化建议
  7. 总结

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 "请求过于频繁,请稍后再试";
}

参数含义:

  1. rate:窗口内允许的请求数
  2. rateInterval:窗口大小(秒)
  3. limitType:按 API、用户、IP 限流
  4. key:业务自定义前缀(可选)
  5. 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. 常见问题和优化建议

  1. trySetRate 每次都调用会不会有问题?
    通常没问题,首次设置成功后不会重复覆盖,但建议压测确认。

  2. USER 限流下未登录怎么办?
    建议降级到 IP,不要直接放过。

  3. 获取 IP 是否可靠?
    生产环境有网关/Nginx 时,记得规范透传并校验可信来源。

  4. OVERALL 还是 PER_CLIENT
    分布式统一配额建议 OVERALL;客户端隔离场景可评估 PER_CLIENT

  5. 限流文案要不要统一?
    建议统一错误码(比如 429)+ 接口自定义文案。


7. 总结

这套方案的优点:

  1. 对业务侵入小:方法上加注解即可。
  2. 分布式一致:多实例共享 Redis 限流状态。
  3. 易扩展:key 维度和参数都可按接口定制。

一句话:先把限流做成基础设施,再谈性能优化,不然高峰期接口一定会先崩在入口。