«1. Обзор

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

2. О cache2k

Библиотека cache2k обеспечивает быстрое время доступа благодаря неблокирующему доступу к кэшированным значениям без ожидания. Он также поддерживает интеграцию с Spring Framework, Scala Cache, Datanucleus и Hibernate.

Библиотека имеет множество функций, включая набор потокобезопасных атомарных операций, загрузчик кеша с блокировкой сквозного чтения, автоматическое истечение срока действия, упреждающее обновление, прослушиватели событий и поддержку JCache-реализации JSR107 API. Мы обсудим некоторые из этих функций в этом руководстве.

Важно отметить, что cache2k не является распределенным решением для кэширования, таким как Infispan или Hazelcast.

3. Зависимость Maven

Чтобы использовать cache2k, нам нужно сначала добавить зависимость cache2k-base-bom в наш pom.xml:

<dependency>
    <groupId>org.cache2k</groupId>
    <artifactId>cache2k-base-bom</artifactId>
    <version>1.2.3.Final</version>
    <type>pom</type>
</dependency>

4. Простой пример cache2k

Теперь давайте посмотрите, как мы можем использовать cache2k в приложении Java, на простом примере.

Давайте рассмотрим пример интернет-магазина. Предположим, что веб-сайт предлагает 20-процентную скидку на все спортивные товары и 10-процентную скидку на другие товары. Наша цель здесь — кэшировать скидку, чтобы мы не вычисляли ее каждый раз.

Итак, сначала мы создадим класс ProductHelper и создадим простую реализацию кэша:

public class ProductHelper {

    private Cache<String, Integer> cachedDiscounts;
    private int cacheMissCount = 0;

    public ProductHelper() {
        cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
          .name("discount")
          .eternal(true)
          .entryCapacity(100)
          .build();
    }

    public Integer getDiscount(String productType) {
        Integer discount = cachedDiscounts.get(productType);
        if (Objects.isNull(discount)) {
            cacheMissCount++;
            discount = "Sports".equalsIgnoreCase(productType) ? 20 : 10;
            cachedDiscounts.put(productType, discount);
        }
        return discount;
    }

    // Getters and setters

}

Как мы видим, мы использовали переменную cacheMissCount для подсчета количества раз, когда скидка не найдена в кеш. Итак, если метод getDiscount использует кеш для получения скидки, cacheMissCount не изменится.

Далее мы напишем тестовый пример и проверим нашу реализацию:

@Test
public void whenInvokedGetDiscountTwice_thenGetItFromCache() {
    ProductHelper productHelper = new ProductHelper();
    assertTrue(productHelper.getCacheMissCount() == 0);
    
    assertTrue(productHelper.getDiscount("Sports") == 20);
    assertTrue(productHelper.getDiscount("Sports") == 20);
    
    assertTrue(productHelper.getCacheMissCount() == 1);
}

Наконец, давайте кратко рассмотрим используемые нами конфигурации.

Первый — это метод name, который устанавливает уникальное имя нашего кеша. Имя кеша является необязательным и генерируется, если мы его не указываем.

Затем мы установили для параметра ever значение true, чтобы указать, что срок действия кэшированных значений не истекает со временем. Таким образом, в этом случае мы можем явно удалить элементы из кеша. В противном случае элементы будут автоматически вытеснены, как только кеш достигнет своей емкости.

Кроме того, мы использовали метод entryCapacity, чтобы указать максимальное количество записей, хранимых в кеше. Когда кеш достигает максимального размера, алгоритм вытеснения кеша удалит одну или несколько записей, чтобы сохранить указанную емкость.

Мы можем дополнительно изучить другие доступные конфигурации в классе Cache2kBuilder.

5. Возможности cache2k

Теперь давайте расширим наш пример, чтобы изучить некоторые возможности cache2k.

5.1. Настройка срока действия кэша

До сих пор мы разрешали фиксированную скидку на все спортивные товары. Однако наш веб-сайт теперь хочет, чтобы скидка была доступна только в течение фиксированного периода времени.

Чтобы выполнить это новое требование, мы настроим срок действия кэша с помощью метода expireAfterWrite:

cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
  // other configurations
  .expireAfterWrite(10, TimeUnit.MILLISECONDS)
  .build();

Теперь напишем тестовый пример для проверки срока действия кэша:

@Test
public void whenInvokedGetDiscountAfterExpiration_thenDiscountCalculatedAgain() 
  throws InterruptedException {
    ProductHelper productHelper = new ProductHelper();
    assertTrue(productHelper.getCacheMissCount() == 0);
    assertTrue(productHelper.getDiscount("Sports") == 20);
    assertTrue(productHelper.getCacheMissCount() == 1);

    Thread.sleep(20);

    assertTrue(productHelper.getDiscount("Sports") == 20);
    assertTrue(productHelper.getCacheMissCount() == 2);
}

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

Для расширенной конфигурации срока действия кэша мы также можем настроить ExpiryPolicy.

5.2. Загрузка кеша или сквозное чтение

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

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

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

cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
  // other configurations
  .loader((key) -> {
      cacheMissCount++;
      return "Sports".equalsIgnoreCase(key) ? 20 : 10;
  })
  .build();

Также удалим логику расчета и обновления скидки из getDiscount:

public Integer getDiscount(String productType) {
    return cachedDiscounts.get(productType);
}

«

@Test
public void whenInvokedGetDiscount_thenPopulateCacheUsingLoader() {
    ProductHelper productHelper = new ProductHelper();
    assertTrue(productHelper.getCacheMissCount() == 0);

    assertTrue(productHelper.getDiscount("Sports") == 20);
    assertTrue(productHelper.getCacheMissCount() == 1);

    assertTrue(productHelper.getDiscount("Electronics") == 10);
    assertTrue(productHelper.getCacheMissCount() == 2);
}

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

5.3. Слушатели событий

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

.addListener(new CacheEntryCreatedListener<String, Integer>() {
    @Override
    public void onEntryCreated(Cache<String, Integer> cache, CacheEntry<String, Integer> entry) {
        LOGGER.info("Entry created: [{}, {}].", entry.getKey(), entry.getValue());
    }
})

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

Entry created: [Sports, 20].

Теперь мы можем выполнить любой из созданных нами тестовых случаев и проверить журнал:

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

5.4. Атомарные операции

Класс Cache имеет множество методов, поддерживающих атомарные операции. Эти методы предназначены для операций только с одной записью.

Среди таких методов есть containsAndRemove, putIfAbsent, removeIfEquals, replaceIfEquals, peekAndReplace и peekAndPut.

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

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