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

Знакомство с cache2k

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

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. Загрузка кэша или сквозное чтение

В нашем примере мы использовали шаблон cache aside для загрузки кеша. Это означает, что мы рассчитали и добавили скидку в кэш по требованию в методе 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 для дальнейшего изучения библиотеки.

Как всегда, полный код этого руководства доступен на GitHub .