Un guide pour Infinispan en Java

Un guide pour Infinispan en Java

1. Vue d'ensemble

Dans ce guide, nous allons découvrirInfinispan, un magasin de données clé / valeur en mémoire qui est livré avec un ensemble de fonctionnalités plus robuste que d'autres outils du même créneau.

Pour comprendre comment cela fonctionne, nous allons créer un projet simple présentant les fonctionnalités les plus courantes et vérifier comment elles peuvent être utilisées.

2. Configuration du projet

Pour pouvoir l'utiliser de cette manière, nous devons ajouter sa dépendance dans nospom.xml.

La dernière version peut être trouvée dans le référentielMaven Central:


    org.infinispan
    infinispan-core
    9.1.5.Final

Toutes les infrastructures sous-jacentes nécessaires seront désormais gérées par programme.

3. Configuration deCacheManager

LeCacheManager est la base de la majorité des fonctionnalités que nous utiliserons. Il agit comme un conteneur pour tous les caches déclarés, contrôlant leur cycle de vie et est responsable de la configuration globale.

Infinispan est livré avec un moyen très simple de créer lesCacheManager:

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

Nous pouvons désormais créer nos caches avec.

4. Configuration des caches

Un cache est défini par un nom et une configuration. La configuration nécessaire peut être construite en utilisant la classeConfigurationBuilder, déjà disponible dans notre classpath.

Pour tester nos caches, nous allons créer une méthode simple qui simule des requêtes lourdes:

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

Aussi, pour pouvoir vérifier les changements dans nos caches, Infinispan fournit une simple annotation@Listener.

Lors de la définition de notre cache, nous pouvons transmettre un objet intéressé à tout événement se produisant à l'intérieur de celui-ci, et Infinispan le notifiera lors de la manipulation du cache:

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

Avant d’imprimer notre message, nous vérifions si l’événement ayant fait l’objet de la notification est déjà passé, car pour certains types d’événements, Infinispan envoie deux notifications: une avant et une juste après son traitement.

Créons maintenant une méthode pour gérer la création du cache pour nous:

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

Remarquez comment nous passons une configuration àCacheManager, puis utilisons les mêmescacheName pour obtenir l'objet correspondant au cache souhaité. Notez également comment nous informons l'écouteur de l'objet cache lui-même.

Nous allons maintenant vérifier cinq configurations de cache différentes, et nous verrons comment nous pouvons les configurer et en tirer le meilleur parti.

4.1. Cache simple

Le type de cache le plus simple peut être défini sur une seule ligne, en utilisant notre méthodebuildCache:

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

Nous pouvons maintenant construire unService:

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

Notez comment nous utilisons le cache, en vérifiant d’abord si l’entrée recherchée est déjà mise en cache. Si ce n'est pas le cas, nous devrons appeler nosRepository, puis les mettre en cache.

Ajoutons une méthode simple dans nos tests pour chronométrer nos méthodes:

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

En le testant, nous pouvons vérifier le temps écoulé entre deux appels de méthode:

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

    assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
      .isLessThan(100);
}

4.2. Cache d'expiration

Nous pouvons définir un cache dans lequel toutes les entrées ont une durée de vie. En d'autres termes, les éléments seront supprimés du cache après une période donnée. La configuration est assez simple:

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

Maintenant, nous construisons notre cache en utilisant la configuration ci-dessus:

public Cache expiringHelloWorldCache(
  DefaultCacheManager cacheManager,
  CacheListener listener) {

    return this.buildCache(EXPIRING_HELLO_WORLD_CACHE,
      cacheManager, listener, expiringConfiguration());
}

Et enfin, utilisez-le de la même manière que notre cache ci-dessus:

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

Testons à nouveau notre époque:

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

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isLessThan(100);
}

En le lançant, nous voyons que successivement le cache est touché. Pour montrer que l'expiration est relative à l'heure de son entréeput, forcons-la dans notre entrée:

@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
  throws InterruptedException {

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

    Thread.sleep(1100);

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

Après avoir exécuté le test, notez comment, après le temps imparti, notre entrée a expiré du cache. Nous pouvons le confirmer en consultant les lignes de journal imprimées de notre auditeur:

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

Notez que l'entrée est expirée lorsque nous essayons d'y accéder. Infinispan recherche une entrée expirée à deux moments: lorsque nous essayons d'y accéder ou lorsque le thread reaper analyse le cache.

Nous pouvons utiliser l'expiration même dans les caches sans celle-ci dans leur configuration principale. La méthodeput accepte plus d'arguments:

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

Ou, au lieu d'une durée de vie fixe, nous pouvons donner à notre entrée un maximum deidleTime:

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

En utilisant -1 pour l'attribut lifespan, le cache n'en souffrira pas d'expiration, mais lorsque nous le combinons avec 10 secondes deidleTime, nous demandons à Infinispan d'expirer cette entrée à moins qu'elle ne soit visitée dans ce délai.

4.3. Expulsion du cache

Dans Infinispan, nous pouvons limiter le nombre d'entrées dans un cache donné avec leseviction configuration:

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

Dans cet exemple, nous limitons le nombre maximal d'entrées de ce cache à un, ce qui signifie que si nous essayons d'en saisir un autre, il sera expulsé de notre cache.

Encore une fois, la méthode est similaire à celle déjà présentée ici:

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

Créons notre 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);
}

En effectuant le test, nous pouvons consulter notre journal des activités de l'auditeur:

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

Vérifiez comment la première clé a été automatiquement supprimée du cache lorsque nous avons inséré la deuxième, puis la deuxième également supprimée pour laisser de la place à notre première clé.

4.4. Cache de passivation

Lecache passivation est l'une des fonctionnalités puissantes d'Infinispan. En combinant passivation et expulsion, nous pouvons créer un cache qui n'occupe pas beaucoup de mémoire, sans perdre d'informations.

Examinons une configuration de passivation:

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

Nous ne forçons à nouveau qu'une seule entrée dans notre mémoire cache, mais nous demandons à Infinispan de passiver les entrées restantes, au lieu de simplement les supprimer.

Voyons ce qui se passe lorsque nous essayons de remplir plusieurs entrées:

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

Créons notre test et exécutons-le:

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

Voyons maintenant nos activités d'audition:

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

Notez combien d'étapes ont été nécessaires pour conserver notre cache avec une seule entrée. Notez également l'ordre des étapes - passivation, éviction, puis chargement suivi de l'activation. Voyons ce que ces étapes signifient:

  • Passivation – notre entrée est stockée dans un autre endroit, loin du stockage secteur d'Infinispan (dans ce cas, la mémoire)

  • Eviction – l'entrée est supprimée, pour libérer de la mémoire et conserver le nombre maximum d'entrées configuré dans le cache

  • Loading – en essayant d'atteindre notre entrée passivée, Infinispan vérifie son contenu stocké et charge à nouveau l'entrée dans la mémoire

  • Activation – l'entrée est à nouveau accessible dans Infinispan

4.5. Cache transactionnel

Infinispan est livré avec un contrôle de transaction puissant. Comme la contrepartie de la base de données, il est utile pour maintenir l'intégrité lorsque plusieurs threads essaient d'écrire la même entrée.

Voyons comment nous pouvons définir un cache avec des capacités transactionnelles:

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

Pour permettre de le tester, construisons deux méthodes: une qui termine sa transaction rapidement et une qui prend un certain temps:

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

Créons maintenant un test qui exécute les deux méthodes et vérifie le comportement d’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);
}

En l'exécutant, nous verrons à nouveau les activités suivantes dans notre console:

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

Vérifiez l'heure sur le thread principal en attendant la fin de la transaction créée par la méthode slow.

5. Conclusion

Dans cet article, nous avons vu ce qu'est Infinispan, et ses principales fonctionnalités et capacités en tant que cache dans une application.

Comme toujours, le code peut être trouvéover on Github.