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;
}
}
Как это работает:
- Фильтр перехватывает все входящие запросы к
/api/*. - Для каждого уникального IP-адреса создаётся отдельная корзина (используется
ConcurrentHashMapдля потокобезопасности). - Метод
tryConsume(1)проверяет, остались ли токены. Если да — запрос проходит к контроллеру. - Если токенов нет, клиенту возвращается HTTP-статус 429 (Too Many Requests) с JSON-сообщением об ошибке.
Расширение примера: В реальном приложении вы, вероятно, захотите добавить заголовки ответа, такие как X-Rate-Limit-Remaining и X-Rate-Limit-Reset, чтобы клиенты могли адаптировать своё поведение. Для этого используйте метод tryConsumeAndReturnRemaining.
Советы по производительности и типичные ошибки
Советы по производительности
- Используйте пул корзин: Создание корзины — лёгкая операция, но хранение миллионов корзин (например, для каждого IP) может привести к проблемам с памятью. Рассмотрите использование кэша с истечением срока (например, Caffeine) для автоматической очистки неактивных корзин.
- Избегайте блокировок в высоконагруженных системах: Метод
consume()блокирует поток. В асинхронных приложениях (WebFlux) используйте реактивные обёртки Bucket4j (модульbucket4j-reactor