前面我们用 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接口限流的资料请关注代码网其它相关文章!
发表评论