Руководство по Infinispan на Java

Руководство по Infinispan на Java

1. обзор

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

Чтобы понять, как это работает, мы создадим простой проект, демонстрирующий наиболее распространенные функции, и проверим, как их можно использовать.

2. Настройка проекта

Чтобы использовать его таким образом, нам нужно добавить его зависимость в нашиpom.xml.

Последнюю версию можно найти в репозиторииMaven Central:


    org.infinispan
    infinispan-core
    9.1.5.Final

Теперь вся необходимая базовая инфраструктура будет обрабатываться программно.

3. CacheManager Настройка

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

Infinispan поставляется с действительно простым способом созданияCacheManager:

public DefaultCacheManager cacheManager() {
    return new DefaultCacheManager();
}

Теперь мы можем создавать с его помощью наши кеши.

4. Настройка кешей

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

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

public class HelloWorldRepository {
    public String getHelloWorld() {
        try {
            System.out.println("Executing some heavy query");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // ...
            e.printStackTrace();
        }
        return "Hello World!";
    }
}

Кроме того, чтобы иметь возможность проверять изменения в наших кэшах, Infinispan предоставляет простую аннотацию@Listener.

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

@Listener
public class CacheListener {
    @CacheEntryCreated
    public void entryCreated(CacheEntryCreatedEvent event) {
        this.printLog("Adding key '" + event.getKey()
          + "' to cache", event);
    }

    @CacheEntryExpired
    public void entryExpired(CacheEntryExpiredEvent event) {
        this.printLog("Expiring key '" + event.getKey()
          + "' from cache", event);
    }

    @CacheEntryVisited
    public void entryVisited(CacheEntryVisitedEvent event) {
        this.printLog("Key '" + event.getKey() + "' was visited", event);
    }

    @CacheEntryActivated
    public void entryActivated(CacheEntryActivatedEvent event) {
        this.printLog("Activating key '" + event.getKey()
          + "' on cache", event);
    }

    @CacheEntryPassivated
    public void entryPassivated(CacheEntryPassivatedEvent event) {
        this.printLog("Passivating key '" + event.getKey()
          + "' from cache", event);
    }

    @CacheEntryLoaded
    public void entryLoaded(CacheEntryLoadedEvent event) {
        this.printLog("Loading key '" + event.getKey()
          + "' to cache", event);
    }

    @CacheEntriesEvicted
    public void entriesEvicted(CacheEntriesEvictedEvent event) {
        StringBuilder builder = new StringBuilder();
        event.getEntries().forEach(
          (key, value) -> builder.append(key).append(", "));
        System.out.println("Evicting following entries from cache: "
          + builder.toString());
    }

    private void printLog(String log, CacheEntryEvent event) {
        if (!event.isPre()) {
            System.out.println(log);
        }
    }
}

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

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

private  Cache buildCache(
  String cacheName,
  DefaultCacheManager cacheManager,
  CacheListener listener,
  Configuration configuration) {

    cacheManager.defineConfiguration(cacheName, configuration);
    Cache cache = cacheManager.getCache(cacheName);
    cache.addListener(listener);
    return cache;
}

Обратите внимание, как мы передаем конфигурациюCacheManager, а затем используем тот жеcacheName, чтобы получить объект, соответствующий желаемому кешу. Обратите внимание, как мы информируем слушателя о самом объекте кэша.

Теперь мы проверим пять различных конфигураций кеша и посмотрим, как их настроить и максимально эффективно использовать.

4.1. Простой кеш

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

public Cache simpleHelloWorldCache(
  DefaultCacheManager cacheManager,
  CacheListener listener) {
    return this.buildCache(SIMPLE_HELLO_WORLD_CACHE,
      cacheManager, listener, new ConfigurationBuilder().build());
}

Теперь мы можем построитьService:

public String findSimpleHelloWorld() {
    String cacheKey = "simple-hello";
    return simpleHelloWorldCache
      .computeIfAbsent(cacheKey, k -> repository.getHelloWorld());
}

Обратите внимание, как мы используем кеш, сначала проверяя, кешируется ли уже требуемая запись. Если это не так, нам нужно будет вызвать нашRepository, а затем кэшировать его.

Давайте добавим в наши тесты простой метод для определения времени наших методов:

protected  long timeThis(Supplier supplier) {
    long millis = System.currentTimeMillis();
    supplier.get();
    return System.currentTimeMillis() - millis;
}

Тестируя это, мы можем проверить время между выполнением двух вызовов методов:

@Test
public void whenGetIsCalledTwoTimes_thenTheSecondShouldHitTheCache() {
    assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
      .isLessThan(100);
}

4.2. Срок действия кеша

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

private Configuration expiringConfiguration() {
    return new ConfigurationBuilder().expiration()
      .lifespan(1, TimeUnit.SECONDS)
      .build();
}

Теперь мы строим наш кеш, используя приведенную выше конфигурацию:

public Cache expiringHelloWorldCache(
  DefaultCacheManager cacheManager,
  CacheListener listener) {

    return this.buildCache(EXPIRING_HELLO_WORLD_CACHE,
      cacheManager, listener, expiringConfiguration());
}

И, наконец, используйте его аналогичным способом из нашего простого кэша выше:

public String findSimpleHelloWorldInExpiringCache() {
    String cacheKey = "simple-hello";
    String helloWorld = expiringHelloWorldCache.get(cacheKey);
    if (helloWorld == null) {
        helloWorld = repository.getHelloWorld();
        expiringHelloWorldCache.put(cacheKey, helloWorld);
    }
    return helloWorld;
}

Давайте еще раз проверим наши времена:

@Test
public void whenGetIsCalledTwoTimesQuickly_thenTheSecondShouldHitTheCache() {
    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isLessThan(100);
}

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

@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
  throws InterruptedException {

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    Thread.sleep(1100);

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);
}

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

Executing some heavy query
Adding key 'simple-hello' to cache
Expiring key 'simple-hello' from cache
Executing some heavy query
Adding key 'simple-hello' to cache

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

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

simpleHelloWorldCache.put(cacheKey, helloWorld, 10, TimeUnit.SECONDS);

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

simpleHelloWorldCache.put(cacheKey, helloWorld, -1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);

Если указать -1 для атрибута срока службы, срок действия кеша не истечет, но когда мы объединим его с 10 секундамиidleTime, мы сообщаем Infinispan, что срок действия этой записи истекает, если он не был посещен в этот период времени.

4.3. Удаление кеша

В Infinispan мы можем ограничить количество записей в данном кэше с помощьюeviction configuration:

private Configuration evictingConfiguration() {
    return new ConfigurationBuilder()
      .memory().evictionType(EvictionType.COUNT).size(1)
      .build();
}

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

Опять же, метод похож на уже представленный здесь:

public String findEvictingHelloWorld(String key) {
    String value = evictingHelloWorldCache.get(key);
    if(value == null) {
        value = repository.getHelloWorld();
        evictingHelloWorldCache.put(key, value);
    }
    return value;
}

Давайте построим наш тест:

@Test
public void whenTwoAreAdded_thenFirstShouldntBeAvailable() {

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 2")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);
}

Запустив тест, мы можем посмотреть журнал действий нашего слушателя:

Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Executing some heavy query
Evicting following entries from cache: key 2,
Adding key 'key 1' to cache

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

4.4. Кэш пассивации

cache passivation - одна из мощных функций Infinispan. Комбинируя пассивацию и вытеснение, мы можем создать кеш, который не занимает много памяти, без потери информации.

Давайте посмотрим на конфигурацию пассивации:

private Configuration passivatingConfiguration() {
    return new ConfigurationBuilder()
      .memory().evictionType(EvictionType.COUNT).size(1)
      .persistence()
      .passivation(true)    // activating passivation
      .addSingleFileStore() // in a single file
      .purgeOnStartup(true) // clean the file on startup
      .location(System.getProperty("java.io.tmpdir"))
      .build();
}

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

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

public String findPassivatingHelloWorld(String key) {
    return passivatingHelloWorldCache.computeIfAbsent(key, k ->
      repository.getHelloWorld());
}

Давайте создадим наш тест и запустим его:

@Test
public void whenTwoAreAdded_thenTheFirstShouldBeAvailable() {

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 2")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 1")))
      .isLessThan(100);
}

Теперь давайте посмотрим на действия наших слушателей:

Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Passivating key 'key 1' from cache
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Passivating key 'key 2' from cache
Evicting following entries from cache: key 2,
Loading key 'key 1' to cache
Activating key 'key 1' on cache
Key 'key 1' was visited

Обратите внимание, сколько шагов потребовалось, чтобы сохранить в нашем кэше только одну запись. Также обратите внимание на порядок шагов - пассивация, выселение, а затем загрузка с последующей активацией. Посмотрим, что означают эти шаги:

  • Passivation – наша запись хранится в другом месте, вдали от основного хранилища Infinispan (в данном случае, памяти)

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

  • Loading – при попытке доступа к нашей пассивированной записи Infinispan проверяет ее сохраненное содержимое и снова загружает запись в память.

  • Activation –: запись снова доступна в Infinispan

4.5. Транзакционный кеш

Infinispan поставляется с мощным управлением транзакциями. Как и аналог базы данных, он полезен для поддержания целостности, когда несколько потоков пытаются записать одну и ту же запись.

Давайте посмотрим, как мы можем определить кеш с транзакционными возможностями:

private Configuration transactionalConfiguration() {
    return new ConfigurationBuilder()
      .transaction().transactionMode(TransactionMode.TRANSACTIONAL)
      .lockingMode(LockingMode.PESSIMISTIC)
      .build();
}

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

public Integer getQuickHowManyVisits() {
    TransactionManager tm = transactionalCache
      .getAdvancedCache().getTransactionManager();
    tm.begin();
    Integer howManyVisits = transactionalCache.get(KEY);
    howManyVisits++;
    System.out.println("I'll try to set HowManyVisits to " + howManyVisits);
    StopWatch watch = new StopWatch();
    watch.start();
    transactionalCache.put(KEY, howManyVisits);
    watch.stop();
    System.out.println("I was able to set HowManyVisits to " + howManyVisits +
      " after waiting " + watch.getTotalTimeSeconds() + " seconds");

    tm.commit();
    return howManyVisits;
}
public void startBackgroundBatch() {
    TransactionManager tm = transactionalCache
      .getAdvancedCache().getTransactionManager();
    tm.begin();
    transactionalCache.put(KEY, 1000);
    System.out.println("HowManyVisits should now be 1000, " +
      "but we are holding the transaction");
    Thread.sleep(1000L);
    tm.rollback();
    System.out.println("The slow batch suffered a rollback");
}

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

@Test
public void whenLockingAnEntry_thenItShouldBeInaccessible() throws InterruptedException {
    Runnable backGroundJob = () -> transactionalService.startBackgroundBatch();
    Thread backgroundThread = new Thread(backGroundJob);
    transactionalService.getQuickHowManyVisits();
    backgroundThread.start();
    Thread.sleep(100); //lets wait our thread warm up

    assertThat(timeThis(() -> transactionalService.getQuickHowManyVisits()))
      .isGreaterThan(500).isLessThan(1000);
}

Запустив его, мы снова увидим в нашей консоли следующие действия:

Adding key 'key' to cache
Key 'key' was visited
Ill try to set HowManyVisits to 1
I was able to set HowManyVisits to 1 after waiting 0.001 seconds
HowManyVisits should now be 1000, but we are holding the transaction
Key 'key' was visited
Ill try to set HowManyVisits to 2
I was able to set HowManyVisits to 2 after waiting 0.902 seconds
The slow batch suffered a rollback

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

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

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

Как всегда, код можно найтиover on Github.