限制方法在N秒内调用M个请求

vtr*_*kov 128 java throttling

我需要一个组件/类来限制某些方法的执行到N秒内的最大M次调用(或ms或nanos,无关紧要).

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

如果您不知道现有的课程,请随时发布您的解决方案/想法如何实现这一点.

Mic*_*rdt 76

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

  • 这就是你使用java.util.concurrent中的DelayQueue的原因.它可以防止多个线程作用于同一条目的问题. (5认同)
  • 对于多线程情况,我认为令牌桶方法可能是更好的选择. (5认同)
  • 可爱.正是我需要的.快速尝试显示~10行实现此目的并且内存占用量最小.只需要考虑线程安全和传入请求的排队. (4认同)
  • @MichaelBorgwardt“如果过去的时间少于 N 秒”。您的意思是_更多_吗?您不是打算仅在指定时间过后才打电话吗? (2认同)

sch*_*rer 76

对我来说开箱即用的是Google Guava RateLimiter.

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

private void someMethod() {
    throttle.acquire();
    // Do something
}
Run Code Online (Sandbox Code Playgroud)

  • 我不推荐这个解决方案,因为Guava RateLimiter会阻塞线程,这将很容易耗尽线程池. (16认同)
  • @kaviddiss如果你不想阻止那么使用`tryAquire()` (14认同)
  • 当前实施RateLimiter的问题(至少对我而言)是它不允许大于1秒的时间段,因此例如每分钟1的速率. (6认同)
  • @John B据我所知,你可以使用RateLimiter.create(60.0)+ rateLimiter.acquire(60)通过RateLimiter每分钟获得1个​​请求 (2认同)
  • @radiantRazor Ratelimiter.create(1.0 / 60)和acquire()每分钟实现1次呼叫。 (2认同)
  • 如果您使用它,请仔细考虑它在过去未充分利用的情况下的表现。您可能会出现允许比预期更多的调用的突发。 (2认同)
  • 作为替代方案,您可以使用 apache.commons 中的 TimedSemaphore :https://commons.apache.org/proper/commons-lang/javadocs/api-release/org/apache/commons/lang3/concurrent/TimedSemaphore.html (2认同)

eri*_*son 30

具体而言,您应该能够使用a实现此功能DelayQueue.使用M Delayed最初设置为零的实例初始化队列.当对方法的请求进入时,会出现take一个令牌,导致该方法阻塞,直到满足限制要求为止.当一个令牌被占用时,add一个新的令牌延迟到队列N.

  • 实际上,在这个应用程序中,由于添加到堆中的新元素几乎总是堆中的最大元素(即具有最长延迟),因此通常需要每次添加一次比较.此外,如果算法正确实现,数组将永远不会增长,因为只有在获取一个元素后才添加一个元素. (5认同)
  • 我发现这也很有用,如果你不希望请求发生大爆发,保持大小M和延迟N按照少数毫秒的顺序相对较小.例如.M = 5,N = 20ms将提供250 /秒的键控脉冲突发,大小为5. (3认同)

Kev*_*vin 21

阅读令牌桶算法.基本上,你有一个带有令牌的桶.每次执行该方法时,都会获取一个令牌.如果没有更多令牌,则阻止直到获得一个令牌.同时,有一些外部演员以固定的间隔补充令牌.

我不知道有一个库(或类似的东西).您可以将此逻辑写入代码或使用AspectJ添加行为.

  • 如果你每秒向桶中添加5个令牌(或者每5秒钟5 - (5个剩余)而不是每1/5秒1个怎么办? (5认同)
  • 感谢您的建议,有趣的算法.但它不完全是我需要的.例如,我需要将执行限制为每秒5次调用.如果我使用令牌桶并且10个请求同时进入,前5个呼叫将占用所有可用令牌并暂时执行,而剩余的5个呼叫将以1/5秒的固定间隔执行.在这种情况下,我需要剩余的5次呼叫才能在1秒后通过单次突发执行. (3认同)
  • @valery是的.(记得在M处盖上代币) (2认同)

小智 7

如果您需要在分布式系统上运行的基于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");
Run Code Online (Sandbox Code Playgroud)

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


Dua*_*ses 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


ton*_*ywl 5

我下面的实现可以处理任意请求时间精度,它对每个请求都有 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;

        lastTimeStamp = now;
        if (available<1)
            return false;
        else {
            available--;
            return true;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)