«1. Введение

В этой статье мы рассмотрим Caffeine — высокопроизводительную библиотеку кэширования для Java.

Одно фундаментальное различие между кешем и картой заключается в том, что кеш удаляет сохраненные элементы.

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

Caffeine использует политику удаления Window TinyLfu, которая обеспечивает почти оптимальную частоту попаданий.

2. Зависимость

Нам нужно добавить зависимость caffeine в наш pom.xml:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>

Вы можете найти последнюю версию caffeine на Maven Central.

3. Заполнение кэша

Давайте сосредоточимся на трех стратегиях Caffeine для заполнения кэша: ручная, синхронная загрузка и асинхронная загрузка.

Во-первых, давайте напишем класс для типов значений, которые мы будем хранить в нашем кэше:

class DataObject {
    private final String data;

    private static int objectCounter = 0;
    // standard constructors/getters
    
    public static DataObject get(String data) {
        objectCounter++;
        return new DataObject(data);
    }
}

3.1. Ручное заполнение

В этой стратегии мы вручную помещаем значения в кеш и извлекаем их позже.

Давайте инициализируем наш кеш:

Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

Теперь мы можем получить некоторое значение из кеша, используя метод getIfPresent. Этот метод вернет null, если значение отсутствует в кеше:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

Мы можем заполнить кеш вручную, используя метод put:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

Мы также можем получить значение, используя метод get, который принимает функцию вместе с ключом в качестве аргумента. Эта функция будет использоваться для предоставления резервного значения, если ключ отсутствует в кеше, который будет вставлен в кеш после вычисления:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

Метод get выполняет вычисление атомарно. Это означает, что вычисление будет выполнено только один раз — даже если несколько потоков одновременно запрашивают значение. Вот почему использование get предпочтительнее, чем getIfPresent.

Иногда нам нужно сделать некоторые кэшированные значения недействительными вручную:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);

assertNull(dataObject);

3.2. Синхронная загрузка

Этот метод загрузки кеша использует функцию, которая используется для инициализации значений, аналогично методу get ручной стратегии. Давайте посмотрим, как мы можем это использовать.

Прежде всего нам нужно инициализировать наш кеш:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Теперь мы можем получить значения, используя метод get:

DataObject dataObject = cache.get(key);

assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());

Мы также можем получить набор значений, используя метод getAll:

Map<String, DataObject> dataObjectMap 
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

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

3.3. Асинхронная загрузка

Эта стратегия работает так же, как и предыдущая, но выполняет операции асинхронно и возвращает CompletableFuture, содержащий фактическое значение:

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

Мы можем использовать методы get и getAll таким же образом, принимая во внимание тот факт, что они возвращают CompletableFuture:

String key = "A";

cache.get(key).thenAccept(dataObject -> {
    assertNotNull(dataObject);
    assertEquals("Data for " + key, dataObject.getData());
});

cache.getAll(Arrays.asList("A", "B", "C"))
  .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

CompletableFuture имеет богатый и полезный API, о котором вы можете прочитать больше в этой статье.

4. Вытеснение ценностей

Caffeine имеет три стратегии вытеснения ценностей: на основе размера, на основе времени и на основе ссылок.

4.1. Выселение на основе размера

Этот тип вытеснения предполагает, что вытеснение происходит при превышении заданного ограничения размера кэша. Есть два способа получить размер — подсчет объектов в кеше или получение их веса.

Давайте посмотрим, как мы можем считать объекты в кеше. При инициализации кеша его размер равен нулю:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

Когда мы добавляем значение, размер явно увеличивается:

cache.get("A");

assertEquals(1, cache.estimatedSize());

Мы можем добавить второе значение в кеш, что приводит к удаление первого значения:

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

Стоит отметить, что мы вызываем метод cleanUp перед получением размера кеша. Это связано с тем, что вытеснение кэша выполняется асинхронно, и этот метод помогает дождаться завершения вытеснения.

Мы также можем передать функцию взвешивания, чтобы получить размер кеша:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumWeight(10)
  .weigher((k,v) -> 5)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

cache.get("A");
assertEquals(1, cache.estimatedSize());

cache.get("B");
assertEquals(2, cache.estimatedSize());

Значения удаляются из кеша, когда вес превышает 10:

cache.get("C");
cache.cleanUp();

assertEquals(2, cache.estimatedSize());

4.2. Выселение по времени

Эта стратегия выселения основана на сроке действия записи и имеет три типа:

    «Истекает после доступа — срок действия записи истекает по истечении периода, прошедшего с момента последнего чтения или записи. Истекает после записи — срок действия записи истекает по истечении периода, прошедшего с момента последней записи. запись индивидуально реализацией Expiry

Давайте настроим стратегию истечения после доступа, используя метод expireAfterAccess:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Чтобы настроить стратегию истечения срока после записи, мы используем метод expireAfterWrite:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

Чтобы инициализировать пользовательскую политику, нам нужно реализовать интерфейс Expiry:

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
    @Override
    public long expireAfterCreate(
      String key, DataObject value, long currentTime) {
        return value.getData().length() * 1000;
    }
    @Override
    public long expireAfterUpdate(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build(k -> DataObject.get("Data for " + k));

4.3. Выселение на основе ссылок

Мы можем настроить наш кеш, чтобы разрешить сборку мусора ключей и/или значений кеша. Для этого мы настроим использование WeakRefence как для ключей, так и для значений, и мы можем настроить SoftReference только для сбора мусора для значений.

Использование WeakRefence разрешает сборку мусора объектов, когда на объект нет сильных ссылок. SoftReference позволяет выполнять сборку мусора для объектов на основе глобальной стратегии JVM «наименее недавно использованные». Более подробную информацию о ссылках в Java можно найти здесь.

Мы должны использовать Caffeine.weakKeys(), Caffeine.weakValues() и Caffeine.softValues() для включения каждой опции:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .softValues()
  .build(k -> DataObject.get("Data for " + k));

5. Обновление

Можно настроить кэш для обновления записей по истечении определенного периода автоматически. Давайте посмотрим, как это сделать с помощью метода refreshAfterWrite:

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Здесь мы должны понимать разницу между expireAfter и refreshAfter. Когда запрашивается запись с истекшим сроком действия, выполнение блокируется до тех пор, пока функция сборки не вычислит новое значение.

Но если запись подходит для обновления, то кеш вернет старое значение и асинхронно перезагрузит значение.

6. Статистика

В Caffeine есть средства записи статистики об использовании кэша:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .recordStats()
  .build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");

assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());

Мы также можем передать поставщику RecordStats, который создает реализацию StatsCounter. Этот объект будет передаваться при каждом изменении, связанном со статистикой.

7. Заключение

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

Показанный здесь исходный код доступен на Github.