1. Обзор
В этом руководстве мы познакомимся с Infinispan — хранилищем данных типа «ключ-значение» в памяти, которое поставляется с более надежным набором функций, чем другие инструменты в той же нише.
Чтобы понять, как это работает, мы создадим простой проект, демонстрирующий наиболее распространенные функции, и проверим, как их можно использовать.
2. Настройка проекта
Чтобы иметь возможность использовать его таким образом, нам нужно добавить его зависимость в наш pom.xml
.
Последнюю версию можно найти в репозитории Maven Central :
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>9.1.5.Final</version>
</dependency>
Отныне вся необходимая базовая инфраструктура будет обрабатываться программно.
3. Настройка кэш-менеджера
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<String, String> event) {
this.printLog("Adding key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntryExpired
public void entryExpired(CacheEntryExpiredEvent<String, String> event) {
this.printLog("Expiring key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryVisited
public void entryVisited(CacheEntryVisitedEvent<String, String> event) {
this.printLog("Key '" + event.getKey() + "' was visited", event);
}
@CacheEntryActivated
public void entryActivated(CacheEntryActivatedEvent<String, String> event) {
this.printLog("Activating key '" + event.getKey()
+ "' on cache", event);
}
@CacheEntryPassivated
public void entryPassivated(CacheEntryPassivatedEvent<String, String> event) {
this.printLog("Passivating key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryLoaded
public void entryLoaded(CacheEntryLoadedEvent<String, String> event) {
this.printLog("Loading key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntriesEvicted
public void entriesEvicted(CacheEntriesEvictedEvent<String, String> 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 <K, V> Cache<K, V> buildCache(
String cacheName,
DefaultCacheManager cacheManager,
CacheListener listener,
Configuration configuration) {
cacheManager.defineConfiguration(cacheName, configuration);
Cache<K, V> cache = cacheManager.getCache(cacheName);
cache.addListener(listener);
return cache;
}
Обратите внимание, как мы передаем конфигурацию в CacheManager
, а затем используем тот же cacheName
для получения объекта, соответствующего требуемому кешу. Обратите также внимание на то, как мы информируем слушателя о самом объекте кеша.
Теперь мы проверим пять различных конфигураций кэша и посмотрим, как мы можем их настроить и наилучшим образом использовать.
4.1. Простой кэш
Самый простой тип кеша можно определить одной строкой, используя наш метод buildCache
:
public Cache<String, String> 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());
}
Обратите внимание, как мы используем кеш, сначала проверяя, не кэширована ли уже нужная запись. Если это не так, нам нужно будет вызвать наш репозиторий
, а затем кэшировать его.
Давайте добавим простой метод в наши тесты для определения времени наших методов:
protected <T> long timeThis(Supplier<T> 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<String, String> 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);
}
Запустив его, мы видим, что в быстрой последовательности попадает кеш. Чтобы продемонстрировать, что истечение срока действия относится к времени пут
-входа , давайте добавим его в нашу запись:
@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 для атрибута продолжительности жизни, кеш не истечет из-за этого, но когда мы объединяем его с 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 .