• 记接口限流令牌桶的学习


           在高并发系统中,存在着巨大的挑战,大流量高并发的访问。一些常见的有天猫的双十一、京东618、秒杀以及延时促销等。短时间内的如此巨大的访问流量往往会给数据库造成巨大的压力,进而影响服务器端的稳定性,那么我们的解决方案包括有:

    1. 前端用nginx做负载均衡;
    2. 对服务器端访问频率较多的查询接口做redis缓存,减小数据库的压力;
    3. 限流

      今天我自己就来学习一下关于如何做到限流。起初我想的是对请求接口做切面处理,获取发送请求端的IP,然后将IP、请求的接口路径、时间等信息存入redis,然后对每一次的请求和redis中存入的信息进行比对,如果IP相同且两次间隔时间很小,那么我们就可以直接返回结果而不进入到controller层,当然也可以存入mysql数据库中,两者区别是,mysql中是持久化的,而redis中会因为服务器挂掉等原因造成数据丢失,当然其实影响也不大,因为限流主要就要保证某一时间点内对访问请求进行限制,所以相关信息存入redis的保存时间不用过长,这些都可以根据实际情况来予以处理。

      然而查阅相关资料,了解到令牌桶,所以就自己实现一下用令牌桶来限流。关于令牌桶的一些知识,有兴趣的可以去了解一下。

      首先,构建springboot项目,我使用的idea2018 +jdk1.8。项目文件目录如图所示:

                          

     接着就是controller层、service层、dao层的代码编写,

      

    import com.white.ratelimiter.service.MassageService;
    import com.white.ratelimiter.service.PayService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.math.BigDecimal;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 功能模块()
     *
     * @Author white Liu
     * @Date 2020/7/31 10:10
     * @Version 1.0
     */
    @RestController
    @RequestMapping("/api")
    @Slf4j
    public class PayController {
        @Autowired
        PayService payService;
        @Autowired
        MassageService massageService;
        /**
         * RateLimiter.create(2)表示以固定速率2r/s,即以每秒2个令牌的速率放至到令牌桶中
         */
        private RateLimiter rateLimiter = RateLimiter.create(1);
        @GetMapping("/pay/get")
        public BaseResult pay(){
            //限流处理,客户端请求从桶中获取令牌,如果在500毫秒没有获取到令牌,则直接走服务降级处理
            boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
            if(!tryAcquire){
                log.info("请求过多,请稍后");
                return BaseResult.error(500,"当前请求过多!");
            }
            int ref = payService.doPay(BigDecimal.valueOf(99.8));
            if(ref>0){
                log.info("支付成功");
                return BaseResult.ok().put("msg","支付成功");
            }
            return BaseResult.error(400,"支付失败,请重新尝试");
        }
    
    }

     启动程序后,用浏览器请求接口,然后再不断快速刷新,页面会显示如下图所示,可见令牌桶限流能够生效!

    下面第二个接口尝试优化代码量,使用自定义注解以及切面类来实现令牌桶限流。

      首先自定义注解:

    package com.white.ratelimiter.annotation;
    
    /**
     * 功能模块(令牌桶限流注解)
     *
     * @Author white Liu
     * @Date 2020/8/1 11:55
     * @Version 1.0
     */
    
    import java.lang.annotation.*;
    
    @Documented
    @Target(value = ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyLimiter {
        //向令牌桶放入令牌的速率
        double rate();
        //从令牌桶获取令牌的超时时间
        long timeout() default 0;
    }

    接着编写切面类,获取使用该自定义注解的接口,然后使用令牌桶来对该接口进行限流

    package com.white.ratelimiter.aspect;
    
    import com.google.common.util.concurrent.RateLimiter;
    import com.white.ratelimiter.annotation.MyLimiter;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 功能模块()
     *
     * @Author white Liu
     * @Date 2020/8/1 11:59
     * @Version 1.0
     */
    @Component
    @Aspect
    @Slf4j
    @Scope
    public class MyRateLimiterAspect {
        @Autowired
        private HttpServletResponse response;//注入HttpServletResponse对象,进行降级处理,返回必要信息提示用户
        
        private RateLimiter rateLimiter = RateLimiter.create(1);
    
        @Pointcut("execution(public * com.white.ratelimiter.controller.*.*(..))")
        public void cutPoint(){}
        @Around("cutPoint()")
        public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
    
            //使用反射获取方法上是否存在@MyRateLimiter注解
            MyLimiter myRateLimiter = signature.getMethod().getDeclaredAnnotation(MyLimiter.class);
            if(myRateLimiter == null){
                //程序正常执行,执行目标方法
                return proceedingJoinPoint.proceed();
            }
            //获取注解上的参数
            //获取配置的速率
            double rate = myRateLimiter.rate();
            //获取客户端等待令牌的时间
            long timeout = myRateLimiter.timeout();
    
            //设置限流速率
            rateLimiter.setRate(rate);
    
            //判断客户端获取令牌是否超时
            boolean tryAcquire = rateLimiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);
            if(!tryAcquire){
                log.info("当前访问太多,请稍后尝试");
                //服务降级
                callback();
                return null;
            }
            //获取到令牌,直接执行
            return proceedingJoinPoint.proceed();
        }
        /**
         * 降级处理
         */
        private void callback() {
            response.setHeader("Content-type", "text/html;charset=UTF-8");
            PrintWriter writer = null;
            try {
                writer =  response.getWriter();
                writer.println("出错了,请重新尝试");
                writer.flush();;
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(writer != null){
                    writer.close();
                }
            }
        }
    }

    然后在接口处添加自定义注解

        @GetMapping("/msg/get")
        @MyLimiter(rate = 1.0,timeout = 500)
        public BaseResult sendMsg(){
            boolean flag = massageService.sendMsg("恭喜您成长值+1");
            if (flag){
                log.info( "消息发送成功");
                return BaseResult.ok("消息发送成功");
            }
            return BaseResult.error(400,"消息发送失败,请重新尝试一次吧...");
        }

    至此,自定义注解限流完成,启动程序,打开浏览器,输入rul,得到下图所示

    尝试不断快速刷新页面,自定义注解会生效,页面会显示

     github代码下载地址:git@github.com:white66/ratelimiter.git

    时光静好,与君语;细水长流,与君同;繁华落尽,与君老!
  • 相关阅读:
    Nginx 集群 反向代理多个服务器
    Nginx 反向代理
    Nginx 图片服务器
    网鼎杯 pwn 记录
    demo.testfire.net 靶场测试流程记录
    靶场测试系列(已办清单)
    Burp Suite插件推荐
    ida不错的插件记录
    0ctf2017-babyheap
    0ctf2018 pwn
  • 原文地址:https://www.cnblogs.com/lyzj/p/13409853.html
Copyright © 2020-2023  润新知