首页 > 其他分享 >限流 SDK 的设计与实现

限流 SDK 的设计与实现

时间:2024-03-18 17:36:23浏览次数:14  
标签:窗口 请求 次数 限流 注解 设计 redisTemplate SDK

需求分析

请设计一套 SDK,用于实现接口限流,针对某个 IP 对于特定接口方法的单位时间访问次数进行控制。

  • 限流算法:滑动窗口
  • 可配置项
    • 时间窗口
    • 限流次数

实现思路

算法知识补充

通过滑动窗口实现限流

思想源于计数器(单位时间内数量超过阈值时拒绝请求),但是引入了滑动窗口,相较于固定窗口,更新过程更为平滑,不会出现临界问题(即在更新时刻前后快速涌入流量,不能防止短期流量剧增,却又导致长期流量受控)。

具体实现

SDK 结构
  • annotation:自定义注解
    • RequestLimit:标识限流接口,支持属性配置(时间窗口、限流次数)
  • aop
    • RequestLimitAspect:限流实现切面,统一实现限流处理
  • common:自定义的 ResponseEntity 的等效实现
    • BaseResponse:自定义统一响应对象
    • ErrorCode:自定义错误码
    • ResultUtils:自定义返回工具
  • config
    • RedisConfig:配置 Redis
  • exception:统一异常处理
    • BusinessException:业务异常,与系统异常做区分
    • GlobalExceptionHandler:全局异常处理器

不加粗部分为基础设施,详情参见 项目学习 鱼皮用户中心

本文详细介绍加粗部分的实现。

Maven 依赖
<!--提供 AOP 支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

<!--提供日志支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<!--提供 @Data-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--提供 Redis 支持-->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.1</version>
</dependency>
代码实现
自定义注解
  • 声明标识的位置 → 修饰方法

  • 声明注解存在的时间 → 运行时仍保留注解

  • 声明注解属性

    • 窗口大小(时间长度)
    • 访问上限
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
    // 限制时间 单位:秒(默认值:一分钟)
    long period() default 60;

    // 允许请求的次数(默认值:5次)
    long count() default 5;
}
切面逻辑

巧用 Redis 的过期策略,可以方便地实现滑动窗口的效果。

每次访问接口后,接口方法执行前:

  1. 记录请求:使用 Redis 中的 ZSet (有序集合)进行记录。

    1. 获取申请请求的 IP 和 URI。

    2. 获取当前时间戳。

    3. 利用 Zet 的添加功能,记录请求。

      1. 设置 Key:将字符串 req_limit_IPURI 的拼接作为 Key。
      2. 设置 Value:将时间戳作为 Value。
      3. 设置 Score:将时间戳作为 Score。
    4. 设置过期时间:安全机制,避免长间隔请求持续占用内存。
      因为窗口控制仅在请求调用时进行,如果长期不调用接口,又不设置过期时间,会导致不必要的内存消耗。

  2. 控制窗口:删除滑动窗口以外的值。

    1. 从注解中获取窗口大小(即时间段长度)
    2. 利用 ZSet 的删除功能,删除滑动窗口以外的值。
  3. 判断当前访问次数是否已经大于限制次数。

    1. 利用 ZSet 的统计功能统计 Key 出现次数,即窗口内 IP 访问 URI 的次数。
    2. 从注解中获取访问次数上限。
    3. 比较访问次数和次数上限,若访问次数超过次数上限,则抛出异常。
@Aspect
@Component
public class RequestLimitAspect {
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    RedisTemplate<String, Long> redisTemplate;

    // 定义切点
    @Pointcut("@annotation(requestLimit)")
    public void controllerAspect(RequestLimit requestLimit) {
    }

    // 织入逻辑
    // 在指定切点周围添加业务逻辑
    @Around("controllerAspect(requestLimit)")
    public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
        // 获取注解中记录的属性
        long period = requestLimit.period(); // 窗口大小
        long limitCount = requestLimit.count(); // 限制次数

        // 引入 ZSet
        ZSetOperations<String, Long> zSetOperations = redisTemplate.opsForZSet();

        // region 记录请求
        // 获取请求:根据参数类型获取 HttpServletRequest
        Object[] args = joinPoint.getArgs();
        HttpServletRequest httpServletRequest = null;
        for (Object arg : args) {
            if (arg instanceof HttpServletRequest) {
                httpServletRequest = (HttpServletRequest) arg;
                break;
            }
        }

        // 从 HttpServletRequest 中获取 IP 和 URI
        // 例:访问 https://www.example.com/products?id=123
        // 假设 www.example.com 对应的 IP 地址为:192.168.1.1
        // getRemoteAddr() → 192.168.1.1
        // getRequestURI → /products?id=123
        String ip = "";
        String uri = "";
        if (httpServletRequest != null) {
            ip = httpServletRequest.getRemoteAddr();
            uri = httpServletRequest.getRequestURI();
            System.out.println(ip);
            System.out.println(uri);
        } else {
            // 没有找到HttpServletRequest参数
            throw new BusinessException(PARAMS_ERROR, "没有找到HttpServletRequest参数");
        }

        // 利用 URI 和 IP 拼接 Key
        String key = "req_limit_".concat(uri).concat(ip);

        // 获取当前时间戳,作为 Value 和 Score
        long currentMs = System.currentTimeMillis();

        // add 参数说明:
        // key:键
        // value:值
        // score :排序权重
        zSetOperations.add(key, currentMs, currentMs);
        // 设置过期时间:安全机制,避免长间隔请求持续占用内存。
        // 即确保内存中的滑动窗口数据不会一直累积,避免内存占用过多。
        // 因为窗口控制仅在请求调用时进行,如果长期不调用接口,又不设置过期时间,会导致不必要的内存消耗。
        redisTemplate.expire(key, period, TimeUnit.SECONDS);
        //endregion

        // region 控制窗口
        // 删除滑动窗口以外的值,根据当前时间和注解中设置的 period 确定窗口大小
        // removeRangeByScore 参数说明:
        // key:表示有序集合的键名。
        // minScore:表示删除范围的最小分数。
        // maxScore:表示删除范围的最大分数。
        zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000);
        //endregion

        // region 判断当前访问次数是否已经大于限制次数
        // 统计当前访问次数
        // zCard 功能说明:获取有序集合中成员的数量。
        // zCard 参数说明:key,表示有序集合的键名。
        Long count = zSetOperations.zCard(key);
        if (count > limitCount) {
            logger.error("接口拦截:{} 请求超过限制频率【{}次/{}s】,IP为{}", uri, limitCount, period, ip);
            throw new BusinessException(FORBIDDEN_ERROR, "请求超过限制频率");
        }
        //endregion

        // 执行用户请求
        return joinPoint.proceed();
    }
}
Redis 配置
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, String> redisTemplate(@Qualifier("jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }

    @Bean
    @Primary
    public JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
        jedisConnectionFactory.setHostName("localhost");
        jedisConnectionFactory.setPort(6379);
        return jedisConnectionFactory;
    }
}

压力测试

Postman vs JMeter

Postman 的 runner 本质上是串行执行多次请求

Jmeter 则是并行执行多个请求

项目源码

基于滑动窗口算法的限流注解实现

参考文档

SpringBoot限制接口访问频率 - 这些错误千万不能犯

JMeter 使用教程

《优化接口设计的思路》系列:第七篇—接口限流策略

标签:窗口,请求,次数,限流,注解,设计,redisTemplate,SDK
From: https://www.cnblogs.com/ba11ooner/p/18081003

相关文章

  • 拌合楼管理系统开发(五) 数据库表和字段的设计方案
    前言:继续闭门造车    今天花时间把前面一段时间思考的整个拌合楼管理系统的数据库实现在mysql中建立起来了.表和字段含义如下了一、数据库表目录序号表名注释/说明1Company往来单位2ContractAttach合同附件3ContractBody合同表体4Contr......
  • 基于PHP+Mysql网上商城购物商城系统设计与实现
     博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育和辅导。所有项目都配有从入门到精通的基础知识视频课程,学习后应对毕业设计答辩。项目配有对应开发文档、开题报告、任务书、P......
  • PFA洗瓶尖嘴设计回流快无残留耐腐蚀洗涤瓶
    产品介绍PFA洗瓶也叫特氟龙洗瓶,Teflon洗瓶,进口聚四氟乙烯洗瓶。用于痕量,超痕量分析,同位素检测,ICP-MS/OES/AAS分析等高端实验。常用规格:60ml100ml250ml300ml500ml1000ml等产品特性1、外观半透明;2、耐高底温:使用温度-200~+260℃;3、耐腐蚀:耐强酸、强碱、王水、氢fu酸等有机......
  • 《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来
    一、前言大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多......
  • 宠物智能喂食机方案设计
    我们都知道,现如今养宠物的人群已经很多了,主要是青年人居多,他们在独自漂泊的在外的工作,免不了情感泛滥,养一些小动物也是在预料之中。但由于工作或者其他各种因数,养宠人不可时时刻刻在家,对于宠物生活上的事不可能一直照料,诸如喂食、饮水、排泄这些宠物每天需要做的。针对这些宠......
  • 基于springboot的在线教育系统的设计与实现
    基于springboot的在线教育系统的设计与实现文章目录基于springboot的在线教育系统的设计与实现引言功能演示视频开发环境系统功能介绍功能对照表功能截图编程框架SpringBoot框架SSM框架vue框架示例代码数据库操作示例源码获取引言博主介绍:✌专注于Java技术......
  • 基于springboot的古典舞在线交流平台的设计与实现
    基于springboot的古典舞在线交流平台的设计与实现文章目录基于springboot的古典舞在线交流平台的设计与实现引言功能演示视频开发环境系统功能介绍功能对照表功能截图编程框架SpringBoot框架SSM框架vue框架示例代码数据库操作示例源码获取引言博主介绍:✌专......
  • CorelDRAW2024中文免费专业平面设计软件,让创意无限飞翔!
    CorelDRAW2024是一款功能强大的专业平面设计软件,它提供了丰富的绘图工具和特效,使用户能够轻松创建各种类型的设计,如图标、海报、宣传册等。无论是从事平面设计、插画、品牌设计还是其他创意领域,CorelDRAW2024都能满足你的需求,帮助你释放无限的创意潜力,让你的设计脱颖而出。......
  • 电气防火限流式保护器在住宅区域的功能与配置是怎样的?
    袁媛ACRELYY安科瑞电气股份有限公司电气防火限流式保护器主要功能功能1.短路保护功能。保护器实时监测用电线路电流,当线路发生短路故障时,能在150微秒内实现快速限流保护,并发出声光报警信号。2.过载保护功能。当被保护线路的电流过载且过载持续时间超过动作时间(3~60秒可......
  • 在可燃性粉尘危险场所安装电气防火限流式保护器的必要性
    袁媛ACRELYY安科瑞电气股份有限公司1.概述所谓的可燃性粉尘环境,是指在大气环境的条件下,粉尘或纤维状的可燃性物质与空气的混合物点燃后,燃烧将传至全部未燃烧混合物的环境。随着现代工业技术的发展,可燃性粉尘的危险场所在不断增多,其危害变得不可避免,相应的粉尘爆炸事故也时......