在N秒内对M个请求进行节流方法调用


137

我需要一个组件/类,该组件/类可以在N秒内将某些方法的执行限制为最多M个调用(或ms或nanos,无所谓)。

换句话说,我需要确保我的方法在N秒的滑动窗口中执行不超过M次。

如果您不知道现有课程,请随时发布解决方案/想法,以了解如何实现。




>我需要确保我的方法在N秒的滑动窗口中执行不超过M次。我最近写了一篇有关如何在.NET中执行此操作的博客文章。您也许可以用Java创建类似的东西。.NET中更好的速率限制
Jack Leitch 2010年

原始问题听起来很像此博客文章中解决的问题:[Java多通道异步调节器 ](cordinc.com/blog/2010/04/java-multichannel-asynchronous.html)。对于以N秒为单位的M个呼叫,本博客中讨论的调节器保证了时间轴上长度为N的任何间隔所包含的呼叫都不会超过M个。
Hbf

Answers:


81

我将使用固定大小为M的时间戳环形缓冲区。每次调用该方法时,您都要检查最旧的条目,如果过去不到N秒,则执行并添加另一个条目,否则就睡觉对于时差。


4
可爱。正是我所需要的。快速尝试显示了大约10行来实现这一目标,并最大程度地减少了内存占用。只需考虑线程安全性和传入请求的排队。
vtrubnikov

5
这就是为什么要使用java.util.concurrent中的DelayQueue的原因。这样可以防止多个线程作用于同一条目的问题。
erickson

5
我认为,对于多线程情况,令牌桶方法可能是更好的选择。
Michael Borgwardt

1
您是否知道该算法的名称叫它怎么称呼?
VladoPandžić18年

80

开箱即用的是Google Guava RateLimiter

// Allow one request per second
private RateLimiter throttle = RateLimiter.create(1.0);

private void someMethod() {
    throttle.acquire();
    // Do something
}

19
我不推荐这种解决方案,因为Guava RateLimiter会阻塞线程,并且很容易耗尽线程池。
kaviddiss 2014年

18
@kaviddiss,如果您不想阻止,请使用tryAquire()
slf

7
当前实施RateLimiter的问题(至少对我而言)是它不允许超过1秒的时间段,因此不允许每分钟1次的速率。
约翰B

4
@John B据我了解,通过使用RateLimiter.create(60.0)+ rateLimiter.acquire(60)
RateByZero 2015年

2
@radiantRazor Ratelimiter.create(1.0 / 60)和acquire()每分钟实现一次通话。
bizentass

30

具体来说,您应该可以使用来实现DelayQueue。使用M Delayed实例初始化延迟为零的实例来初始化队列。当对方法的请求进入时,take令牌将导致方法阻塞,直到满足限制要求为止。取得令牌后,add新令牌以延迟进入队列N


1
是的,这可以解决问题。但是我并不特别喜欢DelayQueue,因为它正在使用(通过PriortyQueue)平衡的二进制哈希(这意味着要进行大量比较,offer并且可能会增加数组),这对我来说有点繁重。我想对于其他人来说可能完全没问题。
vtrubnikov

5
实际上,在此应用程序中,由于添加到堆中的新元素几乎始终是堆中的最大元素(即,具有最长的延迟),因此通常每次添加都需要进行一次比较。同样,如果算法正确实现,数组将永远不会增长,因为只有在取一个元素之后才添加一个元素。
erickson

3
我发现,如果您不希望通过使大小M和延迟N保持相对较小的毫秒数来使请求大幅度突发,那么这也很有用。例如。M = 5,N = 20ms将提供250 / sec的突然爆发吞吐量,发生大小为5。–
FUD

当允许并发请求时,是否可以扩展到一百万转?我将需要添加一百万个delayElements。另外,极端情况下的延迟也会很高-多个线程正在调用poll()且每次都会锁定的情况。
Aditya Joshee

@AdityaJoshee我尚未对它进行基准测试,但是如果我有时间,我将尝试了解开销。不过要注意的一件事是,您不需要一秒钟内过期的一百万个令牌。您可能有100个令牌在10毫秒内到期,10个令牌在1毫秒内到期,依此类推。这实际上迫使瞬时速率接近平均速率,平滑峰值,这可能会导致客户端备份,但这是自然的结果限制速率。但是,一百万RPM听起来并不像节流阀。如果您可以解释用例,我可能会有更好的主意。
埃里克森

21

阅读令牌桶算法。基本上,您有一个带有令牌的存储桶。每次执行该方法时,都会获得一个令牌。如果没有更多令牌,则阻塞直到获得一个令牌。同时,有一些外部参与者以固定的时间间隔补充令牌。

我不知道有图书馆可以做到这一点(或类似的东西)。您可以将此逻辑编写到代码中,或使用AspectJ添加行为。


3
感谢您的建议,有趣的算法。但这并不是我所需要的。例如,我需要将执行限制为每秒5个调用。如果我使用令牌桶,并且同时有10个请求进入,则前5个调用将获取所有可用令牌并立即执行,而其余5个调用将以1/5 s的固定间隔执行。在这种情况下,我只需要在1秒过去之后剩下的5个调用就可以在单个突发中执行。
vtrubnikov

5
如果您每秒将5个令牌添加到存储桶中(或5-(剩余5个)而不是每1/5秒添加1个令牌,该怎么办?
凯文2009年

@Kevin不,这仍然不会给我“滑动窗口”效果
vtrubnikov

2
@valery是的。(记住虽然盖在M中的令牌)

不需要“外部演员”。如果您在请求时间附近保留元数据,那么所有事情都可以单线程完成。
Marsellus Wallace

8

如果您需要一个可在分布式系统上运行的基于Java的滑动窗口速率限制器,则可能需要查看https://github.com/mokies/ratelimitj项目。

Redis支持的配置,将IP请求限制为每分钟50个,如下所示:

import com.lambdaworks.redis.RedisClient;
import es.moki.ratelimitj.core.LimitRule;

RedisClient client = RedisClient.create("redis://localhost");
Set<LimitRule> rules = Collections.singleton(LimitRule.of(1, TimeUnit.MINUTES, 50)); // 50 request per minute, per key
RedisRateLimit requestRateLimiter = new RedisRateLimit(client, rules);

boolean overLimit = requestRateLimiter.overLimit("ip:127.0.0.2");

有关Redis配置的更多详细信息,请参见https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis


5

这取决于应用程序。

想象一下,在这种情况下多线程想令牌做一些全球速率有限的行动不爆裂,允许(即你希望限制每10秒10次的行动,但你不想10个行动,在第一秒发生,然后保持9秒停止)。

DelayedQueue有一个缺点:线程请求令牌的顺序可能不是它们获得请求的顺序。如果阻塞了多个线程等待令牌,则不清楚哪个线程将获取下一个可用令牌。在我看来,您甚至可以让线程永远等待。

一种解决方案是在两个连续动作之间出最短的时间间隔,并以与请求相同的顺序执行动作。

这是一个实现:

public class LeakyBucket {
    protected float maxRate;
    protected long minTime;
    //holds time of last action (past or future!)
    protected long lastSchedAction = System.currentTimeMillis();

    public LeakyBucket(float maxRate) throws Exception {
        if(maxRate <= 0.0f) {
            throw new Exception("Invalid rate");
        }
        this.maxRate = maxRate;
        this.minTime = (long)(1000.0f / maxRate);
    }

    public void consume() throws InterruptedException {
        long curTime = System.currentTimeMillis();
        long timeLeft;

        //calculate when can we do the action
        synchronized(this) {
            timeLeft = lastSchedAction + minTime - curTime;
            if(timeLeft > 0) {
                lastSchedAction += minTime;
            }
            else {
                lastSchedAction = curTime;
            }
        }

        //If needed, wait for our time
        if(timeLeft <= 0) {
            return;
        }
        else {
            Thread.sleep(timeLeft);
        }
    }
}

这是什么minTime意思?它有什么作用?你能解释一下吗?
Flash

minTime是消耗完令牌后必须经过的最短时间,之后才能消耗下一个令牌。
Duarte Meneses


2

我已经实现了一个简单的节流算法,请尝试以下链接 http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

关于算法的简介,

该算法利用了Java Delayed Queue的功能。创建一个具有预期延迟的延迟对象(此处为1000 / M,表示毫秒TimeUnit)。将相同的对象放入延迟的队列中,它将为我们提供移动窗口。然后,每个方法调用之前采取了对象形成队列,采取的是阻塞调用,它只会在指定延迟后返回,并在方法调用后,不要忘了把对象放入队列更新时间(这里当前毫秒) 。

在这里,我们还可以具有多个延迟时间不同的延迟对象。这种方法还将提供高吞吐量。


6
您应该发布算法摘要。如果您的链接消失了,那么您的答案将变得毫无用处。
jwr 2012年

谢谢,我添加了简短的内容。
克里斯(Krishas)'16

1

我下面的实现可以处理任意请求时间精度,每个请求具有O(1)时间复杂度,不需要任何其他缓冲区,例如O(1)空间复杂度,此外它不需要后台线程来释放令牌,而是令牌根据自上次请求以来经过的时间释放。

class RateLimiter {
    int limit;
    double available;
    long interval;

    long lastTimeStamp;

    RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;

        available = 0;
        lastTimeStamp = System.currentTimeMillis();
    }

    synchronized boolean canAdd() {
        long now = System.currentTimeMillis();
        // more token are released since last request
        available += (now-lastTimeStamp)*1.0/interval*limit; 
        if (available>limit)
            available = limit;

        if (available<1)
            return false;
        else {
            available--;
            lastTimeStamp = now;
            return true;
        }
    }
}

0

尝试使用这种简单的方法:

public class SimpleThrottler {

private static final int T = 1; // min
private static final int N = 345;

private Lock lock = new ReentrantLock();
private Condition newFrame = lock.newCondition();
private volatile boolean currentFrame = true;

public SimpleThrottler() {
    handleForGate();
}

/**
 * Payload
 */
private void job() {
    try {
        Thread.sleep(Math.abs(ThreadLocalRandom.current().nextLong(12, 98)));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.err.print(" J. ");
}

public void doJob() throws InterruptedException {
    lock.lock();
    try {

        while (true) {

            int count = 0;

            while (count < N && currentFrame) {
                job();
                count++;
            }

            newFrame.await();
            currentFrame = true;
        }

    } finally {
        lock.unlock();
    }
}

public void handleForGate() {
    Thread handler = new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1 * 900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                currentFrame = false;

                lock.lock();
                try {
                    newFrame.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    });
    handler.start();
}

}



0

这是对上面的LeakyBucket代码的更新。每秒处理1000个以上的请求。

import lombok.SneakyThrows;
import java.util.concurrent.TimeUnit;

class LeakyBucket {
  private long minTimeNano; // sec / billion
  private long sched = System.nanoTime();

  /**
   * Create a rate limiter using the leakybucket alg.
   * @param perSec the number of requests per second
   */
  public LeakyBucket(double perSec) {
    if (perSec <= 0.0) {
      throw new RuntimeException("Invalid rate " + perSec);
    }
    this.minTimeNano = (long) (1_000_000_000.0 / perSec);
  }

  @SneakyThrows public void consume() {
    long curr = System.nanoTime();
    long timeLeft;

    synchronized (this) {
      timeLeft = sched - curr + minTimeNano;
      sched += minTimeNano;
    }
    if (timeLeft <= minTimeNano) {
      return;
    }
    TimeUnit.NANOSECONDS.sleep(timeLeft);
  }
}

和上面的单元测试:

import com.google.common.base.Stopwatch;
import org.junit.Ignore;
import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class LeakyBucketTest {
  @Test @Ignore public void t() {
    double numberPerSec = 10000;
    LeakyBucket b = new LeakyBucket(numberPerSec);
    Stopwatch w = Stopwatch.createStarted();
    IntStream.range(0, (int) (numberPerSec * 5)).parallel().forEach(
        x -> b.consume());
    System.out.printf("%,d ms%n", w.elapsed(TimeUnit.MILLISECONDS));
  }
}

这是什么minTimeNano意思?你可以解释吗?
Flash

0

这是简单的速率限制器的高级版本

/**
 * Simple request limiter based on Thread.sleep method.
 * Create limiter instance via {@link #create(float)} and call {@link #consume()} before making any request.
 * If the limit is exceeded cosume method locks and waits for current call rate to fall down below the limit
 */
public class RequestRateLimiter {

    private long minTime;

    private long lastSchedAction;
    private double avgSpent = 0;

    ArrayList<RatePeriod> periods;


    @AllArgsConstructor
    public static class RatePeriod{

        @Getter
        private LocalTime start;

        @Getter
        private LocalTime end;

        @Getter
        private float maxRate;
    }


    /**
     * Create request limiter with maxRate - maximum number of requests per second
     * @param maxRate - maximum number of requests per second
     * @return
     */
    public static RequestRateLimiter create(float maxRate){
        return new RequestRateLimiter(Arrays.asList( new RatePeriod(LocalTime.of(0,0,0),
                LocalTime.of(23,59,59), maxRate)));
    }

    /**
     * Create request limiter with ratePeriods calendar - maximum number of requests per second in every period
     * @param ratePeriods - rate calendar
     * @return
     */
    public static RequestRateLimiter create(List<RatePeriod> ratePeriods){
        return new RequestRateLimiter(ratePeriods);
    }

    private void checkArgs(List<RatePeriod> ratePeriods){

        for (RatePeriod rp: ratePeriods ){
            if ( null == rp || rp.maxRate <= 0.0f || null == rp.start || null == rp.end )
                throw new IllegalArgumentException("list contains null or rate is less then zero or period is zero length");
        }
    }

    private float getCurrentRate(){

        LocalTime now = LocalTime.now();

        for (RatePeriod rp: periods){
            if ( now.isAfter( rp.start ) && now.isBefore( rp.end ) )
                return rp.maxRate;
        }

        return Float.MAX_VALUE;
    }



    private RequestRateLimiter(List<RatePeriod> ratePeriods){

        checkArgs(ratePeriods);
        periods = new ArrayList<>(ratePeriods.size());
        periods.addAll(ratePeriods);

        this.minTime = (long)(1000.0f / getCurrentRate());
        this.lastSchedAction = System.currentTimeMillis() - minTime;
    }

    /**
     * Call this method before making actual request.
     * Method call locks until current rate falls down below the limit
     * @throws InterruptedException
     */
    public void consume() throws InterruptedException {

        long timeLeft;

        synchronized(this) {
            long curTime = System.currentTimeMillis();

            minTime = (long)(1000.0f / getCurrentRate());
            timeLeft = lastSchedAction + minTime - curTime;

            long timeSpent = curTime - lastSchedAction + timeLeft;
            avgSpent = (avgSpent + timeSpent) / 2;

            if(timeLeft <= 0) {
                lastSchedAction = curTime;
                return;
            }

            lastSchedAction = curTime + timeLeft;
        }

        Thread.sleep(timeLeft);
    }

    public synchronized float getCuRate(){
        return (float) ( 1000d / avgSpent);
    }
}

和单元测试

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RequestRateLimiterTest {


    @Test(expected = IllegalArgumentException.class)
    public void checkSingleThreadZeroRate(){

        // Zero rate
        RequestRateLimiter limiter = RequestRateLimiter.create(0);
        try {
            limiter.consume();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void checkSingleThreadUnlimitedRate(){

        // Unlimited
        RequestRateLimiter limiter = RequestRateLimiter.create(Float.MAX_VALUE);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) < 1000));
    }

    @Test
    public void rcheckSingleThreadRate(){

        // 3 request per minute
        RequestRateLimiter limiter = RequestRateLimiter.create(3f/60f);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 3; i++ ){

            try {
                limiter.consume();
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) >= 60000 ) & ((ended - started) < 61000));
    }



    @Test
    public void checkSingleThreadRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ));
    }

    @Test
    public void checkMultiThreadedRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreaded32RateLimit(){

        // 0,2 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(0.2f);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(8);
        ExecutorService exec = Executors.newFixedThreadPool(8);

        for ( int i = 0; i < 8; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 2; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreadedRateLimitDynamicRate(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {

                Random r = new Random();
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                        Thread.sleep(r.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

}

代码很简单。您只需使用maxRate或周期和比率创建限制器。然后只需调用消耗每个请求即可。只要不超过该速率,限制器就会立即返回或等待一段时间,然后返回较低的当前请求速率。它还具有当前汇率方法,该方法返回当前汇率的滑动平均值。
Leonid Astakhov '18

0

我的解决方案:一个简单的util方法,您可以对其进行修改以创建包装器类。

public static Runnable throttle (Runnable realRunner, long delay) {
    Runnable throttleRunner = new Runnable() {
        // whether is waiting to run
        private boolean _isWaiting = false;
        // target time to run realRunner
        private long _timeToRun;
        // specified delay time to wait
        private long _delay = delay;
        // Runnable that has the real task to run
        private Runnable _realRunner = realRunner;
        @Override
        public void run() {
            // current time
            long now;
            synchronized (this) {
                // another thread is waiting, skip
                if (_isWaiting) return;
                now = System.currentTimeMillis();
                // update time to run
                // do not update it each time since
                // you do not want to postpone it unlimited
                _timeToRun = now+_delay;
                // set waiting status
                _isWaiting = true;
            }
            try {
                Thread.sleep(_timeToRun-now);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // clear waiting status before run
                _isWaiting = false;
                // do the real task
                _realRunner.run();
            }
        }};
    return throttleRunner;
}

JAVA线程反跳和节流中获取

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.