Eine Anleitung zu Infinispan in Java

1. Übersicht

In diesem Handbuch erfahren Sie mehr über Infinispan, einen speicherinternen Schlüssel- / Wert-Datenspeicher, der mit einer robusteren Reihe von Funktionen als andere Tools derselben Nische geliefert wird.

Um zu verstehen, wie es funktioniert, erstellen wir ein einfaches Projekt mit den häufigsten Funktionen und prüfen, wie sie verwendet werden können.

2. Projekteinrichtung

Um es auf diese Weise verwenden zu können, müssen wir seine Abhängigkeit in unsere pom.xml einfügen .

Die neueste Version finden Sie im Maven Central-Repository:

 org.infinispan infinispan-core 9.1.5.Final 

Die gesamte zugrunde liegende Infrastruktur wird von nun an programmgesteuert verwaltet.

3. CacheManager- Setup

Der CacheManager ist die Grundlage für die meisten Funktionen, die wir verwenden werden. Es fungiert als Container für alle deklarierten Caches, steuert deren Lebenszyklus und ist für die globale Konfiguration verantwortlich.

Infinispan wird mit einer wirklich einfachen Methode zum Erstellen des CacheManager ausgeliefert :

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

Jetzt können wir unsere Caches damit erstellen.

4. Caches einrichten

Ein Cache wird durch einen Namen und eine Konfiguration definiert. Die erforderliche Konfiguration kann mit der Klasse ConfigurationBuilder erstellt werden , die bereits in unserem Klassenpfad verfügbar ist.

Um unsere Caches zu testen, erstellen wir eine einfache Methode, die eine schwere Abfrage simuliert:

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

Um in unseren Caches nach Änderungen suchen zu können, bietet Infinispan eine einfache Anmerkung @Listener .

Bei der Definition unseres Caches können wir ein Objekt übergeben, das an einem Ereignis in diesem Cache interessiert ist, und Infinispan benachrichtigt es bei der Behandlung des Caches:

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

Bevor wir unsere Nachricht drucken, prüfen wir, ob das zu benachrichtigende Ereignis bereits eingetreten ist, da Infinispan für einige Ereignistypen zwei Benachrichtigungen sendet: eine vor und eine direkt nach der Verarbeitung.

Erstellen wir nun eine Methode, um die Cache-Erstellung für uns durchzuführen:

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

Beachten Sie, wie wir eine Konfiguration an CacheManager übergeben und dann denselben Cache-Namen verwenden , um das Objekt abzurufen , das dem gewünschten Cache entspricht. Beachten Sie auch, wie wir den Listener über das Cache-Objekt selbst informieren.

Wir werden nun fünf verschiedene Cache-Konfigurationen überprüfen und sehen, wie wir sie einrichten und optimal nutzen können.

4.1. Einfacher Cache

Der einfachste Cache-Typ kann mit unserer Methode buildCache in einer Zeile definiert werden :

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

Wir können jetzt einen Service erstellen :

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

Beachten Sie, wie wir den Cache verwenden, und überprüfen Sie zunächst, ob der gewünschte Eintrag bereits zwischengespeichert ist. Wenn dies nicht der Fall ist, müssen wir unser Repository aufrufen und es dann zwischenspeichern.

Fügen wir unseren Tests eine einfache Methode hinzu, um unsere Methoden zeitlich festzulegen:

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

Beim Testen können wir die Zeit zwischen der Ausführung von zwei Methodenaufrufen überprüfen:

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

4.2. Ablaufcache

Wir können einen Cache definieren, in dem alle Einträge eine Lebensdauer haben. Mit anderen Worten, Elemente werden nach einer bestimmten Zeit aus dem Cache entfernt. Die Konfiguration ist recht einfach:

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

Jetzt erstellen wir unseren Cache mit der obigen Konfiguration:

public Cache expiringHelloWorldCache( DefaultCacheManager cacheManager, CacheListener listener) { return this.buildCache(EXPIRING_HELLO_WORLD_CACHE, cacheManager, listener, expiringConfiguration()); }

Und schließlich verwenden Sie es in einer ähnlichen Methode aus unserem einfachen Cache oben:

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

Lassen Sie uns unsere Zeit noch einmal testen:

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

Wenn wir es ausführen, sehen wir, dass der Cache schnell hintereinander trifft. Zu präsentieren , dass der Ablauf seines Eintritt relativ Put - Zeit, lassen Sie sich Kraft , die es in unserem Beitrag:

@Test public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache() throws InterruptedException { assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isGreaterThanOrEqualTo(1000); Thread.sleep(1100); assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isGreaterThanOrEqualTo(1000); }

Beachten Sie nach dem Ausführen des Tests, wie unser Eintrag nach Ablauf der angegebenen Zeit aus dem Cache abgelaufen ist. Wir können dies bestätigen, indem wir uns die gedruckten Protokollzeilen unseres Hörers ansehen:

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

Beachten Sie, dass der Eintrag abgelaufen ist, wenn wir versuchen, darauf zuzugreifen. Infinispan sucht in zwei Augenblicken nach einem abgelaufenen Eintrag: wenn wir versuchen, darauf zuzugreifen, oder wenn der Reaper-Thread den Cache durchsucht.

Wir können den Ablauf auch in Caches ohne ihn in ihrer Hauptkonfiguration verwenden. Die Methode put akzeptiert weitere Argumente:

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

Oder wir können unserem Eintrag anstelle einer festen Lebensdauer eine maximale Leerlaufzeit geben :

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

Using -1 to the lifespan attribute, the cache won't suffer expiration from it, but when we combine it with 10 seconds of idleTime, we tell Infinispan to expire this entry unless it is visited in this timeframe.

4.3. Cache Eviction

In Infinispan we can limit the number of entries in a given cache with the eviction configuration:

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

In this example, we're limiting the maximum entries in this cache to one, meaning that, if we try to enter another one, it'll be evicted from our cache.

Again, the method is similar to the already presented here:

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

Let's build our test:

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

Running the test, we can look at our listener log of activities:

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

Check how the first key was automatically removed from the cache when we inserted the second one, and then, the second one removed also to give room for our first key again.

4.4. Passivation Cache

The cache passivation is one of the powerful features of Infinispan. By combining passivation and eviction, we can create a cache that doesn't occupy a lot of memory, without losing information.

Let's have a look at a passivation configuration:

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

We're again forcing just one entry in our cache memory, but telling Infinispan to passivate the remaining entries, instead of just removing them.

Let's see what happens when we try to fill more than one entry:

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

Let's build our test and run it:

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

Now let's look at our listener activities:

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

Note how many steps did it take to keep our cache with only one entry. Also, note the order of steps – passivation, eviction and then loading followed by activation. Let's see what those steps mean:

  • Passivation – our entry is stored in another place, away from the mains storage of Infinispan (in this case, the memory)
  • Eviction – the entry is removed, to free memory and to keep the configured maximum number of entries in the cache
  • Loading – when trying to reach our passivated entry, Infinispan checks it's stored contents and load the entry to the memory again
  • Activation – the entry is now accessible in Infinispan again

4.5. Transactional Cache

Infinispan wird mit einer leistungsstarken Transaktionssteuerung geliefert. Wie das Datenbank-Gegenstück ist es nützlich, um die Integrität aufrechtzuerhalten, während mehr als ein Thread versucht, denselben Eintrag zu schreiben.

Mal sehen, wie wir einen Cache mit Transaktionsfunktionen definieren können:

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

Um es zu testen, erstellen wir zwei Methoden - eine, die die Transaktion schnell abschließt, und eine, die eine Weile dauert:

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

Erstellen wir nun einen Test, der beide Methoden ausführt, und überprüfen Sie, wie sich Infinispan verhält:

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

Wenn Sie es ausführen, werden die folgenden Aktivitäten erneut in unserer Konsole angezeigt:

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

Überprüfen Sie die Zeit im Hauptthread und warten Sie auf das Ende der Transaktion, die mit der langsamen Methode erstellt wurde.

5. Schlussfolgerung

In diesem Artikel haben wir gesehen, was Infinispan ist und welche Funktionen es als Cache in einer Anwendung bietet.

Wie immer ist der Code auf Github zu finden.