Java 注解式限流教程(使用 Redis + AOP)

Java 注解式限流教程(使用 Redis + AOP)

在上一节中,我们已经实现了基于 Redis 的请求频率控制。现在我们将进一步升级功能,使用 Spring AOP + 自定义注解 实现一个更优雅、可复用的限流方式 ------ 即通过 @RateLimiter 注解,对任意接口进行限流保护。


🧩 技术栈

  • Spring Boot 3.x
  • Redis
  • Jedis 连接池
  • Lua 脚本实现原子性操作
  • Spring AOP 实现注解式切面处理

📦 Maven 依赖配置(补充 AOP 支持)

确保你的 pom.xml 中包含以下依赖:

xml 复制代码
<!-- Spring Boot Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.2.10</version>
</dependency>

<!-- Jedis 连接池 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.0.2</version>
</dependency>

<!-- Spring Boot AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.2.10</version>
</dependency>

🛠️ Redis 配置(application.yml)

与之前保持一致:

yaml 复制代码
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 
      database: 1
      timeout: 5000ms
      jedis:
        pool:
          max-active: 8   # 最大连接数
          max-idle: 4     # 最大空闲连接
          min-idle: 1     # 最小空闲连接
          max-wait: 2000ms # 获取连接最大等待时间

📁 项目结构概览

复制代码
org.example.websocket.test
├── annotation
│   └── RateLimiter.java
├── aspect
│   └── RateLimiterAspect.java
├── utils
│   └── RedisRateLimiter.java
├── config
│   └── RateLimiterConfig.java
└── controller
    └── RateLimitController.java

🔖 第一步:创建自定义限流注解

RateLimiter.java

java 复制代码
package org.example.websocket.test.annotation;

import java.lang.annotation.*;

/**
 * 自定义限流注解,支持方法或类级别标注
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

    /**
     * 每个窗口内允许的最大请求数,默认10次
     */
    int limit() default 10;

    /**
     * 窗口时间(秒),默认60秒
     */
    int windowTime() default 60;

    /**
     * 限流维度:
     * - ip: 按客户端IP限流
     * - user: 按用户ID限流(需从参数中获取)
     */
    String key() default "ip";
}

🧠 第二步:编写 AOP 切面逻辑

RateLimiterAspect.java

java 复制代码
package org.example.websocket.test.aspect;

import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.example.websocket.test.annotation.RateLimiter;
import org.example.websocket.test.utils.RedisRateLimiter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Random;

@Aspect
@Component
public class RateLimiterAspect {

    private final RedisRateLimiter redisRateLimiter;

    public RateLimiterAspect(RedisRateLimiter redisRateLimiter) {
        this.redisRateLimiter = redisRateLimiter;
    }

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        String keyPrefix = rateLimiter.key();
        String dynamicKey = "";

        if ("ip".equals(keyPrefix)) {
            dynamicKey = getClientIP(request);
        } else if ("user".equals(keyPrefix)) {
            dynamicKey = request.getParameter("account");
        }

        if (dynamicKey == null || dynamicKey.isEmpty()) {
            return "无法识别限流标识,请检查请求参数或IP信息";
        }

        String redisKey = "rate_limit:" + keyPrefix + ":" + dynamicKey;

        // 执行限流判断
        if (!redisRateLimiter.check(redisKey, rateLimiter.limit(), rateLimiter.windowTime())) {
            return "请求过于频繁,请稍后再试";
        }

        // 防枚举攻击延迟
        try {
            Thread.sleep(new Random().nextInt(200));
        } catch (InterruptedException ignored) {}

        // 继续执行原方法
        return joinPoint.proceed();
    }

    private String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");

        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }

        return ip;
    }
}

🧮 第三步:优化 Redis 限流工具类

RedisRateLimiter.java

java 复制代码
package org.example.websocket.test.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;

public class RedisRateLimiter {

    private final StringRedisTemplate redisTemplate;

    public RedisRateLimiter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean check(String key, int limit, int expireTime) {
        String luaScript = buildLuaScript();
        DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(luaScript, Boolean.class);

        Boolean isAllowed = redisTemplate.execute(script,
                Collections.singletonList(key),
                String.valueOf(limit), String.valueOf(expireTime));

        return Boolean.TRUE.equals(isAllowed);
    }

    private String buildLuaScript() {
        return "local key = KEYS[1]\n" +
               "local limit = tonumber(ARGV[1])\n" +
               "local expire_time = tonumber(ARGV[2])\n" +
               "local current = redis.call('incr', key)\n" +
               "if current == 1 then\n" +
               "    redis.call('expire', key, expire_time)\n" +
               "elseif current > limit then\n" +
               "    return false\n" +
               "end\n" +
               "return true";
    }
}

⚙️ 第四步:注册 RedisRateLimiter Bean

RateLimiterConfig.java

java 复制代码
package org.example.websocket.test.config;

import org.example.websocket.test.utils.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class RateLimiterConfig {

    @Bean
    public RedisRateLimiter redisRateLimiter(StringRedisTemplate redisTemplate) {
        return new RedisRateLimiter(redisTemplate);
    }
}

🧪 第五步:在控制器中使用注解限流

RateLimitController.java

java 复制代码
package org.example.websocket.test.controller;

import jakarta.servlet.http.HttpServletRequest;
import org.example.websocket.test.annotation.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    // 使用注解按 IP 限流
    @GetMapping("/testAnnoRateLimit")
    @RateLimiter(limit = 10, windowTime = 60, key = "ip")
    public String testAnnoRateLimit(@RequestParam String account, HttpServletRequest request) {
        return "恭喜,请求成功放行!";
    }

    // 使用注解按用户限流
    @GetMapping("/testUserRateLimit")
    @RateLimiter(limit = 5, windowTime = 30, key = "user")
    public String testUserRateLimit(@RequestParam String account, HttpServletRequest request) {
        return "用户[" + account + "] 请求成功放行!";
    }
}

✅ 效果演示

当你多次快速访问 /testAnnoRateLimit 接口时,在超过设定频率后会返回:

复制代码
请求过于频繁,请稍后再试

📌 总结

你现在拥有了一个 基于注解的限流系统,可以轻松地对任意接口进行限流保护。该方案具备如下优点:

特性 描述
✅ 注解驱动 使用 @RateLimiter 轻松启用限流
✅ 多种限流维度 支持 IP、用户等多维限流
✅ Lua 原子操作 Redis + Lua 保证线程安全
✅ 易于扩展 后续可增加令牌桶算法、滑动窗口优化等

相关推荐
2501_911828506 分钟前
Python训练营---Day41
开发语言·python·深度学习
c无序11 分钟前
【Go-补充】Sync包
开发语言·后端·golang
benpaodeDD1 小时前
IO流1——体系介绍和字节输出流
java
Watink Cpper2 小时前
[Redis] Redis:高性能内存数据库与分布式架构设计
linux·数据库·redis·分布式·架构
guitarjoy5 小时前
Compose原理 - 整体架构与主流程
java·开发语言
小老鼠不吃猫5 小时前
C接口 中文字符问题
c语言·开发语言
babicu1235 小时前
CSS Day07
java·前端·css
小鸡脚来咯5 小时前
spring IOC控制反转
java·后端·spring
前端码虫6 小时前
JS分支和循环
开发语言·前端·javascript
GISer_Jing6 小时前
MonitorSDK_性能监控(从Web Vital性能指标、PerformanceObserver API和具体代码实现)
开发语言·前端·javascript