📢 大家好,我是 【战神刘玉栋】,有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、定义自定义注解
这步骤没什么特殊的,定义一个限流注解,方便添加。
一些和限流相关的参数考虑进去。
@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、调用具体策略类,注册生效这些规则;
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 接口,是为后续策略切换留下扩展方式。
public class SentinelRateLimitRuleRegister implements RateLimitRuleRegister {
@Override
public 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接口,也是为后续扩展准备。
public class SentinelApiRateLimiter implements ApiRateLimiter {
@Override
public boolean accept(String resourceId) {
boolean entry = SphO.entry(resourceId);
if (entry) {
SphO.exit();
}
return entry;
}
}
Step5、利用切面检测限流效果
定义一个切面,对包含 RateLimit 注解的方法生效,调用相应限流策略类,执行其 accept 方法,看是否正常。
@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 {
// 规则资源ID
String resourceId;
// 优先使用注解上的资源ID,如果注解上没有配置资源ID,则使用方法签名作为资源ID
if (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次。
@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 方式,那可以利用自动配置类 + 条件注解的模式实现。
部分代码如下:
@SuppressWarnings("UnstableApiUsage")
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RateLimiter.class)
@ConditionalOnProperty(prefix = OnelinkRateLimitProperties.PREFIX, name = "module", havingValue = "guava")
static class GuavaRateLimitAutoConfiguration {
@Bean
public GuavaApiRateLimiter guavaApiRateLimiter() {
return new GuavaApiRateLimiter();
}
@Bean
public 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 {
@Bean
public SentinelApiRateLimiter sentinelApiRateLimit() {
return new SentinelApiRateLimiter();
}
@Bean
public RateLimitRuleRegister sentinelRateLimitRuleRegister() {
return new SentinelRateLimitRuleRegister();
}
}
总结陈词
此篇文章介绍了关于限流方案的封装,上方提供的是部分代码,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。