Ein Leitfaden für Infinispan in Java

Ein Leitfaden für Infinispan in Java

1. Überblick

In diesem Handbuch erfahren Sie mehr überInfinispan, 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. Projektaufbau

Um es auf diese Weise verwenden zu können, müssen wir die Abhängigkeit in unserenpom.xml hinzufügen.

Die neueste Version befindet sich im Repository vonMaven Central:


    org.infinispan
    infinispan-core
    9.1.5.Final

Die gesamte zugrunde liegende Infrastruktur wird ab sofort programmgesteuert verwaltet.

3. CacheManager Setup

DasCacheManager 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 derCacheManager 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 KlasseConfigurationBuildererstellt werden, die bereits in unserem Klassenpfad verfügbar ist.

Um unsere Caches zu testen, erstellen wir eine einfache Methode, die eine umfangreiche 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 Annotation@Listener.

Wenn wir unseren Cache definieren, können wir ein Objekt übergeben, das an einem Ereignis interessiert ist, das darin stattfindet, und Infinispan benachrichtigt es, wenn der Cache bearbeitet wird:

@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 aufgetreten ist, da Infinispan bei einigen Ereignistypen zwei Benachrichtigungen sendet: eine vor und eine unmittelbar 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 anCacheManager übergeben und dann dasselbecacheName verwenden, um das Objekt zu erhalten, 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 MethodebuildCache 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 einService erstellen:

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

Beachten Sie, wie wir den Cache verwenden. Überprüfen Sie zunächst, ob der gewünschte Eintrag bereits zwischengespeichert ist. Ist dies nicht der Fall, müssen wir unsereRepository aufrufen und 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;
}

Wenn wir es 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, dh Elemente werden nach einem bestimmten Zeitraum aus dem Cache entfernt. Die Konfiguration ist ganz 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;
}

Testen wir unsere Zeit noch einmal:

@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 nacheinander trifft. Um zu zeigen, dass der Ablauf relativ zur Zeit des Eintragsputist, erzwingen wir ihn in unserem Eintrag:

@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
  throws InterruptedException {

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    Thread.sleep(1100);

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);
}

Notieren Sie nach dem Ausführen des Tests, wie nach dem angegebenen Zeitpunkt unser Eintrag aus dem Cache abgelaufen ist. Wir können dies bestätigen, indem wir uns die gedruckten Protokollzeilen von unserem Listener 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 das Verfallsdatum auch in Caches verwenden, ohne es in der Hauptkonfiguration zu haben. Die Methodeput akzeptiert weitere Argumente:

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

Oder wir können unserem Eintrag anstelle einer festen Lebensdauer ein Maximum vonidleTime geben:

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

Wenn Sie -1 für das Attribut "Lebensdauer" verwenden, läuft der Cache nicht ab. Wenn wir ihn jedoch mit 10 Sekunden vonidleTime kombinieren, weisen wir Infinispan an, diesen Eintrag abzulaufen, es sei denn, er wird in diesem Zeitraum besucht.

4.3. Cache-Räumung

In Infinispan können wir die Anzahl der Einträge in einem bestimmten Cache miteviction configuration: begrenzen

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

In diesem Beispiel beschränken wir die maximale Anzahl von Einträgen in diesem Cache auf einen. Wenn wir also versuchen, einen anderen einzugeben, wird dieser aus unserem Cache entfernt.

Auch hier ähnelt die Methode der hier bereits vorgestellten:

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

Lassen Sie uns unseren Test erstellen:

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

Wenn Sie den Test ausführen, können Sie unser Listener-Aktivitätenprotokoll einsehen:

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

Überprüfen Sie, wie der erste Schlüssel beim Einfügen des zweiten Schlüssels automatisch aus dem Cache entfernt wurde. Anschließend wurde der zweite Schlüssel entfernt, um auch wieder Platz für unseren ersten Schlüssel zu schaffen.

4.4. Passivierungs-Cache

Dascache passivation ist eine der leistungsstarken Funktionen von Infinispan. Durch die Kombination von Passivierung und Räumung können wir einen Cache erstellen, der nicht viel Speicher belegt, ohne Informationen zu verlieren.

Schauen wir uns eine Passivierungskonfiguration an:

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

Wir erzwingen erneut nur einen Eintrag in unserem Cache-Speicher, weisen Infinispan jedoch an, die verbleibenden Einträge zu passivieren, anstatt sie nur zu entfernen.

Mal sehen, was passiert, wenn wir versuchen, mehr als einen Eintrag zu füllen:

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

Lassen Sie uns unseren Test erstellen und ausführen:

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

Schauen wir uns nun unsere Höreraktivitäten an:

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

Beachten Sie, wie viele Schritte erforderlich waren, um unseren Cache mit nur einem Eintrag zu speichern. Beachten Sie auch die Reihenfolge der Schritte - Passivierung, Räumung und dann Laden, gefolgt von Aktivierung. Mal sehen, was diese Schritte bedeuten:

  • Passivation – Unser Eintrag wird an einem anderen Ort gespeichert, außerhalb des Netzspeichers von Infinispan (in diesem Fall des Speichers).

  • Eviction – Der Eintrag wird entfernt, um Speicher freizugeben und die konfigurierte maximale Anzahl von Einträgen im Cache beizubehalten

  • Loading – Beim Versuch, unseren passivierten Eintrag zu erreichen, überprüft Infinispan den gespeicherten Inhalt und lädt den Eintrag erneut in den Speicher

  • Activation – Der Eintrag ist jetzt wieder in Infinispan verfügbar

4.5. Transaktions-Cache

Infinispan wird mit einer leistungsstarken Transaktionssteuerung ausgeliefert. Wie das Gegenstück zur Datenbank ist es hilfreich, 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 das Testen zu ermöglichen, 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 auf dem Haupt-Thread und warten Sie auf das Ende der Transaktion, die von der langsamen Methode erstellt wurde.

5. Fazit

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

Wie immer kann der Codeover on Github gefunden werden.