Перейти к основному содержимому

Знакомство с кофеином

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

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

ANDROMEDA 42

1. Введение

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

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

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

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

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

Нам нужно добавить зависимость кофеина в наш 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. Выселение по времени

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

  • Истекает после доступа — срок действия записи истекает по истечении периода с момента последнего чтения или записи.
  • Истекает после записи — срок действия записи истекает по истечении периода с момента последней записи.
  • Пользовательская политика — срок действия рассчитывается для каждой записи индивидуально реализацией срока действия .

Давайте настроим стратегию истечения срока действия после доступа, используя метод 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 .