限流实现及基本原理

限流

使用成本需要消耗成本,用户可能疯狂刷量

解决问题:

  1. 控制成本 => 限制用户调用总次数

  2. 限流 => 用户在短时间内疯狂发请求,导致服务器资源被占满,其他用户无法使用

限流阈值如何设置? => 参考正常用户的使用

限流的几种算法

  1. 固定窗口限流

    维护一个计数器,将单位时间段作为一个窗口,计数器记录这个窗口的请求次数

    • 当次数小于限流阈值 => 允许访问 => 计数器 + 1

    • 次数大于阈值 => 拒绝访问

    • 当前的时间窗口过去之后 => 计数器清零

    缺点:

    • 有临界问题 => 假设限流阈值为5,单位时间窗口是 1 小时,在当前时间窗口前 59 分内都没有用户发起请求,在最后一秒钟的时候,并发了 5 个请求,然后到了第二个时间窗口的第一秒钟的时候也并发了 5 个请求,这时就发生了一个问题 => 在这两秒钟之内并发了 10 个请求,就超过了一个小时之内只能处理 5 个请求的限制了。

  2. 滑动窗口问题

    动窗口问题能解决固定窗口临界值的问题,将单位时间周期分为 n 个小周期,分别记录每个小周期内接口的访问次数,根据时间滑动删除过期的小周期

    • 假设单位时间为 1 秒钟,限流阈值为 5,滑动窗口就把她划分为 5 个周期,滑动窗口(单位时间)被划分为 5 个小格子,每格表示 0.2 s,每过 0.2 s,时间窗口往右滑动一格,每个小周期,都有自己的计数器,如果请求是 在0.83 s 到达的,0.8 ~ 1.0 s对应的计数器就会加一

    实现:

     
    
     /**
          * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
          */
         private int SUB_CYCLE = 10;
     ​
         /**
          * 每分钟限流请求数
          */
         private int thresholdPerMin = 100;
     ​
         /**
          * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
          */
         private final TreeMap<Long, Integer> counters = new TreeMap<>();
     ​
        /**
          * 滑动窗口时间算法实现
          */
         boolean slidingWindowsTryAcquire() {
             long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
             int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数
     ​
             //超过阀值限流
             if (currentWindowNum >= thresholdPerMin) {
                 return false;
             }
     ​
             //计数器+1
             counters.get(currentWindowTime)++;
             return true;
         }
     ​
        /**
         * 统计当前窗口的请求数
         */
         private int countCurrentWindow(long currentWindowTime) {
             //计算窗口开始位置
             long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
             int count = 0;
     ​
             //遍历存储的计数器
             Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
             while (iterator.hasNext()) {
                 Map.Entry<Long, Integer> entry = iterator.next();
                 // 删除无效过期的子窗口计数器
                 if (entry.getKey() < startTime) {
                     iterator.remove();
                 } else {
                     //累加当前窗口的所有计数器之和
                     count =count + entry.getValue();
                 }
             }
             return count;
         }
     
  3. 漏桶算法

    往漏桶了中以任意速率注入水,以固定的速率流出水,当水超过桶的容量时,就会溢出,即被丢弃。因为桶的容量不变 => 保证总体的速率

    • 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。

    • 桶的容量一般表示系统所能处理的请求数。

    • 如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)

    • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。

    缺点:

    • 在正常流量的时候,系统按照固定的速率处理请求,但是在面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,但是我们在这时候希望系统尽快处理请求提高用户体验

     ​
      
    
    /**
          * 每秒处理数(出水率)
          */
         private long rate;
     ​
         /**
          *  当前剩余水量
          */
         private long currentWater;
     ​
         /**
          * 最后刷新时间
          */
         private long refreshTime;
     ​
         /**
          * 桶容量
          */
         private long capacity;
     ​
         /**
          * 漏桶算法
          * @return
          */
         boolean leakybucketLimitTryAcquire() {
             long currentTime = System.currentTimeMillis();  //获取系统当前时间
             long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
             long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
             refreshTime = currentTime; // 刷新时间
     ​
             // 当前剩余水量还是小于桶的容量,则请求放行
             if (currentWater < capacity) {
                 currentWater++;
                 return true;
             }
             
             // 当前剩余水量大于等于桶的容量,限流
             return false;
         }
     ​

  4. 令牌桶算法

    在面对突发流量的时候,我们可以使用令牌桶算法

    令牌桶算法的原理:

    根据限流答小 => 定速往令牌桶里放令牌 => 系统接受到一个用户请求 => 区令牌桶里要一个令牌 => 拿到令牌 => 处理请求 => 拿不到令牌 => 拒绝请求

    实现:

         
    
    /**
          * 每秒处理数(放入令牌数量)
          */
         private long putTokenRate;
         
         /**
          * 最后刷新时间
          */
         private long refreshTime;
     ​
         /**
          * 令牌桶容量
          */
         private long capacity;
         
         /**
          * 当前桶内令牌数
          */
         private long currentToken = 0L;
     ​
         /**
          * 漏桶算法
          * @return
          */
         boolean tokenBucketTryAcquire() {
     ​
             long currentTime = System.currentTimeMillis();  //获取系统当前时间
             long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
             currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
             refreshTime = currentTime; // 刷新时间
             
             //桶里面还有令牌,请求正常处理
             if (currentToken > 0) {
                 currentToken--; //令牌数量-1
                 return true;
             }
             
             return false;
         }
     ​

限流粒度

  1. 针对某个方法限流 => 单位时间内最多同时允许 XX 个操作使用这个方法

  2. 针对某个用户限流 => 单个用户单位时间内最多执行 XX 次操作

  3. 针对某个用户操作某个方法限流 => 单个用户单位时间内最多执行 XX 次这个方法

限流的实现

  1. 本地限流(单机限流)

    每个服务器单独限流,一般适用于单体项目

    Guava RateLimiter:

     
    public static void main(String[] args) {
             // 创建 RedissonClient
             RedisClient redisson = RedisClient.create();
             // 获取限流器
             RSemaphore semaphore = redisson.getSemaphore("mySemaphore");
             // 尝试获取许可证
             boolean result = semphore.tryAcquire();
             if (result) {
                 // 处理请求
             } else {
                 // 超过流量限制,需要做何处理
             }
         }

  2. Redisson 限流实现

    Redisson 内置了一个限流的工具类,可以帮助 利用 Redis 来存储、统计

    1. 引入 Redisson 依赖

       
      <dependency>
                   <groupId>org.redisson</groupId>
                   <artifactId>redisson</artifactId>
                   <version>3.21.3</version>
               </dependency>

    2. 创建 RedisConfig 配置类,用于初始化 RedissonCilent 对象单例

    3. 编写 RedistLimiterManager(专门提供 RedisLimiter 限流基础服务,提高了通用能力,可以放到任何一个项目里)

     
    /**
      * @author zzt
      * 专门提高 RedisLimiter 限流基础服务器的(提供了通用的能力)
      * Redis 实现限流
      */
     @Service
     public class RedisLimiterManager {
     ​
         @Resource
         private RedissonClient redissonClient;
     ​
         /**
          *
          * @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
          */
         public void doRateLimiter(String key) {
             //创建一个名为 user_limiter 的限流器,每秒最多访问 1 次
             RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
             rateLimiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS);
             // 来一次请求,则请求拿一个令牌
             boolean canOperate = rateLimiter.tryAcquire(1);
             if (!canOperate) {
                 throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
             }
         }
     }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值