Java 注解式限流教程(使用 Redis + AOP)
Java 注解式限流教程(使用 Redis + AOP)
在上一节中,我们已经实现了基于 Redis 的请求频率控制。现在我们将进一步升级功能,使用 Spring AOP + 自定义注解 实现一个更优雅、可复用的限流方式 —— 即通过 @RateLimiter
注解,对任意接口进行限流保护。
🧩 技术栈
- Spring Boot 3.x
- Redis
- Jedis 连接池
- Lua 脚本实现原子性操作
- Spring AOP 实现注解式切面处理
📦 Maven 依赖配置(补充 AOP 支持)
确保你的 pom.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)
与之前保持一致:
spring:data:redis:host: 127.0.0.1port: 6379password: database: 1timeout: 5000msjedis: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
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
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
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
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 {@Beanpublic RedisRateLimiter redisRateLimiter(StringRedisTemplate redisTemplate) {return new RedisRateLimiter(redisTemplate);}
}
🧪 第五步:在控制器中使用注解限流
RateLimitController.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 保证线程安全 |
✅ 易于扩展 | 后续可增加令牌桶算法、滑动窗口优化等 |