基于SpringBoot+AOP实现接口限流
作者:希望永不加班
前面我们用 AOP 实现了操作日志和权限校验,彻底摆脱了代码冗余的困扰;今天继续 AOP 实例——SpringBoot + AOP 实现接口限流。
做后端开发的同学都知道,接口限流是系统稳定性的“第一道防线”:
比如登录接口、短信验证码接口、支付接口,很容易被恶意请求刷爆(比如频繁调用发送短信、恶意登录试错),导致系统响应变慢、服务崩溃,甚至产生额外的费用(短信费、接口调用费)。
如果在每个接口中手动写限流逻辑,不仅代码冗余,还难以统一管理和扩展。而用 AOP 实现接口限流,只需一行注解,就能灵活控制接口的请求频率,不侵入业务代码,兼顾优雅和实用。
一、接口限流的核心场景
接口限流的核心是“限制单位时间内的请求次数”,结合企业实战场景,本次需求覆盖以下核心点,可直接适配大部分项目:
- 多限流策略:支持固定窗口限流(简单易实现)和滑动窗口限流(精准度高,避免临界问题),可灵活选择;
- 自定义限流key:支持按 IP 限流(限制单个IP的请求频率)、按用户ID限流(限制单个用户的请求频率),适配不同场景;
- 自定义限流参数:可灵活配置“单位时间”和“最大请求次数”(如 1分钟内最多请求10次、10秒内最多请求3次);
- 统一限流响应:触发限流时,返回统一的 JSON 格式,包含错误码、错误信息,便于前端提示用户“请求过于频繁”;
- 不侵入业务代码:通过 AOP 增强实现,业务接口无需修改,降低耦合度;
- 分布式适配:支持单机限流(本地缓存)和分布式限流(Redis),适配集群部署场景;
- 异常处理:限流逻辑异常时,不影响接口正常访问,保证系统稳定性。
二、设计思路
在写代码前,先搞懂两个核心限流策略(新手也能轻松理解),以及整体设计思路,避免写代码时逻辑混乱。
1. 两种核心限流策略
(1)固定窗口限流
原理:将时间划分为固定的窗口(如 1分钟一个窗口),统计每个窗口内的请求次数,超过最大次数则触发限流。
举例:配置“1分钟内最多请求10次”,第一个窗口(0-60秒)请求10次后,后续请求被限流;60秒后进入新窗口,请求次数重置,可再次请求。
优点:实现简单、性能高;缺点:存在临界问题(比如59秒请求10次,61秒再请求10次,2秒内请求20次,突破限流阈值)。
(2)滑动窗口限流
原理:将固定窗口拆分为多个小窗口(如 1分钟拆分为6个10秒小窗口),每次请求时,只统计“当前时间往前推1分钟”内的请求次数,超过阈值则限流。
举例:同样配置“1分钟内最多请求10次”,59秒请求10次后,61秒请求时,统计的是1-61秒内的请求次数(仍为10次),会被限流,避免临界问题。
优点:限流精准,无临界问题;缺点:实现稍复杂,性能略低于固定窗口。
2. 整体设计思路
- 自定义注解:创建
@RateLimit注解,用于标记需要限流的接口,配置限流策略、限流key、时间窗口、最大请求次数; - 限流工具类:分别实现固定窗口和滑动窗口的限流逻辑,支持本地缓存(单机)和 Redis(分布式)存储请求次数;
- AOP 切面:定义切点(拦截所有添加了
@RateLimit注解的方法),用环绕通知实现限流校验逻辑; - 限流key生成:根据注解配置,生成不同的限流key(IP/用户ID),实现精准限流;
- 统一异常与响应:触发限流时,抛出自定义限流异常,通过全局异常处理器返回统一 JSON 响应;
- 多场景测试:覆盖单机/分布式、不同限流策略、不同限流key,验证限流效果。
三、完整代码
本次实战基于 SpringBoot 2.7.x 版本,整合 Redis(支持分布式限流),所有代码添加详细注释,新手也能轻松理解每一步的作用,无需修改核心逻辑,直接适配项目。
步骤1:导入核心依赖
需要导入 AOP 依赖、Redis 依赖、工具包,pom.xml 如下:
<!-- Spring AOP 核心依赖(限流核心) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis 依赖(分布式限流必备,单机可省略) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具包(JSON 响应、缓存操作,简化代码) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- lombok 依赖(简化实体类、工具类代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>步骤2:配置文件(application.yml)
配置 Redis、服务器端口,单机限流可省略 Redis 配置:
server:
port: 8080 # 服务器端口
# Redis 配置(分布式限流必备)
spring:
redis:
host: localhost # Redis 地址(本地测试用)
port: 6379 # Redis 端口
password: # Redis 密码(无密码则留空)
database: 0 # 数据库索引
lettuce:
pool:
max-active: 100 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
# 限流全局配置(可选,可在注解中覆盖)
rate-limit:
default-time: 60 # 默认时间窗口(秒)
default-count: 10 # 默认最大请求次数
default-type: FIXED_WINDOW # 默认限流策略(FIXED_WINDOW:固定窗口,SLIDING_WINDOW:滑动窗口)
default-key-type: IP # 默认限流key类型(IP:按IP限流,USER_ID:按用户ID限流)步骤3:自定义限流注解
创建 @RateLimit 注解,用于标记需要限流的接口,可灵活配置限流参数,贴合企业实战需求:
import java.lang.annotation.*;
/**
* 自定义接口限流注解
* @Target(ElementType.METHOD):仅作用于方法(接口方法)
* @Retention(RetentionPolicy.RUNTIME):运行时保留,AOP 切面可获取注解属性
* @Documented:生成 API 文档时,显示该注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流策略(固定窗口/滑动窗口)
* 默认为全局配置的策略,可在接口上单独配置覆盖
*/
LimitType type() default LimitType.FIXED_WINDOW;
/**
* 限流key类型(按IP/按用户ID)
* 默认为全局配置的key类型,可单独覆盖
*/
KeyType keyType() default KeyType.IP;
/**
* 时间窗口(单位:秒)
* 默认为全局配置的时间,可单独覆盖(如 60=1分钟,10=10秒)
*/
int time() default 0;
/**
* 单位时间内的最大请求次数(限流阈值)
* 默认为全局配置的次数,可单独覆盖
*/
int count() default 0;
/**
* 限流提示信息(触发限流时返回)
*/
String message() default "请求过于频繁,请稍后再试!";
/**
* 限流存储方式(本地缓存/Redis)
* 默认为 Redis,单机部署可改为 LOCAL
*/
StoreType storeType() default StoreType.REDIS;
/**
* 限流策略枚举
*/
enum LimitType {
FIXED_WINDOW, // 固定窗口限流
SLIDING_WINDOW // 滑动窗口限流
}
/**
* 限流key类型枚举
*/
enum KeyType {
IP, // 按请求IP限流(最常用)
USER_ID // 按当前登录用户ID限流(需结合用户上下文)
}
/**
* 存储方式枚举
*/
enum StoreType {
LOCAL, // 本地缓存(单机部署用)
REDIS // Redis(分布式部署用)
}
}注解属性说明:
- type:选择限流策略,固定窗口简单,滑动窗口精准,可根据场景选择;
- keyType:选择限流粒度,IP 用于匿名接口(如登录、短信),USER_ID 用于登录后接口(如个人中心);
- time + count:共同定义限流规则,如 time=60、count=10 → 1分钟内最多请求10次;
- storeType:单机部署用 LOCAL(本地缓存),集群部署用 REDIS(分布式缓存),保证限流统一。
步骤4:核心工具类
这部分是限流的核心,分别实现固定窗口、滑动窗口的限流逻辑,支持本地缓存和 Redis 存储,代码可直接复用:
4.1 限流常量类(统一管理key前缀)
/**
* 限流常量类(统一管理 Redis/本地缓存的key前缀,避免混乱)
*/
public class RateLimitConstant {
// 限流key前缀(Redis中使用,如 rate_limit:ip:127.0.0.1:接口路径)
public static final String RATE_LIMIT_KEY_PREFIX = "rate_limit:";
// 滑动窗口小窗口大小(默认10秒,可根据需求调整)
public static final int SLIDING_WINDOW_INTERVAL = 10;
}4.2 限流工具类
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 限流工具类(实现固定窗口、滑动窗口限流,支持本地/Redis存储)
*/
@Slf4j
@Component
public class RateLimitUtil {
// 本地缓存(单机限流用,ConcurrentHashMap 线程安全)
private final ConcurrentHashMap<String, Integer> localCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Long> localWindowCache = new ConcurrentHashMap<>();
// Redis 模板(分布式限流用)
@Autowired(required = false) // 单机部署时,Redis 可省略,避免报错
private StringRedisTemplate stringRedisTemplate;
/**
* 固定窗口限流(核心方法)
* @param key 限流key(如 ip:127.0.0.1:接口路径)
* @param time 时间窗口(秒)
* @param count 最大请求次数
* @param storeType 存储方式(本地/Redis)
* @return true=触发限流,false=未触发限流
*/
public boolean fixedWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
if (storeType == RateLimit.StoreType.LOCAL) {
// 本地缓存实现固定窗口
return localFixedWindowLimit(key, time, count);
} else {
// Redis 实现固定窗口(分布式)
return redisFixedWindowLimit(key, time, count);
}
}
/**
* 滑动窗口限流(核心方法)
* @param key 限流key(如 ip:127.0.0.1:接口路径)
* @param time 时间窗口(秒)
* @param count 最大请求次数
* @param storeType 存储方式(本地/Redis)
* @return true=触发限流,false=未触发限流
*/
public boolean slidingWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
if (storeType == RateLimit.StoreType.LOCAL) {
// 本地缓存实现滑动窗口
return localSlidingWindowLimit(key, time, count);
} else {
// Redis 实现滑动窗口(分布式)
return redisSlidingWindowLimit(key, time, count);
}
}
/**
* 本地缓存 - 固定窗口限流
*/
private boolean localFixedWindowLimit(String key, int time, int count) {
// 1. 获取当前窗口的请求次数
Integer currentCount = localCache.getOrDefault(key, 0);
// 2. 检查是否超过限流阈值
if (currentCount >= count) {
log.warn("本地固定窗口限流触发,key:{},当前次数:{},阈值:{}", key, currentCount, count);
return true;
}
// 3. 第一次请求,设置窗口过期时间(time秒后清空)
if (currentCount == 0) {
localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
} else {
// 检查窗口是否过期,过期则重置次数和窗口时间
Long expireTime = localWindowCache.get(key);
if (System.currentTimeMillis() > expireTime) {
localCache.put(key, 1);
localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
return false;
}
}
// 4. 未超过阈值,请求次数+1
localCache.put(key, currentCount + 1);
return false;
}
/**
* Redis - 固定窗口限流(分布式,集群部署用)
*/
private boolean redisFixedWindowLimit(String key, int time, int count) {
// 1. 拼接 Redis key(加上前缀,避免与其他key冲突)
String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
// 2. 自增请求次数(原子操作,避免并发问题)
Long currentCount = stringRedisTemplate.opsForValue().increment(redisKey, 1);
// 3. 第一次请求,设置过期时间(time秒)
if (currentCount != null && currentCount == 1) {
stringRedisTemplate.expire(redisKey, time, TimeUnit.SECONDS);
}
// 4. 检查是否超过限流阈值
if (currentCount != null && currentCount > count) {
log.warn("Redis固定窗口限流触发,key:{},当前次数:{},阈值:{}", redisKey, currentCount, count);
return true;
}
return false;
}
/**
* 本地缓存 - 滑动窗口限流
*/
private boolean localSlidingWindowLimit(String key, int time, int count) {
long now = System.currentTimeMillis();
// 1. 计算当前窗口的起始时间(当前时间 - 时间窗口)
long windowStart = now - time * 1000;
// 2. 拼接滑动窗口的key(包含主key和小窗口时间)
String windowKey = key + ":" + (now / (RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000));
// 3. 获取当前小窗口的请求次数
Integer currentWindowCount = localCache.getOrDefault(windowKey, 0);
// 4. 遍历所有小窗口,统计整个滑动窗口内的总请求次数
int totalCount = 0;
for (String cacheKey : localCache.keySet()) {
if (cacheKey.startsWith(key + ":")) {
// 解析小窗口时间
long windowTime = Long.parseLong(cacheKey.split(":")[2]);
long windowTimeMillis = windowTime * RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000;
// 只统计当前滑动窗口内的小窗口
if (windowTimeMillis >= windowStart) {
totalCount += localCache.get(cacheKey);
} else {
// 移除过期的小窗口缓存
localCache.remove(cacheKey);
}
}
}
// 5. 检查是否超过限流阈值
if (totalCount >= count) {
log.warn("本地滑动窗口限流触发,key:{},当前总次数:{},阈值:{}", key, totalCount, count);
return true;
}
// 6. 未超过阈值,当前小窗口请求次数+1
localCache.put(windowKey, currentWindowCount + 1);
return false;
}
/**
* Redis - 滑动窗口限流(分布式,集群部署用)
*/
private boolean redisSlidingWindowLimit(String key, int time, int count) {
long now = System.currentTimeMillis();
// 1. 计算当前窗口的起始时间(当前时间 - 时间窗口)
long windowStart = now - time * 1000;
// 2. 拼接 Redis key(加上前缀)
String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
// 3. 小窗口大小(默认10秒,可调整)
int interval = RateLimitConstant.SLIDING_WINDOW_INTERVAL;
// 4. 当前小窗口的时间戳(按小窗口大小取整)
long currentWindow = now / (interval * 1000);
// 5. Redis 原子操作:删除过期小窗口 + 统计当前窗口总次数 + 自增当前小窗口次数
// 用 Lua 脚本实现原子操作,避免并发问题
String luaScript = "local key = KEYS[1]\n" +
"local windowStart = ARGV[1]\n" +
"local currentWindow = ARGV[2]\n" +
"local interval = ARGV[3]\n" +
"local count = ARGV[4]\n" +
"-- 删除过期的小窗口(小于windowStart的小窗口)\n" +
"redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n" +
"-- 统计当前窗口内的总请求次数\n" +
"local total = redis.call('ZCARD', key)\n" +
"if total >= tonumber(count) then\n" +
" return 1\n" +
"end\n" +
"-- 自增当前小窗口的请求次数(将小窗口时间戳作为score,请求ID作为value)\n" +
"redis.call('ZADD', key, currentWindow, currentWindow .. ':' .. redis.call('INCR', key .. ':seq'))\n" +
"-- 设置过期时间(确保缓存自动清理)\n" +
"redis.call('EXPIRE', key, tonumber(interval) + 1)\n" +
"return 0";
// 执行 Lua 脚本
Long result = stringRedisTemplate.execute(
new org.springframework.data.redis.core.script.DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(redisKey),
String.valueOf(windowStart),
String.valueOf(currentWindow),
String.valueOf(interval),
String.valueOf(count)
);
// 6. 结果判断:1=触发限流,0=未触发
if (result != null && result == 1) {
log.warn("Redis滑动窗口限流触发,key:{},阈值:{}", redisKey, count);
return true;
}
return false;
}
/**
* 清除指定key的限流缓存(用于特殊场景,如用户注销、IP解封)
*/
public void clearLimitCache(String key, RateLimit.StoreType storeType) {
if (storeType == RateLimit.StoreType.LOCAL) {
// 清除本地缓存(包含所有小窗口)
localCache.keySet().removeIf(k -> k.startsWith(key) || k.equals(key));
localWindowCache.remove(key);
} else {
// 清除Redis缓存
String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
stringRedisTemplate.delete(redisKey);
stringRedisTemplate.delete(redisKey + ":seq");
}
}
}步骤5:辅助工具类(获取IP、用户上下文)
实现获取客户端IP、当前登录用户ID的工具类,用于生成限流key,贴合实战场景:
5.1 IP工具类(获取客户端真实IP,处理代理场景)
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* IP工具类(获取客户端真实IP,处理Nginx代理等场景)
*/
public class IpUtil {
/**
* 获取客户端真实IP
*/
public static String getClientIp() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "127.0.0.1"; // 非web环境,默认本地IP
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多代理场景,取第一个非unknown的IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
// 本地环境默认IP(避免localhost解析问题)
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}5.2 用户上下文
实际项目中,从JWT Token或Spring Security中获取用户ID,这里模拟实现,可直接替换为项目中的真实逻辑:
/**
* 用户上下文(获取当前登录用户信息,用于按用户ID限流)
*/
public class UserContext {
/**
* 获取当前登录用户ID(模拟,实际从JWT/Token中解析)
* @return 用户ID(未登录返回null)
*/
public static Long getCurrentUserId() {
// 模拟:登录用户ID为1001,未登录返回null
// 实际项目替换为:JwtUtils.parseToken(token).getUserId()
return 1001L;
}
}步骤6:AOP 限流切面
创建切面类,拦截所有添加了 @RateLimit 注解的接口,实现限流校验逻辑,优先于日志切面执行:
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* 接口限流切面(核心类)
* @Aspect:标记此类为AOP切面
* @Component:交给Spring管理,确保Spring能扫描到
* @Order(1):优先级高于日志切面(避免限流请求被记录日志)
* @Slf4j:日志输出
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class RateLimitAspect {
// 注入限流工具类
@Resource
private RateLimitUtil rateLimitUtil;
// 全局默认配置(从配置文件读取)
@Value("${rate-limit.default-time}")
private int defaultTime;
@Value("${rate-limit.default-count}")
private int defaultCount;
@Value("${rate-limit.default-type}")
private RateLimit.LimitType defaultLimitType;
@Value("${rate-limit.default-key-type}")
private RateLimit.KeyType defaultKeyType;
// 1. 定义切点:拦截所有添加了 @RateLimit 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.RateLimit)")
public void rateLimitPointcut() {}
// 2. 环绕通知:包裹目标方法,执行限流校验
@Around("rateLimitPointcut()")
public Object doRateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
// 第一步:获取目标方法上的 @RateLimit 注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = signature.getMethod();
RateLimit rateLimitAnno = targetMethod.getAnnotation(RateLimit.class);
// 第二步:获取注解配置的限流参数(无配置则用全局默认值)
RateLimit.LimitType limitType = rateLimitAnno.type() == RateLimit.LimitType.FIXED_WINDOW ?
rateLimitAnno.type() : defaultLimitType;
RateLimit.KeyType keyType = rateLimitAnno.keyType() == RateLimit.KeyType.IP ?
rateLimitAnno.keyType() : defaultKeyType;
int time = rateLimitAnno.time() == 0 ? defaultTime : rateLimitAnno.time();
int count = rateLimitAnno.count() == 0 ? defaultCount : rateLimitAnno.count();
String message = rateLimitAnno.message();
RateLimit.StoreType storeType = rateLimitAnno.storeType();
// 第三步:生成限流key(根据keyType生成,确保唯一)
String limitKey = generateLimitKey(joinPoint, keyType);
log.info("接口限流校验,key:{},策略:{},时间窗口:{}秒,阈值:{}次",
limitKey, limitType, time, count);
// 第四步:执行限流校验(根据限流策略选择对应的方法)
boolean isLimit = false;
if (limitType == RateLimit.LimitType.FIXED_WINDOW) {
isLimit = rateLimitUtil.fixedWindowLimit(limitKey, time, count, storeType);
} else if (limitType == RateLimit.LimitType.SLIDING_WINDOW) {
isLimit = rateLimitUtil.slidingWindowLimit(limitKey, time, count, storeType);
}
// 第五步:判断是否触发限流,触发则抛出异常
if (isLimit) {
throw new RateLimitException(429, message);
}
// 第六步:限流校验通过,执行目标方法(核心业务逻辑)
return joinPoint.proceed();
}
/**
* 生成限流key(确保唯一,避免不同接口/不同IP/不同用户的限流冲突)
* @param joinPoint 切入点(获取接口路径)
* @param keyType 限流key类型(IP/USER_ID)
* @return 唯一限流key
*/
private String generateLimitKey(ProceedingJoinPoint joinPoint, RateLimit.KeyType keyType) {
// 获取接口路径(如 /api/auth/login)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getDeclaringTypeName() + "." + signature.getMethod().getName();
// 根据keyType生成不同的限流key
if (keyType == RateLimit.KeyType.IP) {
// 按IP限流:ip:接口路径(如 ip:127.0.0.1:com.example.demo.controller.AuthController.login)
String ip = IpUtil.getClientIp();
return "ip:" + ip + ":" + methodName;
} else if (keyType == RateLimit.KeyType.USER_ID) {
// 按用户ID限流:user:用户ID:接口路径(如 user:1001:com.example.demo.controller.UserController.edit)
Long userId = UserContext.getCurrentUserId();
if (userId == null) {
// 未登录用户,按IP限流(避免key为空)
String ip = IpUtil.getClientIp();
return "ip:" + ip + ":" + methodName;
}
return "user:" + userId + ":" + methodName;
}
// 默认按IP限流
String ip = IpUtil.getClientIp();
return "ip:" + ip + ":" + methodName;
}
}步骤7:自定义限流异常 + 全局异常处理器
触发限流时,抛出自定义异常,通过全局异常处理器返回统一的 JSON 响应,便于前端统一处理:
7.1 自定义限流异常
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 自定义限流异常(触发限流时抛出)
* 429 状态码:Too Many Requests(请求过于频繁)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RateLimitException extends RuntimeException {
// 错误码(429 标准限流状态码)
private Integer code;
// 错误信息(自定义提示)
private String message;
// 构造方法(简化异常抛出)
public RateLimitException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}7.2 全局异常处理器
import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器(统一响应格式)
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// 拦截限流异常(429 状态码)
@ExceptionHandler(RateLimitException.class)
public JSONObject handleRateLimitException(RateLimitException e) {
JSONObject response = new JSONObject();
response.put("code", e.getCode());
response.put("msg", e.getMessage());
response.put("data", null);
return response;
}
// 拦截其他异常(兜底处理)
@ExceptionHandler(Exception.class)
public JSONObject handleException(Exception e) {
JSONObject response = new JSONObject();
response.put("code", 500);
response.put("msg", "服务器内部异常,请联系管理员");
response.put("data", null);
return response;
}
}步骤8:接口使用示例
在需要限流的接口上添加 @RateLimit 注解,根据业务需求配置参数,无需修改接口内部业务代码:
import com.example.demo.annotation.RateLimit;
import com.example.demo.util.UserContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口(覆盖限流多场景)
*/
@RestController
@RequestMapping("/api")
public class TestController {
/**
* 场景1:短信验证码接口(按IP限流,固定窗口,1分钟最多3次)
* 高频场景,防止恶意刷短信
*/
@RateLimit(
type = RateLimit.LimitType.FIXED_WINDOW,
keyType = RateLimit.KeyType.IP,
time = 60, // 1分钟
count = 3, // 最多3次
message = "短信发送过于频繁,请1分钟后再试!"
)
@PostMapping("/sms/send")
public String sendSms(String phone) {
// 核心业务逻辑:发送短信验证码
return "短信已发送至:" + phone;
}
/**
* 场景2:登录接口(按IP限流,滑动窗口,10秒最多2次)
* 防止恶意暴力破解密码,滑动窗口避免临界问题
*/
@RateLimit(
type = RateLimit.LimitType.SLIDING_WINDOW,
keyType = RateLimit.KeyType.IP,
time = 10, // 10秒
count = 2, // 最多2次
message = "登录请求过于频繁,请10秒后再试!"
)
@PostMapping("/auth/login")
public String login(String username, String password) {
// 核心业务逻辑:用户登录
return "登录成功,欢迎您:" + username;
}
/**
* 场景3:个人中心接口(按用户ID限流,固定窗口,1分钟最多10次)
* 登录后接口,按用户ID限流,避免单个用户恶意请求
*/
@RateLimit(
type = RateLimit.LimitType.FIXED_WINDOW,
keyType = RateLimit.KeyType.USER_ID,
time = 60,
count = 10,
message = "操作过于频繁,请1分钟后再试!"
)
@GetMapping("/user/profile")
public String userProfile() {
Long userId = UserContext.getCurrentUserId();
// 核心业务逻辑:查询用户个人信息
return "用户ID:" + userId + ",个人信息查询成功";
}
/**
* 场景4:分布式限流(Redis存储,滑动窗口,5秒最多5次)
* 集群部署场景,确保多节点限流统一
*/
@RateLimit(
type = RateLimit.LimitType.SLIDING_WINDOW,
keyType = RateLimit.KeyType.IP,
time = 5,
count = 5,
storeType = RateLimit.StoreType.REDIS,
message = "请求过于频繁,请5秒后再试!"
)
@GetMapping("/test/distributed")
public String distributedLimit() {
// 核心业务逻辑:分布式场景测试
return "分布式限流测试成功";
}
}四、测试验证
用 Postman 测试以下核心场景,验证限流效果,确保符合预期:
测试场景1:短信接口限流(固定窗口,IP限流)
请求地址:http://localhost:8080/api/sms/send?phone=13800138000
请求方式:POST
测试操作:1分钟内连续请求4次
测试结果:前3次正常返回“短信已发送”,第4次返回限流响应(code=429,msg=短信发送过于频繁),符合预期。
测试场景2:登录接口限流(滑动窗口,IP限流)
请求地址:http://localhost:8080/api/auth/login?username=test&password=123456
请求方式:POST
测试操作:第1次请求(0秒)、第2次请求(5秒)、第3次请求(8秒)
测试结果:前2次正常返回,第3次触发限流(10秒内超过2次),符合预期,无临界问题。
测试场景3:个人中心接口(用户ID限流)
请求地址:http://localhost:8080/api/user/profile
请求方式:GET
测试操作:1分钟内连续请求11次
测试结果:前10次正常返回,第11次触发限流,符合预期。
测试场景4:分布式限流(Redis存储)
启动2个项目节点(端口8080、8081),用同一IP分别向两个节点请求5次(共10次),时间窗口5秒
测试结果:两个节点合计请求超过5次后,触发限流,说明Redis分布式限流生效,多节点限流统一。
文末小结
SpringBoot + AOP 实现接口限流,是企业项目中保障系统稳定性的必备方案,核心逻辑就是「注解标记 + AOP 拦截 + 限流校验」,不侵入业务代码,灵活适配单机、分布式等多种场景。
以上就是基于SpringBoot+AOP实现接口限流的详细内容,更多关于SpringBoot AOP接口限流的资料请关注脚本之家其它相关文章!
