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(CacheEntryCreatedEventevent) { 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 отправляет два уведомления: одно до и одно сразу после обработки.
Теперь давайте построим метод для обработки создания кэша для нас:
privateCache 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 CachesimpleHelloWorldCache( 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()); }
Обратите внимание, как мы используем кэш, сначала проверив, уже ли кэширована нужная запись. Если это не так, нам нужно будет вызвать наш репозиторий и затем кэшировать его.
Давайте добавим простой метод в наши тесты, чтобы приурочить наши методы:
protectedlong 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 CacheexpiringHelloWorldCache( 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 .