Caffeine для Java: высокопроизводительный кэш в памяти
Что это и зачем нужна?
Caffeine — это высокопроизводительная библиотека для кэширования в памяти, написанная на Java. Она создана Беном Маннесом (Ben Manes) и является одной из самых быстрых реализаций кэша для JVM. Caffeine спроектирована с учетом современных требований к многопоточности и предоставляет богатый API для управления жизненным циклом объектов в кэше.
Зачем нужна Caffeine? В любом приложении, которое работает с данными, часто возникает ситуация, когда одни и те же данные запрашиваются многократно. Вместо того чтобы каждый раз выполнять дорогостоящую операцию (запрос к базе данных, вызов внешнего API, сложные вычисления), вы можете сохранить результат в кэше. Caffeine делает это максимально эффективно, используя алгоритм W-TinyLFU (Window Tiny Least Frequently Used), который обеспечивает высокую точность вытеснения элементов и минимальные накладные расходы.
Библиотека активно используется в таких крупных проектах, как Spring Framework (начиная с версии 5.0), где она является стандартной реализацией кэша. Caffeine поддерживает асинхронную загрузку, автоматическое обновление записей, статистику, и множество стратегий вытеснения (по времени, по размеру, по ссылкам).
Установка
Caffeine распространяется через Maven Central. Для использования добавьте зависимость в ваш pom.xml (Maven) или build.gradle (Gradle).
Maven
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Gradle
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
Для получения последней версии проверьте Maven Central.
Быстрый старт
Минимальный пример создания и использования кэша Caffeine:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class QuickStart {
public static void main(String[] args) {
// 1. Создаем кэш с максимальным размером 100 элементов
// и временем жизни записи 10 минут после последнего доступа
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
// 2. Кладем значение
cache.put("key1", "Hello, Caffeine!");
// 3. Получаем значение
String value = cache.getIfPresent("key1");
System.out.println(value); // Выведет: Hello, Caffeine!
// 4. Получаем значение с функцией загрузки (если ключа нет)
String loadedValue = cache.get("key2", k -> "Loaded: " + k);
System.out.println(loadedValue); // Выведет: Loaded: key2
}
}
Основные методы и классы
Рассмотрим ключевые классы и методы Caffeine. Основной класс для конфигурации — Caffeine, а для работы с кэшем — Cache, LoadingCache и AsyncCache.
1. Caffeine.newBuilder()
Сигнатура: public static Caffeine<K, V> newBuilder()
Что делает: Создает новый билдер для настройки кэша. Позволяет задать все параметры: размер, время жизни, стратегию вытеснения, слушатели и т.д.
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
2. Cache.getIfPresent(key)
Сигнатура: V getIfPresent(@NonNull Object key)
Что делает: Возвращает значение по ключу, если оно присутствует в кэше. Если ключа нет — возвращает null. Не загружает значение автоматически.
Cache<String, Double> cache = Caffeine.newBuilder().build();
cache.put("pi", 3.14159);
Double pi = cache.getIfPresent("pi"); // 3.14159
Double e = cache.getIfPresent("e"); // null
3. Cache.get(key, mappingFunction)
Сигнатура: V get(@NonNull K key, @NonNull Function<? super K, ? extends V> mappingFunction)
Что делает: Атомарная операция. Если ключ есть в кэше — возвращает значение. Если нет — выполняет переданную функцию, сохраняет результат в кэш и возвращает его. Гарантирует, что функция будет выполнена только один раз для каждого ключа (даже при конкурентном доступе).
Cache<Integer, String> cache = Caffeine.newBuilder().build();
String result = cache.get(42, key -> "Value for " + key);
System.out.println(result); // Value for 42
4. Cache.put(key, value)
Сигнатура: void put(@NonNull K key, @NonNull V value)
Что делает: Помещает пару ключ-значение в кэш. Если ключ уже существует — перезаписывает значение. Метод не возвращает предыдущее значение (для этого используйте asMap()).
Cache<String, String> cache = Caffeine.newBuilder().build();
cache.put("user:1", "Alice");
cache.put("user:1", "Bob"); // Перезапись
System.out.println(cache.getIfPresent("user:1")); // Bob
5. Cache.invalidate(key)
Сигнатура: void invalidate(@NonNull Object key)
Что делает: Удаляет запись из кэша по ключу. Если ключа нет — ничего не происходит. Полезно для принудительного удаления устаревших данных.
Cache<String, String> cache = Caffeine.newBuilder().build();
cache.put("temp", "data");
cache.invalidate("temp");
System.out.println(cache.getIfPresent("temp")); // null
6. Cache.cleanUp()
Сигнатура: void cleanUp()
Что делает: Принудительно запускает процесс очистки кэша. Обычно Caffeine выполняет очистку лениво (во время операций get/put), но этот метод позволяет немедленно удалить все истекшие записи.
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
cache.put("key", "value");
Thread.sleep(1500);
cache.cleanUp(); // Принудительная очистка
System.out.println(cache.getIfPresent("key")); // null
7. Caffeine.maximumSize(long)
Сигнатура: public Caffeine<K, V> maximumSize(long size)
Что делает: Устанавливает максимальное количество записей в кэше. При превышении лимита Caffeine вытеснит наименее используемые записи, используя алгоритм W-TinyLFU.
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(3) // Максимум 3 элемента
.build();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.put(4, "d"); // Один из предыдущих будет вытеснен
System.out.println(cache.getIfPresent(1)); // Возможно null
8. Caffeine.expireAfterWrite(long, TimeUnit)
Сигнатура: public Caffeine<K, V> expireAfterWrite(long duration, @NonNull TimeUnit unit)
Что делает: Устанавливает время жизни записи с момента ее создания или последнего обновления. По истечении этого времени запись будет удалена при следующей операции доступа или очистки.
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS) // Живет 5 секунд
.build();
cache.put("session", "token123");
Thread.sleep(6000);
System.out.println(cache.getIfPresent("session")); // null
9. Caffeine.expireAfterAccess(long, TimeUnit)
Сигнатура: public Caffeine<K, V> expireAfterAccess(long duration, @NonNull TimeUnit unit)
Что делает: Устанавливает время жизни записи с момента последнего чтения или записи. Если к записи обращаются часто, она будет жить дольше. Идеально для кэширования сессий или токенов.
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.build();
cache.put("config", "dark_mode=true");
// Если к ключу "config" обращаться хотя бы раз в минуту, он останется в кэше
10. Caffeine.recordStats()
Сигнатура: public Caffeine<K, V> recordStats()
Что делает: Включает сбор статистики использования кэша. После вызова этого метода можно получить объект CacheStats, который содержит информацию о количестве запросов, попаданий (hit), промахов (miss), загрузок и вытеснений.
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build();
cache.put("a", "1");
cache.getIfPresent("a"); // hit
cache.getIfPresent("b"); // miss
CacheStats stats = cache.stats();
System.out.println("Hit count: " + stats.hitCount()); // 1
System.out.println("Miss count: " + stats.missCount()); // 1
System.out.println("Hit rate: " + stats.hitRate()); // 0.5
11. LoadingCache (интерфейс)
Сигнатура: public interface LoadingCache<K, V> extends Cache<K, V>
Что делает: Специализированный кэш, который автоматически загружает значения с помощью переданной функции CacheLoader. Упрощает работу: не нужно вызывать get(key, func) каждый раз.
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.build(key -> "Loaded: " + key.toUpperCase());
// Метод get автоматически загрузит значение, если его нет
System.out.println(cache.get("hello")); // Loaded: HELLO
System.out.println(cache.get("hello")); // Loaded: HELLO (из кэша)
12. AsyncCache (интерфейс)
Сигнатура: public interface AsyncCache<K, V>
Что делает: Асинхронная версия кэша. Все операции возвращают CompletableFuture. Позволяет не блокировать поток при загрузке данных. Отлично подходит для работы с асинхронными API (например, WebClient).
AsyncCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.buildAsync();
CompletableFuture<String> future = cache.get("key", (k, executor) ->
CompletableFuture.supplyAsync(() -> "Async value for " + k));
future.thenAccept(System.out::println); // Async value for key
Реальный практический пример
Рассмотрим пример сервиса для работы с пользователями. Допустим, у нас есть медленный репозиторий (например, обращающийся к базе данных), и мы хотим кэшировать результаты запросов. Также мы добавим автоматическое обновление кэша каждые 10 минут, чтобы данные были свежими.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Пример сервиса с кэшированием пользователей.
* Предполагаем, что UserRepository — это медленный источник данных (БД, внешний API).
*/
public class UserService {
// Имитация медленного репозитория
static class UserRepository {
public User findById(Long id) {
//