SpringBoot 中 AOP 实现接口限流

张开发
2026/4/17 14:12:45 15 分钟阅读

分享文章

SpringBoot 中 AOP 实现接口限流
前面我们用 AOP 实现了操作日志和权限校验彻底摆脱了代码冗余的困扰今天继续 AOP 实例——SpringBoot AOP 实现接口限流。做后端开发的同学都知道接口限流是系统稳定性的“第一道防线”比如登录接口、短信验证码接口、支付接口很容易被恶意请求刷爆比如频繁调用发送短信、恶意登录试错导致系统响应变慢、服务崩溃甚至产生额外的费用短信费、接口调用费。如果在每个接口中手动写限流逻辑不仅代码冗余还难以统一管理和扩展。而用 AOP 实现接口限流只需一行注解就能灵活控制接口的请求频率不侵入业务代码兼顾优雅和实用。一、接口限流的核心场景接口限流的核心是“限制单位时间内的请求次数”结合企业实战场景本次需求覆盖以下核心点可直接适配大部分项目1.多限流策略支持固定窗口限流简单易实现和滑动窗口限流精准度高避免临界问题可灵活选择2.自定义限流key支持按 IP 限流限制单个IP的请求频率、按用户ID限流限制单个用户的请求频率适配不同场景3.自定义限流参数可灵活配置“单位时间”和“最大请求次数”如 1分钟内最多请求10次、10秒内最多请求3次4.统一限流响应触发限流时返回统一的 JSON 格式包含错误码、错误信息便于前端提示用户“请求过于频繁”5.不侵入业务代码通过 AOP 增强实现业务接口无需修改降低耦合度6.分布式适配支持单机限流本地缓存和分布式限流Redis适配集群部署场景7.异常处理限流逻辑异常时不影响接口正常访问保证系统稳定性。二、设计思路在写代码前先搞懂两个核心限流策略新手也能轻松理解以及整体设计思路避免写代码时逻辑混乱。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. 整体设计思路1.自定义注解创建RateLimit注解用于标记需要限流的接口配置限流策略、限流key、时间窗口、最大请求次数2.限流工具类分别实现固定窗口和滑动窗口的限流逻辑支持本地缓存单机和 Redis分布式存储请求次数3.AOP 切面定义切点拦截所有添加了RateLimit注解的方法用环绕通知实现限流校验逻辑4.限流key生成根据注解配置生成不同的限流keyIP/用户ID实现精准限流5.统一异常与响应触发限流时抛出自定义限流异常通过全局异常处理器返回统一 JSON 响应6.多场景测试覆盖单机/分布式、不同限流策略、不同限流key验证限流效果。三、完整代码本次实战基于 SpringBoot 2.7.x 版本整合 Redis支持分布式限流所有代码添加详细注释新手也能轻松理解每一步的作用无需修改核心逻辑直接适配项目。步骤1导入核心依赖需要导入 AOP 依赖、Redis 依赖、工具包pom.xml 如下!-- Spring AOP 核心依赖限流核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency !-- Redis 依赖分布式限流必备单机可省略 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 工具包JSON 响应、缓存操作简化代码 -- dependency groupIdcom.alibaba/groupId artifactIdfastjson2/artifactId version2.0.32/version /dependency !-- lombok 依赖简化实体类、工具类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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; /** * 时间窗口单位秒 * 默认为全局配置的时间可单独覆盖如 601分钟1010秒 */ 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共同定义限流规则如 time60、count10 → 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 ConcurrentHashMaplt;String, Integergt; localCache new ConcurrentHashMap(); private final ConcurrentHashMapString, 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; } }步骤6AOP 限流切面创建切面类拦截所有添加了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?phone13800138000请求方式POST测试操作1分钟内连续请求4次测试结果前3次正常返回“短信已发送”第4次返回限流响应code429msg短信发送过于频繁符合预期。测试场景2登录接口限流滑动窗口IP限流请求地址http://localhost:8080/api/auth/login?usernametestpassword123456请求方式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 拦截 限流校验」不侵入业务代码灵活适配单机、分布式等多种场景。如果你在实战中遇到问题如Redis限流不生效、滑动窗口计数不准、分布式限流冲突欢迎在评论区留言交流一起避坑、一起进步别忘了点赞在看收藏三连关注我解锁更多 SpringBoot 实战干货下期再见❤️

更多文章