使用方法:
@RateLimit(max = 3, timeWindow = 10, useUserId = false)
仅需要在需要限流的接口加上注解和需求即可开箱即用,无侵入且快速又简洁。
🧩 一、功能概述
本功能的目标是:限制同一个 IP 地址(或用户 ID)在指定时间内对某个接口的访问次数,超过限制后拒绝请求。
适用于:防止恶意刷接口、接口过载等场景。
🏗️ 二、整体实现原理
-
使用自定义注解
@RateLimit标记需要限流的接口; -
使用 AOP 切面
RateLimitAspect拦截被注解的方法; -
在 Redis 中为每个用户(或 IP)维护一个访问计数器;
-
每次调用接口时,先检查当前计数是否超限;
-
若超限则抛出异常,否则继续执行业务逻辑。
🧱 三、模块详解
📌 1. 自定义注解 @RateLimit
✅ 作用:
定义一个限流注解,用于声明接口的限流规则。
@Target(ElementType.METHOD)
表示:该注解 只能用在方法上,比如控制器的方法、服务类的方法等。
@Retention(RetentionPolicy.RUNTIME)
表示:该注解在 运行时仍然存在,可以通过反射读取它。(因为 AOP 是在运行时通过反射扫描被注解的方法,所以值必须是RetentionPolicy.RUNTIME)
常见取值包括:
| RetentionPolicy | 说明 |
|---|---|
SOURCE | 仅在源码中保留,编译后丢弃,常用于注解提示 |
CLASS | 编译时保留在 .class 文件中,运行时不可见 |
RUNTIME | 运行时仍然存在,可被反射读取 |
📄 示例代码:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流 key,默认使用 IP 地址 + 方法路径
*/
String key() default "";
/**
* 最大请求次数
*/
int max() default 5;
/**
* 时间窗口(单位:秒)
*/
int timeWindow() default 60;
/**
* 是否使用当前登录用户ID作为key的一部分
*/
boolean useUserId() default false;
}
🧠 参数说明:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| key | String | 空 | 限流标识(不填时自动使用 URI+IP/用户ID) |
| max | int | 5 | 时间窗口内最大请求次数 |
| timeWindow | int | 60 | 时间窗口(单位:秒) |
| useUserId | boolean | false | 是否使用用户ID做为限流维度 |
📌 2. 限流切面 RateLimitAspect
✅ 作用:
在接口调用前拦截注解,记录请求次数并判断是否超过限流条件。
📄 示例代码说明:
import com.annotations.RateLimit;
import com.context.AppContext;
import com.model.User;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Around("@annotation(com.corepass.ca.cloud.annotations.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); // ⏱️记录开始时间
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
int max = rateLimit.max();
int timeWindow = rateLimit.timeWindow();
String key = rateLimit.key();
if (key.isEmpty()) {
String uri = request.getRequestURI();
key = "rate_limit:" + uri;
}
if (rateLimit.useUserId()) {
// 获取当前用户 ID:可根据你的项目实际调整
String userId = getCurrentUserId(request);
key += ":uid:" + userId;
} else {
String ip = request.getRemoteAddr();
key += ":ip:" + ip;
}
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
}
if (count != null && count > max) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}
Object result = joinPoint.proceed();
// ✅ 方法执行后:记录日志
long elapsed = System.currentTimeMillis() - start;
String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName();
log.info("方法 [{}] 调用成功,限流Key={},耗时={}ms", methodName, key, elapsed);
return result;
}
/**
* 示例方法:根据实际业务获取当前用户 ID
*/
private String getCurrentUserId(HttpServletRequest request) {
// 从 token/session 中解析用户 ID,示例代码如下
User user = AppContext.getCurrentUser();
return user != null ? user.getId().toString() : "anonymous";
}
✳️ 核心流程详解:
| 步骤 | 操作说明 |
|---|---|
| ① | 获取请求对象 HttpServletRequest |
| ② | 获取注解对象 RateLimit,提取配置参数 |
| ③ | 生成 Redis 的限流 Key(支持 IP 或用户 ID) |
| ④ | 调用 redisTemplate.opsForValue().increment(key) 对 key 自增 |
| ⑤ | 如果是第一次访问,则设置 key 的过期时间 |
| ⑥ | 如果 count > max,抛出异常 |
| ⑦ | 否则放行执行目标方法 joinPoint.proceed() |
🧠 关键代码:
String key = rateLimit.key();
if (key.isEmpty()) {
key = "rate_limit:" + request.getRequestURI();
}
if (rateLimit.useUserId()) {
key += ":uid:" + getCurrentUserId(request);
} else {
key += ":ip:" + request.getRemoteAddr();
}
最终 Redis 中的 key 形式可能是:
rate_limit:/api/userLimit:ip:192.168.1.10或
rate_limit:/api/userLimit:uid:123
📌 3. 用户 ID 获取逻辑(可根据项目修改)
private String getCurrentUserId(HttpServletRequest request) {
// 从 token/session 中解析用户 ID,示例代码如下
User user = AppContext.getCurrentUser();
return user != null ? user.getId().toString() : "anonymous";
}
❗注意:
-
如果使用 token 登录,需要解析 token 拿到用户 ID;
-
如果使用 session,也可通过
request.getSession()获取。
🧪 四、接口示例与使用方式
@RateLimit(max = 3, timeWindow = 10, useUserId = false)
@IgnoreAuthentication
@PostMapping("/api/userLimit")
public Result testUserLimit() {
return Result.success("用户ID限流测试成功"+count++);
}
表示含义:
-
对
/api/userLimit接口限制访问; -
同一个 IP 地址,在 10 秒 内最多只能调用 3 次;
-
第 4 次调用将抛出异常:
请求过于频繁,请稍后再试。
🗃️ 五、Redis 中的数据结构
使用的是字符串类型(String),实现简单:
| key | value | 说明 |
|---|---|---|
rate_limit:/api/userLimit:ip:127.0.0.1 | 3 | 表示已请求 3 次 |
| key 有 TTL(如 10s) | 到期后自动清除,重置计数 |
✅ 六、优点与可扩展性
✅ 优点:
-
使用注解,业务代码无侵入;
-
使用 Redis,性能高;
-
支持 IP 或用户 ID 粒度控制;
-
控制精度高(秒级时间窗);
-
可应用于任意接口。
🔧 可扩展建议:
-
支持自定义异常类型(比如统一封装成
RateLimitException); -
支持全局配置默认限流策略;
-
支持配置
blockHandler方法用于限流时降级处理; -
支持分布式部署(Redis 天生支持);
-
支持动态配置限流规则(结合 Nacos/配置中心);

3339

被折叠的 条评论
为什么被折叠?



