Введение в кофеин

Введение в кофеин

1. Вступление

В этой статье мы рассмотримCaffeine - ahigh-performance caching library for Java.

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

eviction policy decides which objects should be deleted в любой момент времени. Эта политикаdirectly affects the cache’s hit rate - важнейшая характеристика кеширования библиотек.

Кофеин использует политику выселенияWindow TinyLfu, которая обеспечиваетnear-optimal hit rate.

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

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


    com.github.ben-manes.caffeine
    caffeine
    2.5.5

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

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

Давайте сосредоточимся наthree strategies for cache population кофеина: ручная, синхронная и асинхронная.

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

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 cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

Теперьwe can get some value from the cache using the getIfPresent method. Этот метод вернетnull, если значение отсутствует в кеше:

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

assertNull(dataObject);

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

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

assertNotNull(dataObject);

We can also get the value using the get method, который принимаетFunction вместе с ключом в качестве аргумента. Эта функция будет использоваться для предоставления запасного значения, если ключ отсутствует в кэше, который будет вставлен в кэш после вычисления:

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

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

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

Иногда нам нужноinvalidate some cached values вручную:

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

assertNull(dataObject);

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

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

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

LoadingCache 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 dataObjectMap
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

Значения извлекаются из базовой внутренней инициализацииFunction, которая была передана методуbuild. This makes it possible to use the cache as the main facade for accessing values.

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

Эта стратегияworks the same as the previous but performs operations asynchronously and returns a CompletableFuture удерживает фактическое значение:

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

Мы можемuse the get and getAll methods аналогичным образом, учитывая тот факт, что они возвращают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, о котором вы можете узнать больше оin this article.

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

У кофеина естьthree strategies for value eviction: на основе размера, на основе времени и на основе эталона.

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

Этот тип выселения предполагает, чтоeviction occurs when the configured size limit of the cache is exceeded. Естьtwo ways of getting the size - подсчет объектов в кеше или получение их веса.

Посмотрим, как мы можемcount objects in the cache. Когда кеш инициализируется, его размер равен нулю:

LoadingCache 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());

Следует отметить, что wecall the cleanUp method before getting the cache size. Это потому, что вытеснение кеша выполняется асинхронно, и этот методhelps to await the completion of the eviction.

Мы также можемpass a weigherFunction, чтобы получить размер кеша:

LoadingCache 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. Выселение по времени

Эта стратегия выселенияbased on the expiration time of the entry имеет три типа:

  • Expire after access - запись истекает через период, прошедший с момента последнего чтения или записи

  • Expire after write - запись истекает после того, как прошел период с момента последней записи

  • Custom policy - срок действия рассчитывается для каждой записи индивидуально реализациейExpiry

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

LoadingCache 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() {
    @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. Выселение на основании справки

Мы можем настроить наш кеш, чтобы разрешитьgarbage-collection of cache keys and/or values. Для этого мы настроим использованиеWeakRefence как для ключей, так и для значений, и мы можем настроитьSoftReference только на сборку мусора значений.

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

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

LoadingCache 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));

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

Но если запись подходит для обновления, то кеш вернет старое значение иasynchronously reload the value.

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

Кофеин имеет следующие средстваrecording statistics about cache usage:

LoadingCache 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. Заключение

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

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