springboot redis 窗口限流
时间: 2025-01-31 09:55:40 浏览: 40
### 使用Spring Boot和Redis实现固定时间窗口限流
#### 项目初始化
为了创建一个新的Spring Boot应用程序,可以使用Spring Initializr来快速搭建项目框架。确保引入必要的依赖项,如`spring-boot-starter-data-redis`用于连接Redis数据库以及`spring-boot-starter-aop`支持面向切面编程[^2]。
#### 创建限流注解
定义一个自定义注解`@RateLimit`,该注解可用于标记需要进行速率限制的方法或类。此注解应包含参数以指定允许的最大请求次数及单位时间长度等信息[^1]。
```java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int maxRequests() default 100; // 默认最大请求数
String timeWindow(); // 时间窗口大小(秒)
}
```
#### Lua脚本编写
准备一段Lua脚本来处理计数逻辑并返回是否超出限额的结果。这段代码将在每次调用受保护的服务端点之前执行,并由Redis服务器解释运行[^4]。
```lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current_time = tonumber(redis.call('time')[1])
-- 获取当前存储的时间戳和计数值
local stored_data = redis.call("GET", key)
if not stored_data then
-- 如果不存在,则设置初始值
redis.call("SET", key, string.format("%d:%d", current_time, 1))
redis.call("EXPIRE", key, ARGV[2]) -- 设置过期时间为窗口周期
else
local parts = {}
for part in string.gmatch(stored_data, "[^:]+") do table.insert(parts, part) end
local last_timestamp = tonumber(parts[1])
local count = tonumber(parts[2])
if (current_time - last_timestamp) >= tonumber(ARGV[2]) then
-- 当前时间超过设定间隔,重置计数器
redis.call("SET", key, string.format("%d:%d", current_time, 1))
elseif count < limit then
-- 更新现有条目中的计数
redis.call("INCRBY", key, 1)
else
return "LIMIT_EXCEEDED"
end
end
return "ALLOWED"
```
#### Redis处理器构建
开发服务层组件负责与Redis交互,加载上述Lua脚本并通过命令传递给它相应的参数来进行实际的限流判断工作。
```java
@Service
public class RedisRateLimiter {
private final ReactiveRedisTemplate<String, Object> template;
public RedisRateLimiter(final ReactiveRedisTemplate<String, Object> template){
this.template = template;
}
/**
* 执行Lua脚本实施限流策略.
*/
public Mono<Boolean> isAllowed(String apiKey, Integer maxRequestsPerSecond, Long windowInSeconds) {
List<String> keys = Collections.singletonList(apiKey);
List<String> args = Arrays.asList(
String.valueOf(maxRequestsPerSecond),
String.valueOf(windowInSeconds));
return template.execute(new DefaultRedisScript<>(new Resource() {
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream("""
-- 上述Lua脚本内容...
""".getBytes());
}
}, Boolean.class), keys, args).map(result -> !"LIMIT_EXCEEDED".equals(result));
}
}
```
#### 编写AOP切片
利用Spring AOP特性拦截带有特定注解的方法调用,在方法真正被执行之前先检查其是否满足预设条件——即未违反规定的访问频率限制。
```java
@Aspect
@Component
@Slf4j
public class RateLimitingAspect {
private static final Logger logger = LoggerFactory.getLogger(RateLimitingAspect.class);
private final RedisRateLimiter rateLimiter;
public RateLimitingAspect(RedisRateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
}
@Around("@annotation(rateLimit)")
public Object handleApiCall(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
Class<?> targetClass = method.getDeclaringClass();
boolean allowed = rateLimiter.isAllowed(targetClass.getSimpleName().concat(".").concat(methodName),
rateLimit.maxRequests(),
Long.parseLong(rateLimit.timeWindow())).blockOptional().orElse(false);
if (!allowed) {
throw new TooManyRequestsException("Too many requests");
} else {
return joinPoint.proceed();
}
}
}
```
#### Controller接口测试
最后一步是在控制器中应用新创建好的注解来验证整个流程能否正常运作。当尝试发送过多请求时,应该会收到HTTP状态码429表示太频繁了;而在合理范围内则能成功响应数据。
```java
@RestController
@RequestMapping("/api/v1")
public class ExampleController {
@GetMapping("/test-rate-limit")
@RateLimit(timeWindow="60") // 每分钟最多接收100次请求
public ResponseEntity<String> testRateLimit(){
return ResponseEntity.ok("Request processed successfully.");
}
}
```
阅读全文
相关推荐


















