JAVA实现简单限流器(下)

JAVA实现简单限流器(下)

Guava 如何实现令牌桶算法

既然上篇提到采用定时任务有可能产生误差,那么Guava还能如何去实现呢?

Guava采用巧妙的记录并动态计算下一令牌发放的时间,来实现令牌桶算法,如下说明。

假设存在令牌桶的最大值1,流速为1个/秒,如果在当前T1时刻令牌桶没有令牌,那么请求正好在T1来临,下一次令牌发送时间应该是第3秒的位置

image-20220321235126307

由于在线程T1到达没有令牌,那么需要等待第3秒产生令牌后才能马上获取,那下一次产生令牌的时间就应该往后面移动一秒,也就是第4秒产生。

image-20220321235419149

假设这时T1刚刚获取令牌,T2马上请求令牌那么下一秒令牌产生的时间是多少呢?应该是第5秒

image-20220321235816514

上诉情况都是线程请求在令牌产生之前的,如果线程请求在令牌产生之后呢,如发生在线程T1请求完毕,假设是第7秒的位置,这时第5秒、第6秒和第7秒都会产生令牌,并且中间无线程获取,同时桶的最大值还是1,所以令牌桶中会保存一个令牌,线程T3可以直接获取。

image-20220322000500178

令牌桶算法实现1

从上面的分析可以得出,只要记录下一次的令牌产生时间并且动态的更新就能达到限流的效果,那么将上述分析语义化如下,注意这里实现的是令牌桶为1的情况,并不适合所有情况。

public class SimpleLimiter1 {
    /**
     * 下一次令牌产生的时间(单位纳秒)
     */
    private long next = System.nanoTime();

    /**
     * 一秒
     * 1毫秒 = 1000微秒  1微秒 = 1000纳秒
     */
    private long speed = 1000_000_000;

    /**
     * 计算下一次令牌产生的时间(单位纳秒)
     * @param now
     * @return
     */
    private synchronized long reserve(long now){
        // 请求时间在下一次令牌产生之后 简单处理
        if (now > next){
            next = now;
        }
        // 能够获取令牌的时间
        long at = next;
        // 下一次令牌产生的时间
        next += speed;
        return at;
    }

    /**
     * 申请令牌
     */
    public void acquire(){
        long now = System.nanoTime();
        // 令牌产生时间
        long reserve = reserve(now);

        long waitTime = reserve-now;

        if (waitTime>0){
            try {
                TimeUnit.NANOSECONDS.sleep(waitTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试方法

public static void main(String[] args) {
    SimpleLimiter2 simpleLimiter2 = new SimpleLimiter2();

    ExecutorService executorService = Executors.newFixedThreadPool(10);
    AtomicLong start = new AtomicLong(System.nanoTime());
    for (int i = 0; i <20 ; i++) {
        simpleLimiter2.acquire();
        executorService.execute(()->{
            long end = System.nanoTime();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"==="+(end- start.get())/1000_1000);
            start.set(end);
        });
    }
}

令牌桶算法实现2

通过上述代码我们就能得到一个令牌桶的容量为1的限流算法,但远远不够,如果令牌桶的容量为n呢?应该如何处理。

  • 如果线程在计算下一个令牌产生的时间时,需要重新计算令牌桶的令牌数也就是resync方法。
  • reserve方法则需要修改,所有的令牌将从令牌桶中获取。
public class SimpleLimiter2 {
    /**
     * 当前令牌数量
     */
    private long tokenNum = 0;

    /**
     * 令牌桶最大容量
     */
    private long maxTokenNum = 3;

    /**
     * 下一次令牌产生的时间(单位纳秒)
     */
    private long next = System.nanoTime();

    /**
     * 一秒
     * 1毫秒 = 1000微秒  1微秒 = 1000纳秒
     */
    private long speed = 1000_000_000;

    /**
     * 请求时间在下一令牌产生时间之后
     * 1、重新计算令牌桶中的令牌数(当前的令牌总数)
     * 2、将下一令牌产生时间重置为当前时间
     * @param now
     */
    private void resync(long now){
        if (now > next){
            // 新产生的令牌数
            long newProduceNum = (now - next) / speed;
            // 当前令牌桶中的令牌数
            tokenNum = Math.min(maxTokenNum,newProduceNum+tokenNum);
            next = now;
        }
    }

    /**
     * 计算下一次令牌产生的时间(单位纳秒)
     * @param now
     * @return
     */
    private synchronized long reserve(long now){
        // 请求时间在下一次令牌产生之后 简单处理
        resync(now);
        // 能够获取令牌的时间
        long at = next;
        // 令牌桶能够提供的令牌数 只有两种情况
        // fb = 0 说明tokenNum=0 now <= next  令牌桶中没有令牌
        // fb = 1 说明tokenNum>1 now > next   令牌桶中有令牌
        long fb = Math.min(1,tokenNum);
        // nr=0那么已经消耗了一个令牌    nr=1 令牌桶中没有令牌还没消耗
        long nr = 1- fb;
        // 下一次令牌产生的时间
        next = next + nr*speed;
        // 重新计算令牌桶中的数量
        tokenNum -= fb;
        return at;
    }

    /**
     * 申请令牌
     */
    public void acquire(){
        long now = System.nanoTime();
        // 令牌产生时间
        long reserve = reserve(now);

        long waitTime = Math.max(reserve-now,0);

        if (waitTime>0){
            try {
                TimeUnit.NANOSECONDS.sleep(waitTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面给出一个简单流器实现,使用了令牌桶算法: ``` import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class RateLimiter { private final int capacity; // 令牌桶的容量 private final int rate; // 令牌桶的生成速率,即每秒生成多少个令牌 private AtomicInteger tokens; // 当前令牌桶中令牌的数量 private long lastRefillTime; // 上次令牌桶中令牌生成的时间 public RateLimiter(int capacity, int rate) { this.capacity = capacity; this.rate = rate; this.tokens = new AtomicInteger(capacity); this.lastRefillTime = System.nanoTime(); } // 尝试获取令牌,如果获取成功则返回 true,否则返回 false public boolean tryAcquire() { refill(); return tokens.getAndUpdate(n -> n > 0 ? n - 1 : n) > 0; } // 等待直到获取到令牌 public void acquire() throws InterruptedException { while (!tryAcquire()) { TimeUnit.MILLISECONDS.sleep(100); } } // 生成令牌 private void refill() { long now = System.nanoTime(); long elapsedNanos = now - lastRefillTime; int newTokens = (int) (elapsedNanos * rate / 1_000_000_000); if (newTokens > 0) { tokens.updateAndGet(n -> Math.min(n + newTokens, capacity)); lastRefillTime = now; } } } ``` 使用示例: ``` RateLimiter limiter = new RateLimiter(10, 1); // 令牌桶容量为 10,每秒生成 1 个令牌 for (int i = 0; i < 20; i++) { if (limiter.tryAcquire()) { System.out.println("Task " + i + " is executed"); } else { System.out.println("Task " + i + " is rejected"); } TimeUnit.MILLISECONDS.sleep(100); } ``` 输出: ``` Task 0 is executed Task 1 is rejected Task 2 is rejected Task 3 is rejected Task 4 is rejected Task 5 is rejected Task 6 is rejected Task 7 is rejected Task 8 is rejected Task 9 is rejected Task 10 is executed Task 11 is rejected Task 12 is rejected Task 13 is rejected Task 14 is rejected Task 15 is rejected Task 16 is rejected Task 17 is rejected Task 18 is rejected Task 19 is rejected ``` 上面的代码中,令牌桶的容量为 10,每秒生成 1 个令牌。在执行任务时,先使用 `tryAcquire` 尝试获取令牌,如果获取成功则执行任务,否则等待一段时间后重试。如果不想等待,可以使用 `acquire`,它会一直阻塞直到获取到令牌。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值