📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 写在前面的话
- 接口限流方案
- 设计先行
- 实战方案
- Step1、定义自定义注解
- Step2、加载规则注解
- Step3、限流规则加载
- Step4、定义限流拦截类
- Step5、利用切面检测限流效果
- 开发使用
- 策略切换
- 总结陈词
写在前面的话
接口限流是一种控制应用程序或服务访问速率的技术措施,主要用于防止因请求过多导致系统过载、响应延迟或服务崩溃。在高并发场景下,合理地实施接口限流对于保障系统的稳定性和可用性至关重要。
本篇文章介绍一下在框架封装过程中,如何优雅的实现接口限流方案,希望能帮助到大家。
技术栈:后端 SpringCloud + 前端 Vue/Nuxt
关联文章 - 程序猿入职必会:
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》
《程序猿入职必会(2) · 搭建具备前端展示效果的 Vue》
《程序猿入职必会(3) · SpringBoot 各层功能完善 》
《程序猿入职必会(4) · Vue 完成 CURD 案例 》
《程序猿入职必会(5) · CURD 页面细节规范 》
《程序猿入职必会(6) · 返回结果统一封装》
《程序猿入职必会(7) · 前端请求工具封装》
《程序猿入职必会(8) · 整合 Knife4j 接口文档》
《程序猿入职必会(9) · 用代码生成器快速开发》
《程序猿入职必会(10) · 整合 Redis(基础篇)》
相关博文 - 学会 SpringMVC 系列
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《学会 SpringMVC 系列 · 剖析入参处理》
《学会 SpringMVC 系列 · 剖析出参处理》
《学会 SpringMVC 系列 · 返回值处理器》
《学会 SpringMVC 系列 · 消息转换器 MessageConverters》
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》
接口限流方案
设计先行
先确定一下要实现的效果,再开始编码工作。
限流操作要尽可能灵活,那可以做到控制器方法的层面。
同时,又要支持多个参数组合。那可以考虑自定义注解的方式。
最好还可以支持多种限流策略,那可以选择使用条件注解配置的方式。
实战方案
Step1、定义自定义注解
这步骤没什么特殊的,定义一个限流注解,方便添加。
一些和限流相关的参数考虑进去。
java">@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {/*** 限流名称,例如 TestLimit*/String value() default "";/*** 指定时间内允许通过的请求数*/int count();/*** 限流时间,单位秒*/int durationSeconds();/*** 限流模式*/MetricType metricType() default MetricType.TYPE_REQUEST_AMOUNT;/*** 限流消息提示*/String failureMsg() default "";enum MetricType {/*** 直接拒绝*/TYPE_REQUEST_AMOUNT}}
Step2、加载规则注解
可以借助 SpringBoot 的初始化事件监听机制,在项目启动的时候完成这个动作。
部分示例代码如下,主要逻辑为:
1、找出所有控制器接口方法;
2、过滤出存在 RateLimit 注解的方法;
3、构建为限流实体 RateLimitStrategy;
4、调用具体策略类,注册生效这些规则;
java">public void load() {log.info("开始加载限流规则");List<RateLimitStrategy> rules = SpringUtil.getRequestMappingHandlerMappingBean().getHandlerMethods().entrySet().stream().filter(e -> !e.getKey().getPatternsCondition().getPatterns().isEmpty()).filter(e -> e.getValue().hasMethodAnnotation(RateLimit.class)).map(e -> {HandlerMethod handlerMethod = e.getValue();RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);String resourceId = StrUtil.isBlank(rateLimit.value())? MethodUtil.getMethodSign(handlerMethod.getMethod()): rateLimit.value();return createRule(rateLimit, resourceId);}).collect(Collectors.toList());log.info("共找到{}条规则,开始注册规则...", rules.size());this.rateLimitRuleRegister.registerRules(rules);log.info("限流规则注册完成");
}private static RateLimitStrategy createRule(RateLimit rateLimit, String resourceId) {RateLimitStrategy.MetricType metricType = RateLimitStrategy.MetricType.valueOf(rateLimit.metricType().name());return RateLimitStrategy.newBuilder().setName(resourceId).setMetricType(metricType).setThreshold(rateLimit.count()).setStatDuration(rateLimit.durationSeconds()).setStatDurationTimeUnit(TimeUnit.SECOND).setLimitMode(RateLimitStrategy.LimitMode.MODE_LOCAL).build();
}
Step3、限流规则加载
前面提到加载完成后,开始注册规则。
这里先以 Sentinel 为例实现限流策略加载,自定义 SentinelRateLimitRuleRegister 实现 RateLimitRuleRegister 接口的 registerRules 方法。
这里预留了 RateLimitRuleRegister 接口,是为后续策略切换留下扩展方式。
java">public class SentinelRateLimitRuleRegister implements RateLimitRuleRegister {@Overridepublic void registerRules(List<RateLimitStrategy> rateLimitStrategies) {if (rateLimitStrategies.isEmpty()) {return;}Map<RateLimitStrategy.MetricType, List<RateLimitStrategy>> ruleMap = rateLimitStrategies.stream().collect(Collectors.groupingBy(RateLimitStrategy::getMetricType));// 暂时只考虑支持流控规则List<FlowRule> flowRules = ruleMap.get(RateLimitStrategy.MetricType.TYPE_REQUEST_AMOUNT).stream().map(rateLimitStrategy -> {double threshold = rateLimitStrategy.getThreshold() * 1.0 / rateLimitStrategy.getStatDuration();FlowRule flowRule = new FlowRule();// 资源名,资源名是限流规则的作用对象flowRule.setResource(rateLimitStrategy.getName());// 限流阈值类型,QPS 或线程数模式,这里使用 QPS 模式flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);// 限流阈值flowRule.setCount(threshold <= 1 ? 1 : threshold);// 单机模式flowRule.setClusterMode(false);// 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流,默认直接拒绝flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);return flowRule;}).collect(Collectors.toList());if (!flowRules.isEmpty()) {FlowRuleManager.loadRules(flowRules);}}
}
Step4、定义限流拦截类
还是以 Sentinel 为例说明,这里预留ApiRateLimiter接口,也是为后续扩展准备。
java">public class SentinelApiRateLimiter implements ApiRateLimiter {@Overridepublic boolean accept(String resourceId) {boolean entry = SphO.entry(resourceId);if (entry) {SphO.exit();}return entry;}
}
Step5、利用切面检测限流效果
定义一个切面,对包含 RateLimit 注解的方法生效,调用相应限流策略类,执行其 accept 方法,看是否正常。
java">@Aspect
@RequiredArgsConstructor
@Slf4j
public class ApiRateLimitAspect {private final ApiRateLimiter apiRateLimiter;private final RateLimitFailureResultProvider rateLimitFailureResultProvider;@Around("@annotation(rateLimit)")public Object rateLimitAspect(ProceedingJoinPoint proceedingJoinPoint, RateLimit rateLimit) throws Throwable {// 规则资源IDString resourceId;// 优先使用注解上的资源ID,如果注解上没有配置资源ID,则使用方法签名作为资源IDif (StrUtil.isBlank(rateLimit.value())) {Signature signature = proceedingJoinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method targetMethod = methodSignature.getMethod();resourceId = MethodUtil.getMethodSign(targetMethod);} else {resourceId = rateLimit.value();}boolean accept;try {// 尝试获取令牌accept = this.apiRateLimiter.accept(resourceId);} catch (Exception e) {accept = true;log.error("[RateLimit] 限流异常: {}", e.getMessage());}if (!accept) {String failureMessage = this.rateLimitFailureResultProvider.getFailureMessage(rateLimit.failureMsg());throw new RateLimitException(failureMessage);}return proceedingJoinPoint.proceed();}
}
开发使用
上面的若干步骤,是由框架层面封装的。
针对具体开发人员,使用起来就简单多了。
Step1、选择需要限流的控制层方法,添加@RateLimit注解,下方代表该接口每秒最多只能被调用2次。
java">@RateLimit(count = 2, durationSeconds = 1)
@RequestMapping(value = "/simple3")
public ResultVO simple3() throws Exception {return ResultVO.success("简单测试接口成功");
}
Step2、启动项目,高频访问该接口,会提示报错信息。
{"code":"10100","data":"","message":"请求过于频繁,请稍后再试!","error":"","traceId":"fbc8590f4038347c","guide":""}
策略切换
前面示例可以看到,很多 Sentinel 的策略逻辑,都预留了接口,这个也是为后续扩展策略准备的。
如果还想使用其他模式实现限流,例如 Guava 方式,那可以利用自动配置类 + 条件注解的模式实现。
部分代码如下:
java">@SuppressWarnings("UnstableApiUsage")
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RateLimiter.class)
@ConditionalOnProperty(prefix = OnelinkRateLimitProperties.PREFIX, name = "module", havingValue = "guava")
static class GuavaRateLimitAutoConfiguration {@Beanpublic GuavaApiRateLimiter guavaApiRateLimiter() {return new GuavaApiRateLimiter();}@Beanpublic RateLimitRuleRegister guavaRateLimitRuleRegister(GuavaApiRateLimiter guavaApiRateLimiter) {return new GuavaRateLimitRuleRegister(guavaApiRateLimiter);}
}@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SphO.class)
@ConditionalOnProperty(prefix = OnelinkRateLimitProperties.PREFIX, name = "module", havingValue = "sentinel", matchIfMissing = true)
static class SentinelRateLimitAutoConfiguration {@Beanpublic SentinelApiRateLimiter sentinelApiRateLimit() {return new SentinelApiRateLimiter();}@Beanpublic RateLimitRuleRegister sentinelRateLimitRuleRegister() {return new SentinelRateLimitRuleRegister();}}
总结陈词
此篇文章介绍了关于限流方案的封装,上方提供的是部分代码,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。