• 限流、流量控制方案


    参考资料

    https://blog.csdn.net/xushiyu1996818/article/details/106764890/

    https://zhuanlan.zhihu.com/p/60979444

    https://blog.csdn.net/fubicheng208/article/details/106650146/

    https://blog.csdn.net/weixin_38019299/article/details/120883690

    我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了! (qq.com)

    流量控制

    流量控制在计算机领域称为过载保护。何为过载保护?所谓“过载”,即需求超过了负载能力;而“保护”则是指当“过载”发生了,采取必要的措施保护自己不受“伤害”。在计算机领域,尤其是分布式系统领域,“过载保护”是一个重要的概念。一个不具备“过载保护”功能的系统,是非常危险和脆弱的,很可能由于瞬间的压力激增,引起“雪崩效应”,导致系统的各个部分都同时崩溃,停止服务。这就好像在没有保险丝的保护下,电压突然变高,导致所有的电器都会被损坏一样,“过载保护”功能是系统的“保险丝”。

    接口限流——redis + lua脚本

    主要是借助redis做限流,通过aop或拦截器做限流,利用lua脚步是为了做到原子性。

    Lua脚本和 MySQL数据库的存储过程比较相似,他们执行一组命令,所有命令的执行要么全部成功或者失败,以此达到原子性。也可以把Lua脚本理解为,一段具有业务逻辑的代码块。而Lua本身就是一种编程语言,虽然redis 官方没有直接提供限流相应的API,但却支持了 Lua 脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。

    相比Redis事务,Lua脚本的优点:

    • 减少网络开销:使用Lua脚本,无需向Redis 发送多次请求,执行一次即可,减少网络传输
    • 原子操作:Redis 将整个Lua脚本作为一个命令执行,原子,无需担心并发
    • 复用:Lua脚本一旦执行,会永久保存 Redis 中,,其他客户端可复用

    Lua脚本大致逻辑如下:

    -- 获取调用脚本时传入的第一个key值(用作限流的 key)
    local key = KEYS[1]
    -- 获取调用脚本时传入的第一个参数值(限流大小)
    local limit = tonumber(ARGV[1])
    
    -- 获取当前流量大小
    local curentLimit = tonumber(redis.call('get', key) or "0")
    
    -- 是否超出限流
    if curentLimit + 1 > limit then
        -- 返回(拒绝)
        return 0
    else
        -- 没有超出 value + 1
        redis.call("INCRBY", key, 1)
        -- 设置过期时间
        redis.call("EXPIRE", key, 2)
        -- 返回(放行)
        return 1
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 通过KEYS[1] 获取传入的key参数
    • 通过ARGV[1]获取传入的limit参数
    • redis.call方法,从缓存中getkey相关的值,如果为null那么就返回0
    • 接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
    • 如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1

    限流常在网关这一层做,比如NginxOpenrestykongzuulSpring Cloud Gateway等,而像spring cloud - gateway网关限流底层实现原理,就是基于Redis + Lua,通过内置Lua限流脚本的方式。

    核心代码

        public boolean acquire(String key) {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
            String lua = "local key = KEYS[1]" +
                    " local period = ARGV[1]" +
                    " local limit= ARGV[2]" +
                    " local times = redis.call('incr',key)" +
                    " if times == 1 then" +
                    " redis.call('expire',KEYS[1], period)" +
                    " end" +
                    " if times > tonumber(limit) then" +
                    " return 0" +
                    " end" +
                    " return 1";
            redisScript.setScriptText(lua);
            redisScript.setResultType(Long.class);
            //表示1s 内最多访问3次
            //key [key ...],被操作的key,可以多个,在lua脚本中通过KEYS[1], KEYS[2]获取
            //arg [arg ...],参数,可以多个,在lua脚本中通过ARGV[1], ARGV[2]获取。
            //0: 超出限制,else:正常请求
            Long count = (Long) stringRedisTemplate.execute(redisScript, Arrays.asList(key), "1", "3");
            System.err.println(System.currentTimeMillis() + "<>" + count);
            //0:超出范围
            return count == 0;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    JAVA SDK限流

    原理看这篇:流量控制算法总结_xushiyu1996818的博客-CSDN博客_流量控制

    令牌桶算法

    https://zhuanlan.zhihu.com/p/60979444

    令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

    系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

    生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情

    在这里插入图片描述
    在这里插入图片描述

    实现方式: RateLimiter

    • RateLimiter在并发环境下使用是安全的:它将限制所有线程调用的总速率。注意,它不保证公平调用。Rate limiter(直译为:速度限制器)经常被用来限制一些物理或者逻辑资源的访问速率。这和java.util.concurrent.Semaphore正好形成对照。
    • 一个RateLimiter主要定义了发放permits的速率。如果没有额外的配置,permits将以固定的速度分配,单位是每秒多少permits。默认情况下,Permits将会被稳定的平缓的发放。
    • 可以配置一个RateLimiter有一个预热期,在此期间permits的发放速度每秒稳步增长直到到达稳定的速率。

    核心代码

    <dependency>
      <groupId>com.google.guavagroupId>
      <artifactId>guavaartifactId>
      <version>26.0-jreversion>
      
      <version>26.0-androidversion>
    dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    //一秒创建3个令牌
        RateLimiter rateLimiter = RateLimiter.create(3.0);
    
        public void query() throws InterruptedException {
           //尝试获取令牌
            if (rateLimiter.tryAcquire()) {
                System.err.println(System.currentTimeMillis() + "   业务执行成功!");
                TimeUnit.MILLISECONDS.sleep(200);
            } else {
                TimeUnit.MILLISECONDS.sleep(200);
                System.err.println("************ " + System.currentTimeMillis() + "   业务执行失败!");
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    自己实现:

    long timeStamp=getNowTime(); 
    int capacity; // 桶的容量 
    int rate ;//令牌放入速度
     int tokens;//当前水量  
     
    bool control() {
       //先执行添加令牌的操作
       long  now = getNowTime();
       tokens = max(capacity, tokens+ (now - timeStamp)*rate); 
       timeStamp = now;   //令牌已用完,拒绝访问
     
       if(tokens<1){
         return false;
       }else{//还有令牌,领取令牌
         tokens--;
         retun true;
       }
     } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    漏桶算法

    漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

    在这里插入图片描述
    在这里插入图片描述

    漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

    漏桶算法没有标准的sdk提供实现,可以自己去实现一个漏桶算法。

    long timeStamp = getNowTime(); 
    int capacity = 10000;// 桶的容量
    int rate = 1;//水漏出的速度 
    int water = 100;//当前水量  
     
    public static bool control() {   
        //先执行漏水,因为rate是固定的,所以可以认为“时间间隔*rate”即为漏出的水量
        long  now = getNowTime();
        water = Math.max(0, water - (now - timeStamp) * rate);
        timeStamp = now;
     
        if (water < capacity) { // 水还未满,加水
            water ++; 
            return true; 
        } else { 
            return false;//水满,拒绝加水
       } 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    注意:这里的timeStamp是上一次执行操作的时间,这次操作,要先执行漏水,漏水量是now-timeStamp 相关的函数

    该算法很好的解决了时间边界处理不够平滑的问题,因为在每次请求进桶前都将执行“漏水”的操作,再无边界问题。

    但是对于很多场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合

    二者区别

    两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。

    在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,所以它适合于具有突发特性的流量。

    分布式限流——阿里Sentinel

    官网:Sentinel · alibaba/spring-cloud-alibaba Wiki · GitHub

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    • docker部署—> 导包 —> 配置
    • 想要给某个方法限流,就在方法上加注解(@SentinelResource(value = "brand_list" ,blockHandlerClass = {SentinelUtil.class}, blockHander = handerException );
    • 其中value为资源名、后面的blockHandlerClass是流量超了之后的处理方式、规则所在的类、 blockHander是方法名,叫兜底方法
    • 兜底方法必须与原方法的参数(多一个异常)、返回值类型必须一样
    • 如果从控制台配置限流规则,则是存在了内存中,一旦重启就没了;所以使用nacos的配置中心

    今日推歌

    ----《秘密》 张震岳

    也许在你的心中早就已经有人进去
    或许你不曾接受真正的爱真诚的情
    遗忘吧过去的事
    不要再怀疑
    我彷佛可以听见你的心跳你的声音
    不要只有在梦中才能看你才能靠近
    我可以慢慢的等
    直到你离去

  • 相关阅读:
    论文初稿写到什么程度才算合格?
    5.函数与递归
    Google Earth Engine(GEE)——ccdc分类,采用的是随机森林分类器
    基于复旦微的FMQL45T900全国产化ARM核心模块(100%国产化)
    最新国内maven仓库镜像地址
    Android Ble蓝牙App(七)扫描过滤
    R语言参数检验多重比较
    数据库调优厂商 OtterTune 宣布停止运营
    linux下的gdb调试器
    Win10离线安装choco方案
  • 原文地址:https://blog.csdn.net/m0_51013067/article/details/126486854