Рубрики
Без рубрики

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

Узнайте, как можно использовать Infinispan в качестве решения для кэширования вашего приложения.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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


    org.infinispan
    infinispan-core
    9.1.5.Final

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

3. Настройка диспетчера кэша

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

Infinispan поставляется с очень простым способом создания диспетчера кэша :

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;
}

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

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

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

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

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

Теперь мы можем создать Сервис :

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

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

Давайте добавим простой метод в наши тесты, чтобы приурочить наши методы:

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 time, давайте принудительно введем его в нашу запись:

@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 проверяет наличие просроченной записи в два момента: когда мы пытаемся получить к ней доступ или когда поток reaper сканирует кэш.

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

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

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

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

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

4.3. Выселение кэша

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

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. Кэш пассивации

Пассивация кэша является одной из мощных функций 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

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

  • Пассивация – наша запись хранится в другом месте, вдали от сетевого хранилища Infinispan (в данном случае память)
  • Выселение – запись удаляется, чтобы освободить память и сохранить настроенное максимальное количество записей в кэше
  • Загрузка – при попытке добраться до нашей пассивированной записи Infinispan проверяет ее сохраненное содержимое и снова загружает запись в память
  • Активация – запись теперь снова доступна в 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, и его основные функции и возможности в качестве кэша в приложении.

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