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

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

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

Начать курс

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) {
            //                    

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