RateLimiter Guava в минутах вместо секунд?

Я пытаюсь ограничить количество учетных записей, которые пользователь может создать с помощью моего REST API.

Я хотел бы использовать RateLimiter в Guava, чтобы позволить IP создавать, скажем, 5 учетных записей в течение 10 минут, но метод RateLimiter.create принимает только double, указывающее количество разрешений «в секунду».

Есть ли способ настроить RateLimiter для выдачи разрешений с точностью более одной секунды?


person Johny19    schedule 30.12.2014    source источник


Ответы (6)


Из RateLimiter.create javadoc:

Когда скорость входящих запросов превышает разрешения PerSecond, ограничитель скорости будет выпускать одно разрешение каждые (1.0 / permitsPerSecond) секунды.

Таким образом, вы можете установить permitsPerSecond меньше, чем 1.0, чтобы выдавать разрешение реже, чем раз в секунду.

В вашем конкретном случае пять учетных записей за десять минут упрощаются до одной учетной записи за две минуты, что составляет одну учетную запись за 120 секунд. Вы бы передали 1.0/120 за permitsPerSecond.

В вашем случае использования вы, вероятно, захотите разместить пакетные запросы на создание учетной записи. Спецификация RateLimiter, кажется, не определяет, что происходит с неиспользованными разрешениями, но реализация по умолчанию, SmoothRateLimiter, похоже, позволяет разрешениям накапливаться до некоторого максимума, чтобы удовлетворить всплески. Этот класс не является общедоступным, поэтому документации по javadoc нет, но SmoothRateLimiter имеет длинный комментарий с подробным обсуждением текущего поведения.

person Jeffrey Bosboom    schedule 30.12.2014
comment
Спасибо за ваш ответ @Jeffrey. Но, как вы сказали, использование 1/120 на самом деле не дает 5 разрешений за 10 минут (на пользователей). потому что пользователю придется подождать 2 минуты, прежде чем создать новую учетную запись. На самом деле ему не нужно ждать, и он должен иметь возможность создать 5 учетных записей, когда захочет, в течение 10 минут. Но даже глядя на SmoothRateLimiter, он не позволяет вам это сделать, не так ли? Или может я не понял как это работает? - person Johny19; 31.12.2014
comment
Это правильно, что RateLimiter не выпускает более одного разрешения за раз, но вы можете создать своего рода оболочку, в которой одно разрешение из RateLimiter преобразуется в пять учетных записей, которые можно создать, и настроить RateLimiter для выпуска разрешения каждый раз. десять минут. - person Louis Wasserman; 31.12.2014
comment
@LouisWasserman Не могли бы вы подробнее рассказать о подходе к обертке? - person Vishal John; 23.04.2018

В библиотеке Guava есть класс с именем SmoothRateLimiter.SmoothBursty, который реализует желаемое поведение, но имеет локальный доступ к пакету, поэтому мы не можем использовать его напрямую. Также есть проблема Github, чтобы сделать доступ к этому классу общедоступным: https://github.com/google/guava/issues/1974

Если вы не хотите ждать, пока они выпустят новую версию RateLimiter, вы можете использовать отражение для создания экземпляра SmoothBursty ограничителя скорости. Что-то вроде этого должно работать:

Class<?> sleepingStopwatchClass = Class.forName("com.google.common.util.concurrent.RateLimiter$SleepingStopwatch");
Method createStopwatchMethod = sleepingStopwatchClass.getDeclaredMethod("createFromSystemTimer");
createStopwatchMethod.setAccessible(true);
Object stopwatch = createStopwatchMethod.invoke(null);

Class<?> burstyRateLimiterClass = Class.forName("com.google.common.util.concurrent.SmoothRateLimiter$SmoothBursty");
Constructor<?> burstyRateLimiterConstructor = burstyRateLimiterClass.getDeclaredConstructors()[0];
burstyRateLimiterConstructor.setAccessible(true);

RateLimiter result = (RateLimiter) burstyRateLimiterConstructor.newInstance(stopwatch, maxBurstSeconds);
result.setRate(permitsPerSecond);
return result;

Да, новая версия Guava может затормозить ваш код, но если вы готовы принять этот риск, это может быть выходом.

person Perica Milošević    schedule 06.12.2016

Вы также можете установить одно разрешение в секунду и получить 120 разрешений для каждой учетной записи.

person Dean Hiller    schedule 05.06.2015
comment
Я считаю, что это не сработает - если вы установите 1 разрешение в секунду и попытаетесь получить 120, вам никогда не удастся получить, потому что ограничение проверяется в секунду. - person Opster Elasticsearch Expert; 13.08.2015
comment
Лимит не «проверяется» в секунду. Прочитайте код гуавы. Если у вас есть 10 разрешений в секунду, гуав фактически позволяет вам делать 1 разрешение каждые 100 мс. Мы проверили все это, и, похоже, все работает отлично. - person Dean Hiller; 14.08.2015
comment
и если я помню, я верю, когда вы сначала звоните гуаве, записывает t1 и дает вам разрешение. Затем, когда вы звоните в будущем, он записывает t2 и выполняет t2-t1 и вычисляет, сколько еще разрешений может использовать пользователь. В любом случае, лучше просто прочитать и понять код для вашего варианта использования. Что нас сожгло, так это отсутствие первоначального разрыва. - person Dean Hiller; 14.08.2015
comment
Ты прав. У меня есть работа. в случае, если количество разрешений в секунду меньше 1, скорость RateLimiter гуавы должна быть установлена ​​​​на 1 разрешение в секунду, а получение должно быть для (1/скорость), что соответствует приведенному выше примеру, где скорость составляет 1/120: 1/ rate = 1 / (1/120) = 120 разрешений на получение каждого «чека». Как вы решили поддержку всплеска? Я все еще не понял, как это сделать. - person Opster Elasticsearch Expert; 14.08.2015
comment
Я создаю пул N при запуске, поэтому к тому времени, когда клиент использует его, у них есть определенный всплеск, который они могут использовать, а затем использовать как обычно. Я добавляю в пул, как только клиент берет его, в надежде, что он не используется и у него достаточно времени, чтобы заполниться всплеском (в основном взлом). - person Dean Hiller; 18.08.2015

Я думаю, что столкнулся с той же проблемой, что и в исходном вопросе, и на основе comment вот что я нарисовал:

import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;

public class Titrator {

    private final int numDosesPerPeriod;
    private final RateLimiter rateLimiter;
    private long numDosesAvailable;
    private transient final Object doseLock;

    public Titrator(int numDosesPerPeriod, Duration period) {
        this.numDosesPerPeriod = numDosesPerPeriod;
        double numSeconds = period.getSeconds() + period.getNano() / 1000000000d;
        rateLimiter = RateLimiter.create(1 / numSeconds);
        numDosesAvailable = 0L;
        doseLock = new Object();
    }

    /**
     * Consumes a dose from this titrator, blocking until a dose is available.
     */
    public void consume() {
        synchronized (doseLock) {
            if (numDosesAvailable == 0) { // then refill
                rateLimiter.acquire();
                numDosesAvailable += numDosesPerPeriod;
            }
            numDosesAvailable--;
        }
    }

}

Доза, выдаваемая титратором, аналогична разрешению ограничителя скорости. Эта реализация предполагает, что когда вы принимаете свою первую дозу, часы начинают отсчитывать период дозирования. Вы можете потреблять свои максимальные дозы за период так быстро, как хотите, но когда вы достигнете своего максимума, вам придется подождать, пока не истечет период, прежде чем вы сможете получить еще одну дозу.

Для tryConsume() аналога tryAcquire RateLimiter вы должны проверить, что numDosesAvailable является положительным.

person user4851    schedule 09.08.2017

На случай, если вы пропустите это, RateLimiter указывает, что случилось с неиспользованным разрешением. По умолчанию неиспользуемая ссылка сохраняется до одной минуты Ограничитель скорости.

person MidTierDeveloper    schedule 10.10.2016
comment
Javadoc говорит The default RateLimiter configuration can save the unused permits of up to one second - person Vytenis Bivainis; 06.03.2018

Наш обходной путь для этого — создать класс RateLimiter самостоятельно и изменить единицы времени. Например, в нашем случае мы хотим сделать дневной лимит скорости.

Все то же самое, что и RateLimiter, за исключением функции получения (разрешений), где мы изменили единицу времени в (двойной) TimeUnit.SECONDS.toMicros (1L) на желаемую единицу. В нашем случае мы меняем его на TimeUnit.Day для дневных лимитов.

Затем мы создаем собственный плавный RateLimiter и в функциях doSetRate(double PerDaysPerDay, long nowMicros) и doGetRate() также меняем единицу измерения времени.

person Martin Z    schedule 12.02.2019