
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 保证线程安全 |
✅ 易于扩展 | 后续可增加令牌桶算法、滑动窗口优化等 |