Java thottling机制

时间:2013-02-01 01:35:49

标签: java multithreading locking mutex throttling

更新:我使用的是Java 1.6.34,无法升级到Java 7。

我有一个场景,我只允许每分钟调用80次方法。它实际上是由第三方编写的服务API,如果你多次调用它,它会“关闭”(忽略调用)它的API:

public class WidgetService {
    // Can only call this method 80x/min, otherwise it
    // it just doesn't do anything
    public void doSomething(Fizz fizz);
}

我想写一个ApiThrottler类,它有一个boolean canRun()方法,告诉我的Java客户端是否可以调用doSomething(Fizz)方法。 (当然它总是可以被称为,但如果我们超过了我们的费率就没有任何意义。)

这样可以让我编写这样的代码:

// 80x/min
ApiThrottler throttler = new ApiThrottler(80);

WidgetService widgetService = new DefaultWidgetService();

// Massive list of Fizzes
List<Fizz> fizzes = getFizzes();

for(Fizz fizz : fizzes)
    if(throttler.canRun())
        widgetService.doSomething(fizz);

这不一定是API(ApiThrottler#canRun),但是我需要一个可靠/可靠的机制,在WidgetService#doSomething(Fizz) 可以之前暂停/休眠调用。

这让我感觉就像我们正在进入使用多个线程的领域一样,这让我感觉感觉就像我们可以使用某种锁定机制和Java一样通知(wait() / notify())模型。但是在这个领域没有经验,我似乎无法绕过最优雅的解决方案。提前谢谢。

4 个答案:

答案 0 :(得分:5)

可能最好的选择之一就是使用Semaphore http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html课程,每分钟给它80个许可证。这可以通过使用计时器类http://docs.oracle.com/javase/7/docs/api/java/util/Timer.html来完成。 每次通过调用信号量上的acquire()执行对服务的调用时,调用者线程将使用许可证,如果所有许可证已经耗尽,则会阻塞该函数。

如你所提到的,当然可以使用wait / notify和整数计数器与计时器或单独的线程进行编码,但与使用更现代的java.util.concurrent API相比,这会更复杂。我已在上面概述过。

它可以看起来接近以下内容:

class Throttler implements TimerTask {
  final Semaphore s = new Semaphore(80);  
  final Timer timer = new Timer(true);

  Throttler() {
    timer.schedule(this, 0, 60*1000);   //schedule this for 1 min execution  
  }

  run() {  //called by timer 
    s.release(80 - s.availablePermits());
  }

  makeCall() {
    s.acquire();
    doMakeCall();
  }

}

这应该从Java 5开始。

更好的解决方案是使用Guava的com.google.common.util.concurrent.RateLimiter。它看起来像这样:

class Throttler {
  final RateLimiter rateLimiter = RateLimiter.create(80.0/60.0);

  makeCall() {
    rateLimiter.acquire();
    doMakeCall();
  }
}

与Semaphore解决方案相比,语义略有不同,RateLimiter最适合您的情况。

答案 1 :(得分:1)

我最近写过类似的东西。唯一的变化是我的代码在函数运行完毕时需要回调。所以如果它无法运行,我会直接调用回调。

另外一个变化是,由于此调用可能是异步的,因此当您第二次调用它时,可能正在进行调用。在这种情况下,我只是忽略了这个电话。

我的throttler有一个名为call的辅助函数,它接受函数调用和回调。这是在C ++中,所以对于你来说,它将采用Action和侦听器接口的形式。

这比基于Semaphore的解决方案更有优势,它可以序列化请求,这样您就不会经常调用它

interface Callback{
    public void OnFunctionCalled();
}

class APIThrottler
   //ctor etc
   boolean CanCall();

   public boolean IsInProgress();

   public void SetInProgress(boolean inProgress = true);

   public void Mark(){/*increment counter*/; SetInProgress(false);} // couldnt think of a better name..

   public void Call(Callable event, Callback callback){
        If(IsInProgress())
            return;
        else if(CanCall())
        {
            SetInProgress();
            event.Call();
        }
        else
            callback.OnFunctionCalled();

    }
}

一旦函数回调(或者在函数内部,如果它是同步的),一旦完成,你需要Mark()

这在很大程度上是我的实现,唯一的区别是我在x秒(或分钟)内处理了一次。

答案 2 :(得分:0)

这可以通过使用简单的计数器来实现。我们将其称为“许可”计数器,在80处初始化。在您调用“该功能”之前,首先尝试获取许可证。完成后,将其释放。然后设置一个重复计时器,每秒将计数器重置为80.

class Permit extends TimerTask
{
    private Integer permit = 80;

    private synchronized void resetPermit() { this.permit = 80; }

    public synchronized boolean acquire() {
        if(this.permit == 0) return false;
        this.permit--;
        return true;
    }

    public synchronized void release() { this.permit++; }

    @Override public void run() { resetPermit(); }
}

将计时器设置为每秒重置许可,如下所示。 schedule方法的第一个参数是TimerTask的一个实例(传递上面的Permit类对象)。对于第二个参数(此处为1000毫秒/ 1秒)指定的每个句点,将调用run()方法。 'true'参数表示这是一个守护程序计时器,它将继续重复。

Timer timer = new Timer(true);
timer.schedule(permit, 1000);

然后,每当您需要呼叫您的功能时,首先检查您是否可以获得任何许可。完成后不要忘记发布

void myFunction() {
  if(!permit.acquire()) {
      System.out.println("Nah.. been calling this 80x in the past 1 sec");
      return;
  }

  // do stuff here

  permit.release();
}

注意在上面的Permit类方法中使用synchronized关键字 - 这应该避免多于1个线程同时执行同一个对象实例的任何方法

答案 3 :(得分:0)

您可以保留时间记录,并确保最后一分钟内不超过80条记录。

// first=newest, last=oldest
final LinkedList<Long> records = new LinkedList<>();

synchronized public void canRun() throws InterruptedException
{
    while(true)
    {
        long now = System.currentTimeMillis();
        long oneMinuteAgo = now - 60000;

        // remove records older than one minute
        while(records.getLast() < oneMinuteAgo)
            records.removeLast();

        if(records.size()<80) // less than 80 records in the last minute
        {
            records.addFirst(now);
            return;   // can run
        }

        // wait for the oldest record to expire, then check again
        wait( 1 + records.getLast() - oneMinuteAgo);
    }
}

有可能在第0秒,我们发出80个电话,等待一分钟,然后在第60秒,我们再发出80个电话。由于两端的时钟不精确或网络随机延迟,另一端可能碰巧在一分钟内测量160个呼叫。为了安全起见,请放大时间窗口,比如每70秒拨打80个电话。