• SpringBoot使用自定义注解+AOP+Redis实现接口限流


    为什么要限流

    系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用。为了避免这种情况,我们就需要对接口请求进行限流。

    所以,我们可以通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统或避免不必要的资源浪费,一旦达到限制速率则可以拒绝服务、排队或等待。 

     

    限流背景

    系统有一个获取手机短信验证码的接口,因为是开放接口,所以为了避免用户不断的发送请求获取验证码,防止恶意刷接口的情况发生,于是用最简单的计数器方式做了限流,限制每个IP每分钟只能请求一次,然后其他每个手机号的时间窗口限制则是通过业务逻辑进行判断。一般一些接口访问量比较大的,可能会压垮系统的,则需要加入流量限制!如:秒杀等...

     

    实现限流

    1、引入依赖

    复制代码
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>        
    复制代码

     

    2、自定义限流注解

    复制代码
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RateLimiter
    {
        /**
         * 限流key
         */
         String key() default Constants.RATE_LIMIT_KEY;
    
        /**
         * 限流时间,单位秒
         */
         int time() default 60;
    
        /**
         * 限流次数
         */
        int count() default 100;
    
        /**
         * 限流类型
         */
        LimitType limitType() default LimitType.DEFAULT;
    
        /**
         * 限流后返回的文字
         */
        String limitMsg() default "访问过于频繁,请稍候再试";
    }
    复制代码

     

    3、限流切面

    复制代码
    @Aspect
    @Component
    public class RateLimiterAspect {
    
        private final static Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    
        @Autowired
        private RedisUtils redisUtils;
    
        @Before("@annotation(rateLimiter)")
        public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
        {
            int time = rateLimiter.time();
            int count = rateLimiter.count();
            long total = 1L;
    
            String combineKey = getCombineKey(rateLimiter, point);
            try
            {
                if(redisUtils.hasKey(combineKey)){
                    total = redisUtils.incr(combineKey,1);  //请求进来,对应的key加1
                    if(total > count)
                        throw new ServiceRuntimeException(rateLimiter.limitMsg());
                }else{
                    redisUtils.set(combineKey,1,time);  //初始化key
                }
            }
            catch (ServiceRuntimeException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                throw new ServiceRuntimeException("网络繁忙,请稍候再试");
            }
        }
    
        /**
         * 获取限流key
         * @param rateLimiter
         * @param point
         * @return
         */
        public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
        {
            StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
            if (rateLimiter.limitType() == LimitType.IP)
            {
                stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-");
            }
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            Class targetClass = method.getDeclaringClass();
            stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
            return stringBuffer.toString();
        }
    
    
    
    }
    复制代码

     

    4、写一个简单的接口进行测试

    复制代码
    @RestController
    public class TestController {
    
        @RateLimiter(time = 60, count = 1, limitType = LimitType.IP, limitMsg = "一分钟内只能请求一次,请稍后重试")
        @GetMapping("/hello")
        public ResultMsg hello() {
            return ResultMsg.success("Hello World!");
        }
    }
    复制代码

     

    5、全局异常拦截

    复制代码
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
        /**
         * 业务异常
         */
        @ExceptionHandler(ServiceRuntimeException.class)
        public ResultMsg handleServiceException(ServiceRuntimeException e, HttpServletRequest request)
        {
            return ResultMsg.error(e.getMessage());
        }
    
        /**
         * 系统异常
         */
        @ExceptionHandler(Exception.class)
        public ResultMsg handleException(Exception e, HttpServletRequest request)
        {
            return ResultMsg.error("系统异常");
        }
    
    }
    复制代码

     

    6、接口测试

    1)第一次发送,正常返回结果

     

    2)一分钟内第二次发送,返回错误,限流提示

     

     

    好了,大功告成啦

    还有其他的限流方式,如滑动窗口限流方式(比计数器更严谨)、令牌桶等...,有兴趣的小伙伴可以学习一下

     

    附源码

    https://gitee.com/jae_1995/ratelimiter

     

  • 相关阅读:
    【JavaScript作用域】
    newstarctf
    数字化转型风险大,企业该如何应对?
    秋招基础整理
    LeetCode 36. 有效的数独
    修改npm全局安装的插件(下载目录指向)
    Android:ChildHelper的Bucket
    2024年测试工程师必看系列之fiddler设置手机端抓包全套教程
    什么是语句?什么是表达式?
    防火墙实验二——实现域间、域内双向NAT、双机热备实验
  • 原文地址:https://www.cnblogs.com/jae-tech/p/16625091.html