限流算法及实现

1.固定窗口算法

        分别使用代码,guava cache和redis进行实现

        代码思想:

       (1)定义时间窗口,限流阙值,记录的窗口时间,当前请求量四个参数

       (2)逻辑处理:获取当前时间与记录的窗口时间比较,如果大于时间窗口,证明窗口已经过期,重新赋值:记录的窗口时间,当前的请求量。如果小于情况下,证明满足时间要求,判断是否已经达到限流阙值,达到拒绝,不打到通过并请求量+1.

       guava cache代码思想:

        (1)利用guava cache自动淘汰机制,设置最大容量,设置过期时间,同时load方法重新。

          (2) 获取当前时间的值,如果+1之后比限流阙值大情况下,拒绝任务;比限流阙值小的情况下,通过任务

       redis思想:

         (1)利用String结构,setNx命令,设置key为限流名称,value代表流量个数,同时设置过期时间为时间窗口大小

          (2)方法判断key是否存在,不存在情况下初始化key,value;存在情况下,通过key获取数量,如果数量+1比流量阙值大情况下,拒绝请求;否则进行+1操作。

      代码如下:

/**
 * 固定窗口限流算法
 * 核心思想:在一个时间窗口内达到的并发量
 */
public class FixedWindows {

    //数量限制
    private static int limit = 100;

    //时间窗口 1000ms
    private static Long windowUnit = 1000L;

    //时间
    private static Long time = 0L;

    //当前的请求数
    private static int curRequest = 0;

    /**
     * 固定窗口函数,使用synchronized保证线程安全
     * 1.获取当前时间与之前窗口时间比较,大的情况下证明过期了,重新初始化变量
     * 2.小的情况下,判断请求数是否比限制数大,大拒绝,小同意
     */
    public static synchronized void fixedWindows() {
        Long curTime = System.currentTimeMillis();
        //判断当前窗口是否过期
        if(curTime-time>windowUnit) {
            //过期处理
            time = curTime;
            curRequest=1;
            System.out.println("1.正常通过请求");
            return;
        }
        //在窗口内处理
        curRequest++;
        if(curRequest>limit) {
            System.out.println("2.限流了,拒绝请求");
            return;
        }
        System.out.println("3.正常通过请求");
    }

    //guavaCache实现限流
    static LoadingCache<Long, AtomicInteger> cache = CacheBuilder.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(1,TimeUnit.SECONDS)
            .build(new CacheLoader<Long, AtomicInteger>() {
                @Override
                public AtomicInteger load(Long key) throws Exception {
                    AtomicInteger a =new AtomicInteger(0);
                    System.out.println(a.intValue());
                    return a;
                }
            });
    
    public  void fixedWindowsGuavaCache() throws ExecutionException {
        //秒级
        Long time = System.currentTimeMillis()/1000;
        if(cache.get(time).incrementAndGet()>limit) {
            System.out.println("拒绝任务");
        }else{
            System.out.println("接收任务");
        }
    }

    @Autowired
    RedisTemplate<String,Object> redisTemplate;

    //redis实现固定窗口限流
    //数据结构为String key为限流key名称,value代表次数,过期时间为固定窗口
    //1.判断key是否存在,不存在情况下,进行初始key,value,并设置过期时间
    //2.key存在情况下,获取key的数量,和限制的值进行比较,不满足情况下+1,满足拒绝任务

    public void fixedWindowsRedis() {
        //先判断key是否存在
        if(redisTemplate.hasKey("limit")) {
            int sum = (int) redisTemplate.opsForValue().get("limit");
            if(sum+1>limit) {
                //拒绝任务
                System.out.println("拒绝任务");
            }else{
                //+1操作
                redisTemplate.opsForValue().increment("limit",1);
                System.out.println("正常接收任务");
            }
        }else{
            //使用什么结构 String
            redisTemplate.opsForValue().setIfAbsent("limit",1);
            //1s
            redisTemplate.expire("limit",1, TimeUnit.SECONDS);
            System.out.println("正常接收任务");
        }


    }

    public static void main(String[] args) {
        for(int i=0;i<101;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    fixedWindows();
                }
            }).start();
        }
    }
}

优缺点分析:

   优点:实现简单

   缺点:临界值问题

2.滑动窗口实现

        优点:解决了固定窗口的临界值问题。扩展能力强。

        缺点:流量不够平滑,有可能第一个窗口就满足要求了,其他都不能接收请求了。

        各种代码实现思路:

     代码实现:

        (1)定义几个参数,划分几个格子,限制的次数,存储每个格子的数据量TreeMap

        (2)1.获取格子归属当前时间

                 2.获取所有符合条件格子的总数,不符合的情况下删除key,value。

                 3.判断总数和限制次数,大于情况下拒绝任务,否则接收任务,将计数器+1

          guava cache实现思路:

                 1.设置guava cache的初始化参数,最大容量,最大并发量,过期时间,构造函数

                 2.获取guava cache的数量,从一个个时间窗口中获取到起初的时间窗口,累加,成功返回.

         redis实现滑动窗口原理:

                利用zset结构,k存储限流名称 value 存储次数 score 存储时间戳,过期时间这里可以比限制时间长,否则会有数据丢失情况。

              1.判断key是否存在,不存在直接赋值

              2.存在情况下,zrangByScore从time-时间间隔开始一直到time看看有多少数量。数量比限制值大的情况拒绝,否则通过。

             缺点是zset会随着构建数据不断增长

     代码实现:

      

public class SlidingWindows {
    //代码实现
    //单位时间的周期,10代表每10s一个周期,每分钟有6个格子
    private static int sub_cycle = 10;
    //每分钟限制多少流量
    private static int limitMin = 100;
    //每个格子计数器,使用TreeMap机构,为什么使用TreeMap结构,因为TreeMap有序,key代表格子的开始时间,value代表数量
    private static TreeMap<Long,Integer> map = new TreeMap<>();

    //1.获取当前时间
    public static synchronized void slidingWindows() {
            //获取在哪个格子里
            Long time =  LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / sub_cycle * sub_cycle;
            //计算总数,treeMap各个格子加在一起的数量
            int count = getSum(time);
            //判断总数和limit谁大
            if(count+1>limitMin) {
                System.out.println("拒绝任务");
            }else{
                System.out.println("接收任务");
                map.put(time,map.getOrDefault(time,0)+1);
            }
    }

    public static int getSum(Long time) {
        //计算窗口开始位置
        long startTime = time - sub_cycle* (60/sub_cycle-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = map.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;
    }

    //guava cache限流
    static LoadingCache<Long, AtomicInteger> cache = CacheBuilder.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(10, TimeUnit.SECONDS)
            .build(new CacheLoader<Long, AtomicInteger>() {
                @Override
                public AtomicInteger load(Long key) throws Exception {
                    AtomicInteger a =new AtomicInteger(0);
                    System.out.println(a.intValue());
                    return a;
                }
            });

    public static void slidingWindowsGuavaCache() throws ExecutionException {
        Long time = System.currentTimeMillis()/(1000*sub_cycle)*sub_cycle;
        //滑动窗口改造
        int sum = cache.get(time).incrementAndGet();
        for(int i=1;i<sub_cycle;i++) {
            sum+=cache.get(time-sub_cycle*(60/sub_cycle-i)).intValue();
        }
        if(sum>limitMin) {
            System.out.println("拒绝请求");
        }else{
            System.out.println("接收请求");
        }
    }

    Long interVal = 60000L;
    @Autowired
    RedisTemplate redisTemplate;
    /**
     * redis滑动窗口限流
     * 数据结构:zset key:限流名称 score:时间戳 value次数
     */
    public void slidingWindowRedis() {
        //获取时间窗口
        Long time = new Date().getTime();
        //判断key存在与否
        if(redisTemplate.hasKey("limit")) {
            int size =redisTemplate.opsForZSet().rangeByScore("limit",time-interVal,time).size();
            if(size+1>limitMin) {
                System.out.println("拒绝请求");
                return;
            }
         
        }
        redisTemplate.opsForZSet().add("limit",1,time);
       
    }

    public static void main(String[] args) {
        for(int i=0;i<1000;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    slidingWindows();
                }
            }).start();
        }
    }
}

    3.漏桶算法

        优点:平滑处理流量,通过设置容量+速率可以动态控制流出

       缺点:突发流量情况下,桶容量满了,就会丢失数据。

      本地代码实现:

     思想:设置桶容量,速率,当前桶容量,上次漏水时间戳

       1.当放入水的时候,先进行漏水一次

          漏水逻辑:获取当前时间戳,和上一次漏水时间戳计算出来应该流出多少。

                           将桶中容量减去流出水量,初始化当前桶容量,漏水时间。

       2.漏水之后进行判断容量+此次漏水量和总容量进行比较,如果大于拒绝请求,否则接收请求,初始化容量

         核心点:桶容量,速率

                         

public class LeakyBucket {
    private int capacity;//总容量
    private Long rate;//此处是每秒流水速率
    private Long curCap;//当前水量
    private Long lastLeakTime;//上次漏水时间戳

    //构造函数 初始化参数
    public LeakyBucket(int capacity,Long rate) {
        this.capacity = capacity;
        this.rate = rate;
        curCap = 0L;
        lastLeakTime = System.currentTimeMillis();
    }

    /**
     * 尝试放入桶中
     * @param waterRequested
     */
    public synchronized void tryConsume(Long  waterRequested) {
        //漏水
        leak();
        if(curCap+waterRequested>capacity) {
            System.out.println("桶满了,拒绝请求");
            return;
        }
        curCap+=waterRequested;
        System.out.println("接收请求");
    }

    /**
     * 漏水
     * 根据当前时间和上次漏水时间戳计算出应该漏出的水量,然后更新桶中的水量和漏水时间戳等状态。
     */
    public synchronized void leak() {
        Long curTime = System.currentTimeMillis();
        Long leakWater = (curTime-lastLeakTime)/1000*rate;
        if(leakWater>0) {
            curCap = Math.max(0L,curCap - leakWater);
            lastLeakTime = curTime;
        }
    }

    public static void main(String[] args) {
        LeakyBucket leakyBucket =new LeakyBucket(100,2L);
        for(int i=1;i<1000;i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    leakyBucket.tryConsume((long) finalI);
                }
            }).start();
        }
    }
}

redis实现:

个人感觉:使用list结构,利用左出右进或者左进右出的特性;有些地方说也可以用zset结构,个人感觉漏桶使用zset不太合适,score和value应该存储什么内容,如何流出数据。个人感觉list更合适。

/**
 * redis实现漏桶
 * 1.桶容量设置
 * 2.速率设置,
 */
public class LeakBucketRedis {
    private int cap;//桶容量
    private int rate;//速率
    private Long lastLeakTime;//时间
    @Autowired
    RedisTemplate redisTemplate;

    public LeakBucketRedis(int cap,int rate) {
        this.cap = cap;
        this.rate = rate;
        this.lastLeakTime = System.currentTimeMillis();
    }

    //list实现 利用左出右进的思想
    public void leakBucketRedisList() {
        //如果没有初始化先进行初始化
        if(redisTemplate.hasKey("limit")) {
            redisTemplate.opsForList().rightPush("limit",1);
            lastLeakTime = System.currentTimeMillis();
        }else{
            //判断先按照速率流出
            Long curTime = System.currentTimeMillis();
            int leakSum = (int) ((curTime-lastLeakTime)/1000*rate);
            for(int i=0;i<leakSum;i++) {
                redisTemplate.opsForList().leftPop("limit");
            }

            //计算当前结构的容量
            Long count =redisTemplate.opsForList().size("limit");
            if(count+1>cap) {
                System.out.println("桶满了,不可以放入元素了");
                return;
            }
            redisTemplate.opsForList().rightPush("limit",1);
        }
    }
}

4.令牌桶限流

        优点:

  • 稳定性高:令牌桶算法可以控制请求的处理速度,可以使系统的负载变得稳定。

  • 精度高:令牌桶算法可以根据实际情况动态调整生成令牌的速率,可以实现较高精度的限流。

  • 弹性好:令牌桶算法可以处理突发流量,可以在短时间内提供更多的处理能力,以处理突发流量。

       缺点:

  • 实现复杂:相对于固定窗口算法等其他限流算法,令牌桶算法的实现较为复杂。对短时请求难以处理:在短时间内有大量请求到来时,可能会导致令牌桶中的令牌被快速消耗完,从而限流。这种情况下,可以考虑使用漏桶算法。

  • 时间精度要求高:令牌桶算法需要在固定的时间间隔内生成令牌,因此要求时间精度较高,如果系统时间不准确,可能会导致限流效果不理想。

 令牌桶算法具有较高的稳定性和精度,但实现相对复杂,适用于对稳定性和精度要求较高的场景。

     原理如下图所示:redis代码实现:

 使用list结构,左出右进或者左进右出的思想放入令牌

public class TokenBucketRedis {

    private final static String TOKEN_KEY = "TOKEN_KEY";

    /**
     * 令牌个数
     */
    private int TOKEN_SIZE = 10;

    @Autowired
    RedisTemplate redisTemplate;

    public synchronized void tokenBucket() {
        //判断令牌个数,循环一直等着>0
        int i=0;
        while(redisTemplate.opsForList().size(TOKEN_SIZE)<0&&i<3) {
            System.out.println("等待令牌生成");
            i++;
        }
        if(i==3) {
            System.out.println("重试3次未获得令牌,拒绝请求");
            return;
        }
        //获取令牌
        redisTemplate.opsForList().leftPop(TOKEN_KEY);
        System.out.println("获取令牌成功,请求接收");
    }

    /**
     * 动态生成令牌:
     * fixedDelay代表每秒生成,控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次
     */
    @Scheduled(fixedDelay=1000)
    public void takeToken() {
        //令牌个数不满足情况下,加入令牌
        Long size = redisTemplate.opsForList().size(TOKEN_SIZE);
        for(Long i=size;i<TOKEN_SIZE;i++){
            redisTemplate.opsForList().rightPush(TOKEN_KEY, UUID.randomUUID());
        }
    }
}

5.总结:

     固定时间窗口:一个固定窗口的次数

          优点:实现简单 缺点:临界值问题,流量不平滑

          实现方式:1.定义一个时间窗口,一个限流数,通过当前时间和上次时间进行比较,如果大于时间阙值,证明已经过期;小于情况下,判断当前流量请求数和限流阙值,如果满足的情况下,通过请求,否则拒绝请求。 2.使用redis的string结构,key存储限流名称,value存储次数,过期时间设置为时间窗口,利用自动的过期时间进行实现固定窗口限流。

   滑动窗口:将一个时间窗口等分几块,随着时间进行格子移动。

        优点:解决了固定窗口临界值问题

        缺点:处理不够平滑

        实现方式:1.将一个时间窗口几等分,限流阙值,记录每个窗口的次数可以使用TreeMap结构,key存储时间窗口的起始位置,value存储当前格子的次数;请求来的时候,计算时间阙值以内所有的次数与限流阙值比较,大情况下拒绝请求,小的时候通过请求

                      2.使用redis的zset结构,利用zset结构,key存储限流名称,score存储时间戳,value存储次数或者id,利用score的排序性,获取当前时间-时间阙值的个数与限流阙值进行比较,如果大于等于说明已经满足了拒绝请求,否则通过请求,同时zadd数据

      漏桶算法:一个桶,流出速率固定,请求进入的数量不确定。当桶满足了,拒绝请求,以固定速率处理请求,以保护当前系统。

            优点:相对于滑动窗口更加平滑。

            缺点:突发流量场景下处理不足。

            实现:1.两个必须参数:桶容量,流出速率

                        2.流量进入之后,判断桶是否满足限流要求,满足进行限流。

                                                   不满足情况下,将请求加在桶中。

                                                  以固定速率处理桶里面数据

                         redis实现:使用list代表桶,左出右进或者左进右出;进入时候判断list的size和桶容量,满足要求拒绝请求,不满足请求将请求放入list中。以固定速率进行出队列。

     令牌桶:一个桶中存储了一堆令牌,每个请求去获取令牌,获取到接收请求,获取不到拒绝请求。

      优点:解决了突发流量

       缺点:实现困难;时间精度高

       实现:redis list结构实现,list代表令牌数量,请求获取list的数据,获取到出队列,获取不到代表没有令牌了,拒绝请求。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值