《框架封装 · 优雅接口限流方案》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

写在前面的话

接口限流是一种控制应用程序或服务访问速率的技术措施,主要用于防止因请求过多导致系统过载、响应延迟或服务崩溃。在高并发场景下,合理地实施接口限流对于保障系统的稳定性和可用性至关重要。

本篇文章介绍一下在框架封装过程中,如何优雅的实现接口限流方案,希望能帮助到大家。

技术栈:后端 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 {
    @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接口,也是为后续扩展准备。

java 复制代码
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 方法,看是否正常。

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 {

        // 规则资源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次。

java 复制代码
@RateLimit(count = 2, durationSeconds = 1)
@RequestMapping(value = "/simple3")
public ResultVO simple3() throws Exception {
    return ResultVO.success("简单测试接口成功");
}

Step2、启动项目,高频访问该接口,会提示报错信息。

json 复制代码
{"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 {

    @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();
    }

}

总结陈词

此篇文章介绍了关于限流方案的封装,上方提供的是部分代码,仅供学习参考。

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
种树人202408191 小时前
如何在 Spring Boot 中启用定时任务
spring boot
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity3 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq