Rate Limiting в Java: как защитить API от перегрузок и ботов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Rate Limiting и Throttling в Java: от теории до боевого кода

Представьте: ваш новый микросервис на Java только встал на ноги, как вдруг на него обрушивается лавина запросов. Не злоумышленники, нет — просто нерадивый клиент забыл поставить задержку в своём скрипте. Результат: база данных падает, сервер задыхается, а пользователи (включая добросовестных) получают 502 Bad Gateway. Знакомо?

Сегодня мы поговорим о том, как этого избежать. Тема нашего разговора — rate limiting и throttling в Java. Это не просто модные термины из мира DevOps, а ваша первая линия обороны для любого публичного API. Мы разберём, чем отличается «ограничение скорости» от «дросселирования», рассмотрим популярные алгоритмы и, самое главное, напишем реальный код.

Если вы пишете на Java и хотите, чтобы ваш сервис оставался живым под любой нагрузкой — вы по адресу. Поехали!

Rate Limiting vs Throttling: в чём разница?

Часто эти понятия путают, хотя они решают разные, хоть и смежные, задачи.

  • Rate Limiting — это жёсткий барьер. «Не больше 10 запросов в секунду с одного IP». Превысил лимит? Получи 429 Too Many Requests. Всё честно.
  • Throttling — это более мягкий механизм. Сервис не блокирует запрос, а замедляет его обработку. Например, вы ставите запрос в очередь и обрабатываете его с фиксированной скоростью.

В реальных проектах throttling в Java часто реализуется через пулы потоков (ThreadPoolExecutor) с ограниченной пропускной способностью, а rate limiting — через счётчики, кэши и промежуточные фильтры.

Алгоритмы Rate Limiting: выбираем оружие

Прежде чем писать код, давайте выберем стратегию. Вот три самых популярных алгоритма для ограничения запросов в Java.

1. Token Bucket (Корзина токенов)

Классика. У нас есть «ведро», в которое каждую секунду капает N новых токенов. Чтобы выполнить запрос, нужно забрать один токен. Если ведро пусто — запрос отклоняется. Алгоритм отлично справляется с внезапными всплесками трафика, позволяя «занять» токены вперёд (до размера ведра).

2. Sliding Window (Скользящее окно)

Более точный метод. Вместо того чтобы считать запросы за календарную секунду, мы двигаем окно времени. Например, «не более 100 запросов за последние 60 секунд». Это предотвращает «бамп» в начале каждого интервала.

3. Fixed Window (Фиксированное окно)

Самый простой, но опасный. Сбрасываем счётчик раз в N секунд. Проблема: в конце окна можно сделать тысячу запросов, и в начале следующего — ещё тысячу. Для критичных систем не подходит.

Мы сосредоточимся на первых двух — они дают наилучший баланс между производительностью и точностью.

Пишем код: Rate Limiter своими руками

Давайте реализуем rate limiter в Java с использованием алгоритма Token Bucket. Это отличный пример для понимания того, как работают системы ограничения изнутри.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class TokenBucketRateLimiter {

    private final long maxTokens;         // Максимальный размер ведра
    private final long refillRate;        // Токенов в секунду
    private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();

    public TokenBucketRateLimiter(long maxTokens, long refillRate) {
        this.maxTokens = maxTokens;
        this.refillRate = refillRate;
    }

    public boolean tryAcquire(String clientId) {
        Bucket bucket = buckets.computeIfAbsent(clientId, k -> new Bucket(maxTokens));
        return bucket.tryConsume();
    }

    private class Bucket {
        private final AtomicLong tokens;
        private volatile long lastRefillTimestamp;

        Bucket(long capacity) {
            this.tokens = new AtomicLong(capacity);
            this.lastRefillTimestamp = System.nanoTime();
        }

        boolean tryConsume() {
            refill();
            while (true) {
                long currentTokens = tokens.get();
                if (currentTokens <= 0) {
                    return false; // Лимит исчерпан
                }
                if (tokens.compareAndSet(currentTokens, currentTokens - 1)) {
                    return true; // Токен получен
                }
            }
        }

        private void refill() {
            long now = System.nanoTime();
            long elapsed = now - lastRefillTimestamp;
            if (elapsed < TimeUnit.SECONDS.toNanos(1)) {
                return; // Прошло меньше секунды — не доливаем
            }
            // Сколько токенов нужно добавить?
            long tokensToAdd = (elapsed / TimeUnit.SECONDS.toNanos(1)) * refillRate;
            if (tokensToAdd > 0) {
                tokens.updateAndGet(current -> Math.min(maxTokens, current + tokensToAdd));
                lastRefillTimestamp = now;
            }
        }
    }
}

Использовать этот Java rate limiter проще простого:

public class ApiService {
    private final TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5);

    public void handleRequest(String userId) {
        if (!limiter.tryAcquire(userId)) {
            throw new RuntimeException("429 Too Many Requests");
        }
        // Обработка запроса
        System.out.println("Запрос от " + userId + " обработан.");
    }
}

Этот код — база. В продакшене вы захотите добавить нормальные HTTP-статусы, логирование и, возможно, использовать более производительные структуры данных.

Throttling в Java на практике

Если rate limiting — это про отказ, то throttling — про терпение. Представьте, что вы обрабатываете поток данных. Вы не можете их отбросить, но и не успеваете обработать мгновенно. Решение — Semaphore или ThreadPoolExecutor с ограничением.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class ThrottledProcessor {

    private final Semaphore throttle = new Semaphore(5); // Не более 5 одновременных задач
    private final ExecutorService executor = Executors.newFixedThreadPool(3);

    public void processTask(Runnable task) {
        executor.submit(() -> {
            try {
                throttle.acquire();
                task.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                throttle.release();
            }
        });
    }
}

Здесь Semaphore играет роль дроссельной заслонки. Если все 5 «разрешений» заняты, новый поток встанет в очередь. Это и есть классический throttling в Java.

Готовые решения: Guava и Spring Boot

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

Guava RateLimiter (Smooth Bursty)

Библиотека от Google — золотой стандарт. Она реализует алгоритм Token Buck

Блоги

Рекомендаций по книгам