首页 > 技术文章 > 限流:漏桶和令牌桶算法 单机实现

oyjg 2020-06-10 15:42 原文

漏桶:漏桶可以看作是一个漏斗类似,水可以以任意速度流入,桶保存一定量的水,水以一定的速率流出。

 

 

 

令牌桶:桶会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

 

 

 

从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。

在单机上的实现

漏桶

import java.time.LocalDateTime;

/**
 * 漏桶
 */
public class LeakyBucket {
    //流水速率  固定
    private double rate;
    //桶的大小
    private double burst;
    //最后更新时间
    private int refreshTime;
    //private Long refreshTime;
    //桶里面的水量
    private int water;

    public LeakyBucket(double rate,double burst){
        this.rate=rate;
        this.burst=burst;
    }

    /**
     * 刷新桶的水量
     */
    private void refreshWater(){
        //long now = System.currentTimeMillis(); //毫秒生成
        LocalDateTime time=LocalDateTime.now();  //每秒生成
        int now = time.getSecond();
        //现在时间-上次更新的时间   中间花费的时间(秒)*流水速率=流水量(处理的请求的数量)  通过上次水总量减去流水量等于现在的水量
        //如果流水量太多导致桶里都没那么多水就应该置0, 所以通过math.max函数实现
        water = (int)Math.max(0,water-(now-refreshTime)*rate);
        //更新上次时间
        refreshTime = now;
    }

    /**
     * 获取令牌
     */
    public synchronized boolean tryAcquire(){
        //刷新桶的水量
        refreshWater();
        //如果桶的水量小于桶的容量就可以添加进来
        if(water<burst){
            water++;
            return true;
        }else {
            return false;
        }
    }
}
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest {
    public static LeakyBucket leakyBucket = new LeakyBucket(10,100);
    public static void main(String[] args) {long start = System.currentTimeMillis();
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(leakyBucket.tryAcquire());
                }
            }).start();
        }
        System.out.println("总花费:"+(System.currentTimeMillis()-start));
        System.out.println("线程执行完毕");
    }
}

令牌桶

Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法,所以我们直接使用Guava即可实现。

加入依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>
import com.google.common.util.concurrent.RateLimiter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 令牌桶
 */
public class TokenBucket {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    /**
     * permitsPerSecond为每秒生成的令牌
     *
     */
     /** 平衡稳定 
     * * 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
     * * 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
     * * 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
     */
    /**平衡预热
     * 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
     * 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
     * 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
     * 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
     * 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
     */
    private static final RateLimiter rateLimiter = RateLimiter.create(10,2L, TimeUnit.SECONDS);
    //private static final RateLimiter rateLimiter = RateLimiter.create(10);

    /**
     * tryAcquire尝试获取permit,默认超时时间是0,意思是拿不到就立即返回false
     * @return
     */
    public String sayHello(){
        if(rateLimiter.tryAcquire()){    //一次拿一个
            System.out.println(sdf.format(new Date()));
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            return "no";
        }
        return "hello";
    }

    /**
     * acquire拿不到就等待,拿到为止
     * @return
     */
    public String sayHi(){
        rateLimiter.acquire(1);  //一次拿5个  意思就是生成10个令牌才去全部拿去给一个请求
        System.out.println(sdf.format(new Date()));
        return "hi";
    }
}
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest {
    private static TokenBucket tokenBucket = new TokenBucket();
    public static void main(String[] args) {long start = System.currentTimeMillis();
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(tokenBucket.sayHi());
                }
            }).start();
        }
        System.out.println("总花费:"+(System.currentTimeMillis()-start));
        System.out.println("线程执行完毕");
    }
}

区别:

漏桶

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

令牌桶

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

最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失。

推荐阅读