☰
SpringBoot AOP实战:基于自定义注解的接口限流与频率控制
2026/10/15 3:40:17 网站建设 项目流程

1. 为什么要用AOP做接口频率限制

1.1 业务场景里频率限制到底解决了什么

在SpringBoot项目里,接口频率限制是一个绕不开的话题。我之前负责的一个内部项目,上线过一个公开的查询接口,本来只给特定合作方调用,结果被某第三方平台无意间抓取,每分钟几千次请求直接打到数据库上,CPU一路飙升,最终把正常用户的请求也拖垮了。事后复盘发现,这类问题如果不提前在接口层面做限制,等项目跑起来再处理就非常被动。

频率限制的核心诉求其实很朴素:在单位时间内,同一个调用方对同一个接口最多只能请求N次。超过这个次数,直接拒绝或者进入排队等待。它解决的不只是恶意攻击问题,更多时候是防止业务代码被误用、防止突发流量把下游打挂、防止第三方回调逻辑死循环导致的消息积压。比如短信发送接口,如果不过滤,一次线上bug就能把短信费用烧掉几万块。再比如登录接口,不做频控,撞库攻击迟早找上门。

限流这件事,技术方案很成熟,可以放在网关层做,可以在Nginx做,也可以在业务代码里做。但网关和Nginx往往属于运维或架构层面,改起来要动配置、要发布,而且很多项目根本没有单独网关。这时候,在SpringBoot应用内部用AOP切面做限流,就成了一个既灵活又轻量的选择。它的好处是:对业务代码几乎零侵入,某个接口想加限制,加一个注解就搞定,想改阈值,改注解参数就行,完全不用动Controller里面的逻辑。

1.2 过滤器和拦截器为什么不太合适

很多初学者会问,SpringBoot里不是有Filter和Interceptor吗,为什么还要用AOP?这三个东西确实都能在请求进入Controller之前做点事,但侧重点完全不同。

Filter是Servlet层面的,它拿到的只是HttpServletRequest和HttpServletResponse,能做的事比较底层,比如设置编码、做跨域处理。Interceptor是Spring MVC层面的,它能访问HandlerMethod,可以做登录校验、日志记录。但这两个方案都有一个共同的麻烦:它们拦截的是请求,而不是具体的业务方法。如果想做细粒度的限制,比如“这个方法每10秒最多5次”“这个方法的某个参数值每分钟最多2次”,Filter和Interceptor就得靠URL去匹配,然后自己解析参数,耦合度高,代码也不容易复用。

AOP切面则直接作用在业务方法上。通过切点表达式和自定义注解,我可以精确地把限流规则贴到某个Service方法或Controller方法上。而且AOP能拿到方法执行的上下文,包括参数、返回值、甚至异常信息,做限流的时候可以做得非常细。我用@Aspect配合自定义注解做出来的效果,基本就是“方法级限流”,哪个方法需要保护,就在方法头上挂一个注解,规则写在注解参数里,可读性和维护性都更好。

另外还有一点,Filter和Interceptor的注册顺序、路径匹配有时候会折腾半天,特别是指定多个拦截器的时候,执行顺序一旦搞错,排查起来很痛苦。AOP的切面可以用@Order控制执行顺序,逻辑清晰,和业务代码完全解耦。所以我的结论是:简单粗暴的全局限流可以上Filter,但追求精细控制和可维护性的接口限流,AOP方案明显更合适。

2. 环境准备与AOP核心概念

2.1 引入依赖和项目环境

先交代一下我这次用的环境,算是比较常规的组合:SpringBoot 2.7.x,JDK 1.8,Maven管理依赖。AOP本身不是SpringBoot内置的,需要单独引入spring-boot-starter-aop这个starter,它会把AspectJ相关的依赖一并带进来。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

如果是Gradle项目,对应加这一行:

implementation 'org.springframework.boot:spring-boot-starter-aop'

引入之后,SpringBoot会自动启用AOP支持,不用再手动加@EnableAspectJAutoProxy注解。不过这里有个细节需要留意:如果项目里自己配置了@EnableAspectJAutoProxy或者关掉了代理设置,切面可能不会生效,所以遇到切面莫名其妙不执行的情况,先检查这两个位置。

依赖引入之后,我用一个最简单的切面来验证环境是否正常。定义一个类,加上@Aspect和@Component注解,写一个空方法加上@Before切点,然后在任意一个接口上打上日志。如果请求进来能打印出日志,说明AOP环境没问题,可以往下继续了。

2.2 几个绕不开的AOP概念

AOP对新手来说最劝退的就是一堆术语:切面、切点、通知、连接点、代理。我当初学的时候也是绕了半天才理清楚。这里用大白话解释一遍,后面看代码就不懵了。

连接点(JoinPoint)就是“程序执行过程中的某个位置”,对于Spring AOP来说,通常就是指某个方法的执行。切点(Pointcut)是“一组连接点的集合”,它通过表达式来匹配哪些方法需要被拦截,相当于一个过滤器。通知(Advice)是“在切点匹配到的方法上执行的逻辑”,分为前置通知、后置通知、环绕通知、异常通知和最终通知五种。切面(Aspect)是“切点+通知”的集合体,一个类上标了@Aspect,它就承载了某个横切逻辑的完整定义。

这里面最常用、也最强大的就是环绕通知@Around。为什么选择它做限流?因为@Around可以在方法执行之前做检查,如果发现请求超限,直接抛出异常或者返回一个降级结果,完全不让目标方法执行;如果没超限,就正常调用proceed()放行。相当于我在方法的入口加了一道安检闸门。相比之下,@Before只能在方法执行前做点事,但没法阻止方法继续执行(除非抛异常),@AfterReturning和@AfterThrowing又只能处理结果,都不适合做限流这种需要“先决定要不要放行”的场景。

还有一个不得不说的概念是代理。Spring AOP默认用的是动态代理,如果目标类实现了接口,就用JDK动态代理;如果没有实现接口,就用CGLIB代理。代理对象会包在真实对象外面,调用方法时先经过代理逻辑,再进入真实方法。这一点很关键,因为后面会提到一个经典大坑:类内部方法自调用时,代理不生效,切面被绕过了。

3. 基于自定义注解的限流切面完整实现

3.1 自定义注解的设计思路

真正好用的限流方案,不应该让使用方去理解切点表达式,而是提供一个业务友好的注解。调用方只需要关心三件事:时间窗口多长、最多允许多少次、超限后怎么反馈。所以我设计了一个RateLimit注解,核心字段如下。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { /** * 时间窗口,单位秒,默认1秒 */ int window() default 1; /** * 窗口内最大请求次数 */ int limit() default 10; /** * 限流后的提示信息 */ String message() default "请求过于频繁,请稍后再试"; }

@Target(ElementType.METHOD)表示这个注解只能贴在方法上,防止误用。@Retention(RetentionPolicy.RUNTIME)必须配成RUNTIME,因为切面在运行时需要通过反射读取注解属性。这两个元注解缺一不可,如果只写@Target不写@Retention,默认是CLASS级别,运行时反射拿不到,切面就没办法读参数了。

除了上面三个基本字段,实际项目里往往还需要一个key字段,用来区分不同的限流维度。比如按用户限制和按IP限制,同样的注解却要表达不同的含义。我通常会在注解里再加一个String key(),让调用方自己说明限流的维度,比如"user"、"ip"、"phone",切面里再根据key去取具体的标识。

3.2 滑动窗口计数器的实现原理

限流算法有很多种,令牌桶、漏桶、固定窗口、滑动窗口。我这次选的是滑动窗口计数器,原因很简单:实现不复杂,又能解决固定窗口的临界突刺问题。

固定窗口的思路是:把时间切成固定长度的小段,每个小段内维护一个计数器。比如限制“1秒内最多10次请求”,就每1秒清空一次计数器。它的缺陷在于:如果请求在第一个窗口的最后100毫秒来了10次,又在下一个窗口的开始100毫秒来了10次,那么在这200毫秒内其实通过了20次请求,超出了原本设定的每秒10次限制。这就是临界突刺问题,不够平滑。

滑动窗口的思路则是:记录每个请求到达的时间戳,只保留当前时间往前一个窗口内的请求记录,然后统计数量。比如窗口是1秒,我存下来最近1秒内的所有请求时间戳,每次新请求来的时候,把窗口之外的时间戳清掉,再数一数剩下的有多少个。如果小于阈值就放行并记录,否则拒绝。这样任意一个时间点往前看一个窗口,请求数都不会超过阈值,有效避免了临界突刺。

滑动窗口的劣势是内存占用略高,因为要存时间戳列表。但在我这个场景里,每个限流key维护一个LinkedList,存几十上百个时间戳完全没有压力,可以忽略不计。如果对性能要求极高,可以考虑用Redis的ZSet来实现分布式滑动窗口,后面会单独说。

3.3 切面类完整代码

下面直接贴上我最终落地的切面代码。这个切面用了一个本地缓存来维护每个限流key的请求时间戳列表,为了避免并发问题,使用了ConcurrentHashMap,并且对每个key的LinkedList做了synchronized加锁。

@Aspect @Component public class RateLimitAspect { /** * 缓存限流记录,key为"类名#方法名#限流维度值",value为时间戳列表 */ private static final Map<String, LinkedList<Long>> RECORD_MAP = new ConcurrentHashMap<>(); @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = buildKey(joinPoint, rateLimit.key()); // 获取当前窗口内的请求记录 LinkedList<Long> records = RECORD_MAP.computeIfAbsent(key, k -> new LinkedList<>()); synchronized (records) { long currentTimeMillis = System.currentTimeMillis(); long windowStart = currentTimeMillis - rateLimit.window() * 1000L; // 清除窗口之外的过期记录 while (!records.isEmpty() && records.peekFirst() < windowStart) { records.removeFirst(); } // 判断是否超限 if (records.size() >= rateLimit.limit()) { throw new RuntimeException(rateLimit.message()); } // 记录本次请求时间 records.addLast(currentTimeMillis); } // 放行目标方法 return joinPoint.proceed(); } private String buildKey(ProceedingJoinPoint joinPoint, String key) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodKey = signature.getDeclaringTypeName() + "#" + signature.getName(); return methodKey + "#" + key; } }

这段代码有几个点值得展开说。

第一,切点表达式用的是@annotation(rateLimit),它会把当前方法上的RateLimit注解对象直接绑定到环绕通知的第二个参数上,不需要额外反射获取,这也是Spring AOP里的一个便捷技巧。在@Around方法里,第一个参数必须是ProceedingJoinPoint,第二个参数就是切点表达式绑定的注解,顺序不能反。

第二,buildKey方法里加上了类名和方法名,是为了防止不同接口上的限流记录互相干扰。比如两个接口都用了limit=10的注解,但它们应该有各自的计数,如果key只用限流维度值,就会出现A接口请求了10次,B接口请求直接被拒绝的错误情况。我见过网上很多简化版代码没注意这一点,实际使用时就会踩坑。

第三,整个检查逻辑放在synchronized代码块里,是为了保证并发安全。因为records是共享对象,多个线程同时进来时如果不加锁,就会出现“最后1个名额被两个人同时抢到”的竞态问题。这里锁粒度已经控制在单个key上,不会影响其他key的限流,性能损耗可以接受。

第四,超限之后直接抛RuntimeException,可以让全局异常处理器统一捕获并返回友好提示。如果项目里有自定义的全局异常处理,建议针对这个场景定义一个RateLimitException,专门用来做限流降级,返回HTTP 429状态码会更规范。如果没有全局异常处理,也可以改为return一个Result对象,不过这要求目标方法的返回值统一,不够灵活。

3.4 参数扩展与灵活配置

上面代码里key()字段我留了口子,但还有一个问题:同一个接口不同用户请求时,怎么区分?切面必须能从请求上下文里拿到当前用户的标识。这里提供一个通用的做法:定义key的解析策略接口,由业务方自行实现。

public interface LimitKeyResolver { /** * 根据当前请求上下文解析限流key */ String resolve(); }

比如按用户限流时,实现类从SecurityContext或Session里拿用户ID;按IP限流时,实现类从RequestContextHolder拿当前请求的IP。我个人建议把IP获取的逻辑单独抽出来,因为实际部署到Nginx后面时,remoteAddr拿到的往往是Nginx的地址,得通过X-Forwarded-For请求头才能拿到真实IP。这块处理很考验经验,后面在常见问题里我会专门写。

如果不想引入策略接口,也可以用SpEL表达式来指定key的取值来源。比如注解可以加一个String keyBy(),值为"#request.getRemoteAddr()",切面里通过SpEL解析参数。但这种方式耦合了参数名和表达式语法,写起来很绕,调试也不方便。我更推荐策略接口,简单直接。

限流维度拓展到接口后,还有一个很容易被忽略的点:限流阈值是否应该支持动态配置?把limit写死在注解里,意味着调整限流阈值必须改代码重新发布。如果业务上经常调整接口的频控策略,建议把limit做成外部配置,注解里的值当作默认值,切面读取时优先取配置中心的值。我之前就在一个项目里把重点接口的限流阈值放到了配置中心,活动开始时调高、活动结束后调低,不用发版,很方便。

4. 在接口上落地使用与测试验证

4.1 在业务方法上引入注解

到了这一步,使用方就非常无感了。在任意需要限流的接口上,加上@RateLimit注解即可。

@RestController public class UserController { @GetMapping("/api/user/info") @RateLimit(window = 1, limit = 2, message = "操作太频繁,请稍后再试") public Result<UserInfo> getUserInfo(@RequestParam String userId) { return Result.success(userService.getUserInfo(userId)); } }

这个例子表示,这个接口在1秒内最多允许2次请求,超出的直接抛出异常。如果你用浏览器快速刷新它,第二次之后就会看到限流提示。

下面再展示一个带key维度的例子。假设业务要求每个用户每分钟最多发5次验证码,可以这样写:

@GetMapping("/api/captcha/send") @RateLimit(key = "userId", window = 60, limit = 5, message = "发送太频繁,60秒后再试") public Result<Void> sendCaptcha(@RequestParam String userId) { return Result.success(); }

配合自定义的LimitKeyResolver实现,从请求参数里拿到userId作为限流标识,就能实现按用户维度的频控。我这里故意用接口来演示,实际业务中更建议把限流注解用在Service层,尤其是那些被多个入口调用的核心方法。因为如果只限制Controller,绕过Controller直接调Service的话,限流就形同虚设了。

4.2 三种常用的测试验证方式

写完限流切面,一定要验证,不能只在浏览器里点几下就算完。我通常会分三步来测。

第一步,用浏览器或Postman手动测试。连续请求接口,观察第N+1次请求是否被拦截。注意要开无痕窗口或者换浏览器,排除浏览器缓存影响。这一步能快速验证注解和切面是否生效。

第二步,用命令行工具做简单压测。比如用ab(Apache Bench)模拟100个请求,看成功数和失败数是否符合预期。假设接口限流是1秒2次,跑100个并发请求,最终成功数应该只有2个。

ab -n 100 -c 10 http://localhost:8080/api/user/info?userId=123

第三步,写一个简单的循环脚本,验证时间窗口滑动效果。比如限制10秒内最多3次,用sleep命令控制请求间隔,观察窗口的滚动行为。这一步主要是为了让滑动的逻辑在时间维度上得到验证。

我自己在实际验证中还发现一个值得记录的现象:本地用Postman测试时,快速点击的成功数有时会多于限流值。这个问题不是切面逻辑错了,而是Postman在短时间内发出了并发请求,而synchronized锁保证了同一时刻只有一个线程在更新records,但限流判断和业务执行是分离的,所以如果并发请求都在业务方法执行中,就会出现“放行后立刻又被下一个请求放行”的错觉。实际上,只要同步块内的size判断正确,最终能通过的数量一定不会超过limit。

4.3 常见问题与排查技巧实录

这个方案我自己用了很久,也在不同的项目里反复踩过坑。下面把最典型的几个问题列出来,每个都是实战中真实遇到过的。

问题1:切面完全不生效,方法还是能无限调用

首先检查切面类上有没有加@Component,没有的话Spring容器根本不会把它管理起来。其次检查启动类或配置类上有没有多余的@EnableAspectJAutoProxy设置,有时候显式配置反而会覆盖默认值。再检查切点表达式是否正确,@annotation(rateLimit)里的参数名必须和方法参数一致,否则匹配不上。最后,确认Controller方法是否被Spring管理,如果直接在类上new出来的对象,AOP是不会处理它的。

问题2:内部方法自调用导致限流失效

这是最隐蔽的一个坑。假设Controller里的A方法调用了同一个类里的B方法,B方法上加有@RateLimit注解,结果是:限流不会生效。原因在于Spring AOP基于代理机制,外部调用Controller时,实际调用的是代理对象的方法,代理会拦截这个调用并执行切面逻辑。但在A方法内部调用B方法时,调用的是this对象的B方法,也就是原始对象的方法,根本没有经过代理,切面自然被绕过。

解决方案有三种:第一,把需要限流的方法拆到另一个Service类里,通过Spring注入的Bean去调用;第二,在类内部注入自身,通过代理对象调B方法;第三,如果项目用了AspectJ的编译期织入,可以解决自调用问题,但配置成本较高,不推荐。

问题3:部署多个实例时,限流变成“各自为政”

这个方案默认是单机限流,每台机器上的内存缓存是独立的。如果服务部署了3个实例,每个实例都允许10秒内5次请求,那实际上10秒内最多能进来15次请求,限流效果就大打折扣了。对于要求严格的场景,需要把限流计数统一放到外部存储,比如Redis。用Redis做滑动窗口,核心思路是用ZSet,key是限流维度标识,value是毫秒时间戳,每次请求时把窗口外的时间戳删掉,再统计ZCard判断是否超限。

Redis版本的核心代码我贴一下,思路和本地版本完全一致,只是存储从内存换成了Redis。

public boolean isAllowed(String key, int window, int limit) { long currentTime = System.currentTimeMillis(); long windowStart = currentTime - window * 1000L; String redisKey = "rate:limit:" + key; // 移除窗口外的时间戳 redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart); Long count = redisTemplate.opsForZSet().zCard(redisKey); if (count != null && count >= limit) { return false; } redisTemplate.opsForZSet().add(redisKey, currentTime, currentTime); redisTemplate.expire(redisKey, window, TimeUnit.SECONDS); return true; }

Redis方案要注意两点:一是ZSet的score和value都必须唯一,直接存储时间戳即可,不用担心重复;二是加上过期时间,防止长期不活跃的限流key堆积在Redis里占用内存。另外,由于Redis操作本身不是原子的,建议用Lua脚本封装整个判断和写入过程,避免并发下面出现超卖。

问题4:获取不到真实用户IP

当服务通过Nginx反向代理时,request.getRemoteAddr()拿到的是Nginx的IP,所有用户都长一个样,按IP限流会误伤所有人。正确的做法是从X-Forwarded-For请求头里取IP,并且注意它可能是多个IP拼接,取逗号分割后的第一个。但这个头是可以被客户端伪造的,更可靠的方式是在Nginx层配置real_ip模块,或者在网关把可信的IP写到某个专用请求头中。一个妥协方案是:优先取X-Forwarded-For的第一个IP,如果为空则取remoteAddr,同时对接入层做信任配置。这块没有银弹,要根据自己的部署架构调整。

问题5:限流阈值和并发量不匹配,出现误限流

如果业务方法的平均执行时间接近窗口大小,而且峰值并发量刚好卡在阈值附近,就会出现正常用户被拒绝的情况。比如接口平均耗时800ms,限流“5秒内10次”,同一个用户连续提交表单时,第二次请求进来可能第一次还没处理完,计数器已经到10了。这种情况需要在业务层面区分“并发中的请求”和“已完成的历史请求”,如果确实需要,可以考虑信号量或并发锁方案。通常在有限流需求的接口上,阈值都会适当放宽到峰值的2-3倍,或者结合令牌桶算法的平滑能力来缓解。

问题6:全局异常处理捕获不了限流异常

很多项目会写一个@RestControllerAdvice的全局异常处理器,统一返回JSON。但限流切面抛出的RuntimeException,如果异常处理器只处理具体类型的异常,可能会漏掉限流异常,直接返回500。建议定义一个RateLimitException,然后在全局异常处理器中专门处理它,返回429状态码和对应的提示信息。如果不想定义独立异常类,直接把message用业务错误码包装一下也可以,但状态码务必和普通服务器错误区分开,方便前端做统一提示和降级。

5. 方案对比与适用场景分析

5.1 令牌桶、漏桶和滑动窗口怎么选

刚才反复提到限流算法,这几个概念经常被混在一起说,我简单梳理一下各自的脾气。

令牌桶算法的逻辑是:系统以恒定速率生成令牌放入桶中,桶满则丢弃。每个请求需要消耗一个令牌,有令牌就放行,没令牌就等待或拒绝。它的特点是允许一定程度的突发流量,只要桶里有累积的令牌,突发请求就能瞬时通过。适合需要处理突发场景的接口,比如秒杀活动前几秒的流量洪峰。

漏桶算法的逻辑是:请求像水一样注入桶中,桶底部以固定速率漏水。不管请求进来多猛,出去的速率始终恒定。它追求的是平滑流出,不允许突发,适合下游处理能力有限的场景,比如调用外部接口时要控制调用速率。

滑动窗口计数器前面已经详细说了,它兼顾了固定窗口的简单和一定程度的平滑性,适合绝大多数业务场景。从严格意义上说,滑动窗口并不是真正的平滑限流,它只是把时间分得更细。但如果窗口足够小,比如1秒或500毫秒,实际效果已经很接近需求了。

我的个人经验是:业务接口限流优先用滑动窗口,因为它实现直观、容易定制“按用户限流”“按IP限流”,扩展性强。如果做网关层的全局流量管控,才考虑令牌桶或漏桶。很多人在初期容易过度设计,我觉得限流方案的选择应该跟着业务复杂度走,别一上来就上Sentinel这种重框架。

5.2 单机内存版和Redis分布式版如何取舍

这个取舍说白了就是三个维度:部署形态、精度要求、维护成本。

如果项目是单实例部署,或者多实例但限流阈值本身就有较大冗余,直接用本地内存版就够了。好处是零额外依赖、性能极高,毕竟就是往内存里加几个数字。坏处是每个实例各自计数,无法做到全局唯一。

如果项目是多实例部署,而且限流规则需要精确到“整个集群每10秒最多100次”,那就必须上Redis。Redis版本要注意网络延迟的影响,虽然ZSet操作加上Lua脚本已经很轻量,但每次请求多一次网络IO,对超高并发接口还是会有性能损耗。这种情况下可以考虑本地计数+定期间步的折中方案,不过实现复杂度会明显上升。

还有一种折中做法:热点接口的限流用本地内存,兜底的全局限流用Redis。比如验证码接口,先用本地内存挡掉大部分恶意请求,再在Redis里做一层全局熔断。我在一个活动项目中就这么干的,效果不错,既保证了性能,又兼顾了全局一致。

5.3 这套方案适合什么项目,不适合什么项目

适合的是中小型SpringBoot项目、内部管理系统、没有单独网关的服务端应用。这类项目的限流需求通常集中在几个核心接口上,用注解+切面已经完全够用,而且代码侵入极小,团队成员看几行代码就明白用法。

不适合的是已经上了完整微服务治理体系的架构,比如已经有Spring Cloud Gateway、Sentinel、或云厂商自带的API网关。这种情况下,网关注入全局限流策略会更统一,业务代码里再做一层AOP限流收益不大,反而增加维护成本。另外,如果对限流的实时性和持久化有严格要求,比如需要精确到秒级的全局限流并做监控报表,还是应该用专业的流量治理组件,而不是自己造轮子。

个人建议的判断标准是:如果团队在10人以内,项目规模不大,根本没有专职的架构师去维护网关,那自己用AOP实现一套轻量限流是非常划算的选择,成本低、见效快,出了问题也好排查。

附:关于这套方案的几点后续扩展思路

前面把核心实现和常见问题讲完了,最后想再聊几个我在实际项目里沉淀的思路,都是基于这套方案的延伸。

第一点是限流数据的可视化。目前限流发生的时候只是抛出异常,如果想知道哪个接口被限流了多少次、来自哪些IP,可以考虑在切面里把每次限流事件发到消息队列或者直接打进日志系统,配合监控告警。我后来在项目里就是每次触发限流时增加一条结构化日志,再通过日志平台统计,效果很好。这一步单独拎出来做成本不高,但运维价值很大。

第二点是限流规则支持热更新。前面提到可以把limit字段的值抽到配置中心,更进一步,可以把整个限流策略都配置化。比如接口路径作为key,对应的限流策略存到数据库或配置中心,切面动态读取。这样做的好处是运营人员不用发版就能调整所有接口的频控策略。不过要注意,配置化之后一定要加缓存和本地兜底,防止配置中心挂掉导致限流失效。

第三点是结合动态代理做更灵活的限流点。@annotation切点只能匹配被注解标注的方法,如果有一天需要把限流规则挂到包级别、类级别上,AOP的切点表达式同样支持。比如execution(* com.xxx.service..*(..))可以拦截整个service包下所有方法,这种场景适合做整层服务的统一风控。切面逻辑不用改,只需要调整切点定义。这个扩展能力也是我坚持用AOP方案而不是Filter方案的原因之一。

最后说一点心得。限流不是写个注解就完事的设计,它最重要的价值是让系统在异常流量面前有存活能力。我见过很多项目把限流写成了“装饰品”,设置了很大的阈值却从没触发过,或者根本没有日志,限流逻辑已经失效了也不知道。个人建议是,限流上线后一定要配合压测去验证,并且在监控面板上能实时看到每个接口的限流触发次数。没有数据的限流,等于没限流。如果你正准备在项目里做接口频率限制,希望这篇内容能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询