Bucket4j для Java: полное руководство с примерами

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

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

Начать курс

Bucket4j для Java: Полное руководство по Rate Limiting

В современной разработке программного обеспечения, особенно при создании высоконагруженных веб-сервисов и API, критически важно контролировать интенсивность поступающих запросов. Без такого контроля один недобросовестный пользователь или баг в системе может привести к отказу в обслуживании (DoS) для всех остальных клиентов. Именно здесь на помощь приходит концепция Rate Limiting (ограничения скорости), а библиотека Bucket4j является одним из самых мощных и гибких инструментов для её реализации на платформе Java.

Bucket4j — это потокобезопасная, основанная на алгоритме Token Bucket (корзина токенов) библиотека для Java. Она позволяет разработчикам легко и эффективно ограничивать частоту выполнения операций: количество HTTP-запросов к API, вызовов базы данных, отправку сообщений и т.д. В отличие от простых счётчиков, алгоритм Token Bucket сглаживает нагрузку, позволяя кратковременные всплески трафика, но строго соблюдая среднюю скорость в долгосрочной перспективе. Это делает его идеальным выбором для большинства реальных сценариев.

Зачем использовать Bucket4j? Во-первых, это защита вашего бэкенда от перегрузок и злоумышленников. Во-вторых, это возможность внедрить многоуровневые тарифные планы (premium-пользователи получают больше запросов в секунду). В-третьих, библиотека предоставляет богатый API для тонкой настройки, включая различные стратегии восстановления токенов, работу с распределёнными системами через Redis или Hazelcast, и интеграцию с популярными фреймворками, такими как Spring Boot. Bucket4j — это не просто "заглушка", а полноценный производственный инструмент.

Установка

Bucket4j распространяется через центральный репозиторий Maven (Maven Central). Для подключения библиотеки к вашему проекту необходимо добавить зависимость в файл pom.xml (для Maven) или build.gradle (для Gradle).

Maven (pom.xml):

<dependency>
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.7.0</version>
</dependency>

Gradle (build.gradle):

implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:8.7.0'

Важно: Версия 8.x требует Java 11 или выше. Если вы используете Java 8, вам потребуется версия 7.x. Для работы с распределёнными кэшами (Redis, Hazelcast) необходимо добавить соответствующие модули, например bucket4j-redis.

Быстрый старт: Минимальный рабочий пример

Давайте создадим простейший ограничитель, который позволяет выполнять не более 10 операций в секунду.

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

import java.time.Duration;

public class QuickStartExample {

    public static void main(String[] args) {
        // 1. Конфигурация: 10 токенов, пополнение по 10 токенов каждую секунду
        Bandwidth limit = Bandwidth.classic(10, Refill.greedy(10, Duration.ofSeconds(1)));

        // 2. Создание корзины
        Bucket bucket = Bucket.builder()
                .addLimit(limit)
                .build();

        // 3. Симуляция запросов
        for (int i = 1; i <= 15; i++) {
            boolean consumed = bucket.tryConsume(1);
            if (consumed) {
                System.out.println("Запрос " + i + ": Успешно (токен получен)");
            } else {
                System.out.println("Запрос " + i + ": ОТКАЗАНО (превышен лимит)");
            }
        }
    }
}

Этот код создаёт корзину на 10 токенов. Каждую секунду в неё добавляется 10 новых токенов (greedy refill — токены добавляются непрерывно, а не пачкой в конце интервала). Первые 10 запросов будут выполнены мгновенно, а 11-й получит отказ, так как токены ещё не восстановились.

Основные классы и методы

Для эффективной работы с Bucket4j необходимо понимать её ключевые компоненты. Ниже приведены 8 основных элементов API с примерами.

1. Bandwidth (Пропускная способность)

Сигнатура: public static Bandwidth classic(long capacity, Refill refill)

Описание: Определяет лимит скорости. capacity — это максимальное количество токенов, которое может быть накоплено в корзине (размер "ведра"). refill — стратегия пополнения токенов.

// Лимит: 5 запросов в минуту
Bandwidth limit = Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1)));

2. Refill (Пополнение)

Сигнатура: public static Refill greedy(long tokens, Duration period)

Описание: Определяет, как и когда токены возвращаются в корзину. greedy — токены добавляются непрерывно (каждую наносекунду). intervally — токены добавляются одной пачкой в конце интервала.

// Непрерывное пополнение: 100 токенов в секунду
Refill greedyRefill = Refill.greedy(100, Duration.ofSeconds(1));

// Интервальное пополнение: 100 токенов каждую секунду (в начале)
Refill intervallyRefill = Refill.intervally(100, Duration.ofSeconds(1));

3. Bucket (Корзина)

Сигнатура: public static BucketBuilder builder()

Описание: Основной класс для создания корзины. Использует паттерн Builder для конфигурации.

Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.classic(10, Refill.greedy(10, Duration.ofSeconds(1))))
    .build();

4. tryConsume (Попытка потребить)

Сигнатура: public boolean tryConsume(long numTokens)

Описание: Пытается извлечь указанное количество токенов из корзины. Возвращает true, если токены были успешно извлечены, и false — если нет (без блокировки потока).

if (bucket.tryConsume(1)) {
    System.out.println("Запрос разрешён");
} else {
    System.out.println("Слишком много запросов");
}

5. consume (Потребление с блокировкой)

Сигнатура: public void consume(long numTokens)

Описание: Блокирует текущий поток до тех пор, пока в корзине не появится достаточное количество токенов. Полезно для реализации "честной" очереди.

try {
    bucket.consume(1); // Поток будет ждать, пока не появится токен
    System.out.println("Запрос выполнен после ожидания");
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

6. tryConsumeAndReturnRemaining (Попытка с остатком)

Сигнатура: public ConsumptionProbe tryConsumeAndReturnRemaining(long numTokens)

Описание: Пытается извлечь токены и возвращает объект ConsumptionProbe, который содержит информацию об успехе, количестве оставшихся токенов и времени ожидания до следующего пополнения.

ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
    System.out.println("Осталось токенов: " + probe.getRemainingTokens());
} else {
    long nanosToWait = probe.getNanosToWaitForRefill();
    System.out.println("Подождите " + nanosToWait / 1_000_000 + " мс");
}

7. getAvailableTokens (Доступные токены)

Сигнатура: public long getAvailableTokens()

Описание: Возвращает текущее количество токенов, доступных для потребления в данный момент.

long available = bucket.getAvailableTokens();
System.out.println("Текущий запас токенов: " + available);

8. addLimit (Добавление лимита)

Сигнатура: public BucketBuilder addLimit(Bandwidth bandwidth)

Описание: Позволяет добавить несколько лимитов в одну корзину. Это реализует концепцию "многоуровневого" Rate Limiting (например, 100 запросов в секунду, но не более 1000 в час).

Bucket multiLimitBucket = Bucket.builder()
    .addLimit(Bandwidth.classic(10, Refill.greedy(10, Duration.ofSeconds(1))))  // 10 rps
    .addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1)))) // 100 rpm
    .build();

// Запрос будет отклонён, если превышен хотя бы один лимит
boolean result = multiLimitBucket.tryConsume(1);

Реальный практический пример: Защита REST API в Spring Boot

Рассмотрим полноценный пример интеграции Bucket4j в веб-приложение на Spring Boot. Мы создадим фильтр, который ограничивает количество запросов от одного IP-адреса до 5 запросов в минуту.

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// 1. Конфигурационный класс для создания корзин
@Component
public class RateLimitingFilter implements Filter {

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Получаем IP-адрес клиента
        String clientIp = httpRequest.getRemoteAddr();

        // Получаем или создаём корзину для этого IP
        Bucket bucket = buckets.computeIfAbsent(clientIp, this::createNewBucket);

        // Пытаемся потребить 1 токен
        if (bucket.tryConsume(1)) {
            // Лимит не превышен, передаём запрос дальше
            chain.doFilter(request, response);
        } else {
            // Лимит превышен, возвращаем 429 Too Many Requests
            httpResponse.setStatus(429);
            httpResponse.getWriter().write("{\"error\": \"Too many requests. Please try again later.\"}");
        }
    }

    private Bucket createNewBucket(String clientIp) {
        // Лимит: 5 запросов в минуту
        Bandwidth limit = Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1)));
        return Bucket.builder()
                .addLimit(limit)
                .build();
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Инициализация не требуется
    }

    @Override
    public void destroy() {
        // Очистка не требуется
    }
}

// 2. Простой REST-контроллер
@RestController
@RequestMapping("/api")
public class ApiController {

    @GetMapping("/data")
    public ResponseEntity<String> getData() {
        return ResponseEntity.ok("{\"message\": \"This is protected data\"}");
    }
}

// 3. Регистрация фильтра (если не используете @ComponentScan)
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<RateLimitingFilter> rateLimitingFilter() {
        FilterRegistrationBean<RateLimitingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new RateLimitingFilter());
        registrationBean.addUrlPatterns("/api/*");
        return registrationBean;
    }
}

Как это работает:

  1. Фильтр перехватывает все входящие запросы к /api/*.
  2. Для каждого уникального IP-адреса создаётся отдельная корзина (используется ConcurrentHashMap для потокобезопасности).
  3. Метод tryConsume(1) проверяет, остались ли токены. Если да — запрос проходит к контроллеру.
  4. Если токенов нет, клиенту возвращается HTTP-статус 429 (Too Many Requests) с JSON-сообщением об ошибке.

Расширение примера: В реальном приложении вы, вероятно, захотите добавить заголовки ответа, такие как X-Rate-Limit-Remaining и X-Rate-Limit-Reset, чтобы клиенты могли адаптировать своё поведение. Для этого используйте метод tryConsumeAndReturnRemaining.

Советы по производительности и типичные ошибки

Советы по производительности

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