AOP + Redisson 实现接口级限流机制(注解式优雅接入)

🧭 前言

在高并发系统中,限流是保护后端服务的一道重要“保险”。尤其是开放的 API 接口,如果没有限流策略,容易被恶意调用,造成雪崩效应。

本篇将结合 Redis + Redisson,实现在 Spring Boot 项目中通过自定义注解优雅地接入接口限流。


🎯目标需求

  • ✅ 基于注解 @RateLimiter 控制接口访问频率;

  • ✅ 支持自定义限流时间窗口和请求次数;

  • ✅ 支持基于 IP 或自定义 key 维度限流;

  • ✅ 使用 Redisson 分布式锁/令牌桶机制,保证集群下生效;

  • ✅ 超过限流阈值后返回友好提示,不继续执行业务逻辑。


🧱技术选型

  • Redis:作为分布式限流的数据中枢;

  • Redisson:提供分布式锁、RateLimiter 等限流组件;

  • AOP:统一拦截使用 @RateLimiter 注解的方法;

  • Spring Boot:快速整合和注解配置。


🔧注解定义

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    String key() default "";        // 限流 key,支持 SpEL
    long time() default 60;         // 时间窗口(单位:秒)
    long count() default 10;        // 最大访问次数
    boolean perIp() default true;   // 是否按 IP 限流
}

 


⚙AOP 切面实现

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimiterAspect {

    private final RedissonClient redissonClient;

    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint pjp, RateLimiter rateLimiter) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Object[] args = pjp.getArgs();
        String[] paramNames = nameDiscoverer.getParameterNames(method);

        String baseKey = StringUtils.defaultIfBlank(rateLimiter.key(), method.getName());
        String ip = IpUtils.getIpAddress(); // 自定义工具类,获取请求 IP

        // 解析 SpEL 表达式
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }

        String spelKey = baseKey;
        if (StringUtils.isNotBlank(rateLimiter.key())) {
            spelKey = parser.parseExpression(rateLimiter.key()).getValue(context, String.class);
        }

        String finalKey = "rate_limiter:" + spelKey;
        if (rateLimiter.perIp()) {
            finalKey += ":" + ip;
        }

        /*
         * 设置限流规则
         *
         * 限流类型:全局限流
         * 最大令牌数量(允许的请求次数)
         * 时间窗口大小
         * 时间单位(秒、分钟等)
         */
        RRateLimiter limiter = redissonClient.getRateLimiter(finalKey);
        limiter.trySetRate(RateType.OVERALL, rateLimiter.count(), rateLimiter.time(), RateIntervalUnit.SECONDS);

        // 尝试获取一个令牌
        boolean allowed = limiter.tryAcquire(1);
        if (!allowed) {
            log.warn("访问被限流,key={}, ip={}", finalKey, ip);
            throw new CustomException("请求过于频繁,请稍后再试");
        }

        return pjp.proceed();
    }
}

🧪使用示例

@PostMapping("/submit")
@RateLimiter(key = "#user.id", count = 5, time = 60)
public CustomJsonResult submit(@RequestBody UserDTO user) {
    return CustomJsonResult.success("提交成功");
}

 

 
posted @ 2025-05-20 08:48  ~落辰~  阅读(228)  评论(0)    收藏  举报