• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

无信不立

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

【数据结构和算法】令牌桶和漏桶的算法区别和实现 以及 固定窗口算法和滑动窗口算法

转载:https://blog.csdn.net/m0_37477061/article/details/95313062

一、令牌桶和漏桶算法区别

漏桶算法与令牌桶算法在表面看起来类似,很容易将两者混淆。但事实上,这两者具有截然不同的特性,且为不同的目的而使用。

漏桶算法与令牌桶算法的区别在于:

  •  漏桶算法能够强行限制数据的传输速率。
  •  令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。

需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

 

1、漏桶算法的示意

2、令牌桶的算法示意

========令牌桶的基本原理===============

【令牌的生成】:令牌以恒定的速率生成,放入令牌桶中,待令牌使用

【令牌的消费】:请求通过令牌桶,要先获取令牌,获取则请求通过,未获取的请求拒绝或阻塞到有令牌生成为至

 

========令牌生成的原理=================

令牌时间片( slot )= 1S / QPS

令牌生成斜率(rate) = QPS / 1s

当前桶中的令牌数 = Min ( max_token_num , last_token_num + (current_time - last_access_time ) * rate )

当前桶中的令牌数 = Min ( max_token_num , last_token_num + (current_time - last_access_time ) / slot )

令牌桶算法具有两个很重要的特性:流量整形和方便处理突发流量。

流量整形是指令牌桶算法通过阻塞、拒绝等手段使请求以稳定的速度通过限流器,原本不规则的流量在经过限流器后变得平滑且均匀。流量整形效果非常有利于服务端稳定运行,类似我们在高并发系统中常用的基于消息队列实现的“削峰填谷”手段,经过整形后,服务端能够以稳定的状态接收并处理请求。

突发流量是指随机出现的、短时间的流量突刺。如果严格遵循流量整形的限制,那么服务端在遇到突发流量时会突然拒绝一大波请求,在客户端有重试机制的情况下还可能导致情况进一步恶化。因此,在服务端资源充足的条件下,限流器应该具有一些“弹性”,允许服务端临时超频处理一些突发请求。

在令牌桶算法模型中,“弹性”处理突发流量是非常容易实现的,只需要给桶中生成的令牌设置一个有效期即可。有突发流量时,限流器可以使用有效期内的剩余令牌来通过更多请求,从而临时提高服务端处理效率,避免大量请求被拒绝。

 

 

 

 

二、常用的限流算法

 

 

常用的限流算法有两种:漏桶算法和令牌桶算法。

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

 

 

图1 漏桶算法示意图

 

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

 

 图2 令牌桶算法示意图

 

并不能说明令牌桶一定比漏洞好,她们使用场景不一样。

  • 令牌桶:可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制
  • 漏桶算法:用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
  •  总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。

 

二、算法实现

1、漏桶算法实现

/**
 * 漏桶限流
 **/
public class LeakyBucketLimit {
    //桶的最大水量
    private Double capacity;
    //当前桶中的水量
    private Double currentWater;
    //当前时间
    private Double nextLeakyWaterTime;
    //流水速率
    private Double rate;
    //每滴水的时间片
    private Double waterSlot;

    private Object lock = new Object();


    public LeakyBucketLimit(Integer maxBucket) {
        if (maxBucket == null || maxBucket <= 0) {
            throw new IllegalArgumentException("参数异常");
        }
        this.capacity = maxBucket.doubleValue();
        this.currentWater = 0D;
        this.nextLeakyWaterTime = Double.valueOf(System.currentTimeMillis());
        this.rate = this.capacity / 1000;
        this.waterSlot = 1000/this.capacity;
    }

    public boolean acquireLimit() {
        long time=0;
        synchronized (lock) {
            Long currentTime = System.currentTimeMillis();
            //放水
            leakyWater(currentTime);

            //注入水
            Double swapWater = this.currentWater + 1;
            if (swapWater > this.capacity) {
                return false;
            } else {
                this.currentWater = swapWater;
                //更新一下下次放水时间
                this.nextLeakyWaterTime += waterSlot;
                //这个是计算当前这次请求需要睡眠的时长,保障漏水的速度是均衡的
                time = Double.valueOf(this.nextLeakyWaterTime - currentTime).longValue();
            }
        }
        if(time>0){
            //延迟到指定的时间进行漏水
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

    /**
     * 放水
     * @param currentTime
     */
    private void leakyWater(Long currentTime) {
        if (currentTime > nextLeakyWaterTime) {
            Double leakyWater = (currentTime - nextLeakyWaterTime) * rate;
            this.currentWater = Math.max(0D, this.currentWater - leakyWater);
            this.nextLeakyWaterTime = currentWater;
        }
    }
}
View Code

(1)未满加水:通过代码 water +=1进行不停加水的动作。
(2)漏水:通过时间差来计算漏水量。
(3)剩余水量:总水量-漏水量。
(4)下次漏水时间= 下次漏水时间 + 获取入桶的水滴时间片   或者 = 当前时间(当前时间>下次漏水时间)

2、令牌桶算法实现

可以参考guava实现的令牌桶限流器:https://ifeve.com/guava-ratelimiter/

/**
 * 令牌桶限流
 **/
public class TokenBucketLimit {

    //令牌桶最大令牌
    private Double maxToken;
    //当前令牌桶的令牌数
    private Double currentToken;
    //下一次产生令牌的时间
    private Long nextFireTokenTime;
    //每个令牌的时间片
    private Double tokenSlot;
    //并发锁
    private Object lock = new Object();

    /**
     * 初始化令牌桶
     * @param maxToken
     */
    public TokenBucketLimit(Integer maxToken){
        if(maxToken == null || maxToken<=0){
            throw new IllegalArgumentException("参数异常");
        }
        this.maxToken= maxToken.doubleValue();
        this.tokenSlot = Double.valueOf(1000 / maxToken);
        this.currentToken = 0D;
        this.nextFireTokenTime = System.currentTimeMillis();
    }


    /**
     * 令牌桶
     * 【令牌桶属性】
     * 1、最大令牌数
     * 2、每个令牌时间片
     * 3、下次发放令牌时间
     * 4、当前令牌数
     * <p>
     * 【公式】
     * 1、每个令牌时间片= 1000 ms / 最大令牌数
     * 2、刷新令牌公式:当前令牌 = min( max令牌数 , (当前时间-下次发放时间)/每个令牌时间片 + 当前令牌 )
     * 3、扣减令牌公式:
     *
     * @return
     */
    public boolean acquireLimit() {
        //并发锁控制
        synchronized (lock){
            Long time = System.currentTimeMillis();
            //发放令牌
            fireBucketForToken(time);
            //消耗令牌
           return consumerBucketToken(time,1);
        }
    }


    /**
     * 基于时间控制进行令牌发放
     * @param currentTime
     */
    private void fireBucketForToken(Long currentTime) {
        if (currentTime > nextFireTokenTime) {
            Double addToken = (currentTime - nextFireTokenTime) / tokenSlot;
            //当前最新令牌
            this.currentToken = Math.min(this.maxToken, addToken + this.currentToken);
            //更新下次发放令牌的时间
            this.nextFireTokenTime = currentTime;
        }
    }

    /**
     * 消费令牌
     *
     * @param currentTime  当前时间
     * @param consumerToken 消费令牌数
     * @return
     */
    private boolean consumerBucketToken(Long currentTime, int consumerToken) {
        //找到交换令牌数
        Double swapToken = Math.min(this.currentToken,consumerToken);
        //计算需要超发的令牌数
        Double waitFireToken = consumerToken - swapToken;

        //判定是否在超发令牌范围内: 下次发放令牌时间 + 超发令牌数* 令牌时间片 - 当前时间  > 1s
        double  waiteTime = this.nextFireTokenTime + waitFireToken*tokenSlot - currentTime;
        if( waiteTime > 1000){
            //不允许超发令牌,没有获得限流
            return false;
        }else{
            //更新当前令牌数
            if(waitFireToken > 0){
                //超发令牌,则当前令牌数=0
                this.currentToken = 0D;
            }else{
                //未超发令牌,则进行正常的令牌扣减
                this.currentToken -= consumerToken;
            }
            //更新下一次令牌发放时间 = 当前下一次令牌发送时间 + 超发令牌耗时
            this.nextFireTokenTime = this.nextFireTokenTime + Double.valueOf(waitFireToken * tokenSlot).longValue();
            return true;
        }
    }
}
View Code

 

3、固定窗口算法实现

2.1.1 实现原理

固定窗口又称固定窗口(又称计数器算法,Fixed Window)限流算法,是最简单的限流算法。实现原理:在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。如图所示,我们要求3秒内的请求不要超过150次:

Clipboard_Screenshot_1756710978

2.1.2、代码实现

/**
 * https://developer.aliyun.com/article/1403803
 * 固定窗口限流算法
 */
public class FixedWindowRateLimiter {
    //时间窗口大小,单位毫秒
    long windowSize;
    //允许通过的请求数
    int maxRequestCount;
    //当前窗口通过的请求数
    AtomicInteger counter = new AtomicInteger(0);
    //窗口右边界
    long windowBorder;

    public FixedWindowRateLimiter(long windowSize, int maxRequestCount) {
        this.windowSize = windowSize;
        this.maxRequestCount = maxRequestCount;
        this.windowBorder = System.currentTimeMillis() + windowSize;
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        if (windowBorder < currentTime) {

            do {
                windowBorder += windowSize;
            } while (windowBorder < currentTime);
            counter = new AtomicInteger(0);
        }
        if (counter.intValue() < maxRequestCount) {
            counter.incrementAndGet();

            return true;
        } else {
            return false;
        }
    }
}
View Code

2.1.3 优缺点

优点:实现简单,容易理解

缺点:

1.限流不够平滑。例如:限流是每秒3个,在第一毫秒发送了3个请求,达到限流,窗口剩余时间的请求都将会被拒绝,体验不好。

2.无法处理窗口边界问题。因为是在某个时间窗口内进行流量控制,所以可能会出现窗口边界效应,即在时间窗口的边界处可能会有大量的请求被允许通过,从而导致突发流量。即:如果第2到3秒内产生了150次请求,而第3到4秒内产生了150次请求,那么其实在第2秒到第4秒这两秒内,就已经发生了300次请求了,远远大于我们要求的3秒内的请求不要超过150次这个限制,如下图所示:

Clipboard_Screenshot_1756711140

 

 

4、滑动窗口算法实现

2.2.1 实现原理

滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求。在滑动窗口算法中,窗口的起止时间是动态的,窗口的大小固定。这种算法能够较好地处理窗口边界问题,但是实现相对复杂,需要记录每个请求的时间戳。实现原理:滑动窗口在固定窗口的基础上,将时间窗口进行了更精细的分片,将一个窗口分为若干个等份的小窗口,每次仅滑动一小块的时间。每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阈值。其中,Sentinel就是采用滑动窗口算法来实现限流的。如图所示:

Clipboard_Screenshot_1756711194

 

核心步骤:

1.把3秒钟划分为3个小窗,每个小窗限制请求不能超过50秒。

2.比如我们设置,3秒内不能超过150个请求,那么这个窗口就可以容纳3个小窗,并且随着时间推移,往前滑动。每次请求过来后,都要统计滑动窗口内所有小窗的请求总量。

2.2.2 代码实现

package obj.sf;

public class SlidingWindowRateLimiter {

    //时间窗口大小,单位毫秒
    long windowSize;
    //分片窗口数
    int shardNum;
    //允许通过的请求数
    int maxRequestCount;
    //各个窗口内请求计数
    int[] shardRequestCount;
    //请求总数
    int totalCount;
    //当前窗口下标
    int shardId;
    //每个小窗口大小,毫秒
    long tinyWindowSize;
    //窗口右边界
    long windowBorder;

    public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) {
        this.windowSize = windowSize;
        this.shardNum = shardNum;
        this.maxRequestCount = maxRequestCount;
        this.shardRequestCount = new int[shardNum];
        this.tinyWindowSize = windowSize / shardNum;
        this.windowBorder = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        if (windowBorder < currentTime) {

            do {
                shardId = (++shardId) % shardNum;
                totalCount -= shardRequestCount[shardId];
                shardRequestCount[shardId] = 0;
                windowBorder += tinyWindowSize;
            } while (windowBorder < currentTime);
        }
        if (totalCount < maxRequestCount) {

            shardRequestCount[shardId]++;
            totalCount++;
            return true;
        } else {
            return false;
        }
    }
}
View Code

2.2.3 优缺点

优点:解决了固定窗口算法的窗口边界问题,避免突发流量压垮服务器。缺点:还是存在限流不够平滑的问题。例如:限流是每秒3个,在第一毫秒发送了3个请求,达到限流,剩余窗口时间的请求都将会被拒绝,体验不好。

 

 

posted on 2021-06-02 10:08  无信不立  阅读(974)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3