Руководство по 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.