Um guia para o Infinispan em Java

Um guia para o Infinispan em Java

1. Visão geral

Neste guia, aprenderemos sobreInfinispan, um armazenamento de dados de chave / valor na memória que vem com um conjunto de recursos mais robusto do que outras ferramentas do mesmo nicho.

Para entender como isso funciona, vamos construir um projeto simples mostrando os recursos mais comuns e verificar como eles podem ser usados.

2. Configuração do Projeto

Para poder usá-lo dessa forma, precisaremos adicionar sua dependência em nossopom.xml.

A versão mais recente pode ser encontrada no repositórioMaven Central:


    org.infinispan
    infinispan-core
    9.1.5.Final

Toda a infraestrutura subjacente necessária será tratada programaticamente a partir de agora.

3. Configuração deCacheManager

OCacheManager é a base da maioria dos recursos que usaremos. Ele atua como um contêiner para todos os caches declarados, controlando seu ciclo de vida e é responsável pela configuração global.

O Infinispan vem com uma maneira realmente fácil de construir oCacheManager:

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

Agora podemos construir nossos caches com ele.

4. Configuração de caches

Um cache é definido por um nome e uma configuração. A configuração necessária pode ser construída usando a classeConfigurationBuilder, já disponível em nosso classpath.

Para testar nossos caches, construiremos um método simples que simula algumas consultas pesadas:

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

Além disso, para poder verificar as alterações em nossos caches, o Infinispan fornece uma anotação simples@Listener.

Ao definir nosso cache, podemos passar algum objeto interessado em qualquer evento que ocorra dentro dele, e o Infinispan o notificará ao manipular o 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);
        }
    }
}

Antes de imprimir nossa mensagem, verificamos se o evento que está sendo notificado já aconteceu, porque, para alguns tipos de eventos, o Infinispan envia duas notificações: uma antes e outra logo após o processamento.

Agora vamos construir um método para lidar com a criação do cache para nós:

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

Observe como passamos uma configuração paraCacheManager e, em seguida, usamos o mesmocacheName para obter o objeto correspondente ao cache desejado. Observe também como informamos o ouvinte ao próprio objeto de cache.

Vamos agora verificar cinco configurações de cache diferentes e ver como podemos defini-las e fazer o melhor uso delas.

4.1. Cache Simples

O tipo mais simples de cache pode ser definido em uma linha, usando nosso métodobuildCache:

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

Agora podemos construir umService:

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

Observe como usamos o cache, primeiro verificando se a entrada desejada já está em cache. Se não for, precisaremos chamar nossoRepositorye armazená-lo em cache.

Vamos adicionar um método simples em nossos testes para cronometrar nossos métodos:

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

Testando, podemos verificar o tempo entre a execução de duas chamadas de método:

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

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

4.2. Expiration Cache

Podemos definir um cache no qual todas as entradas têm uma vida útil, ou seja, os elementos serão removidos do cache após um determinado período. A configuração é bastante simples:

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

Agora criamos nosso cache usando a configuração acima:

public Cache expiringHelloWorldCache(
  DefaultCacheManager cacheManager,
  CacheListener listener) {

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

E, finalmente, use-o em um método semelhante a partir do nosso cache simples acima:

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

Vamos testar nossos tempos novamente:

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

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

Ao executá-lo, vemos que em rápida sucessão o cache é atingido. Para mostrar que a expiração é relativa ao tempo deput de sua entrada, vamos forçá-la em nossa entrada:

@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
  throws InterruptedException {

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

    Thread.sleep(1100);

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

Após a execução do teste, observe como após o tempo especificado nossa entrada expirou do cache. Podemos confirmar isso observando as linhas de log impressas do nosso ouvinte:

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

Observe que a entrada expirou quando tentamos acessá-la. O Infinispan verifica uma entrada expirada em dois momentos: quando tentamos acessá-la ou quando o thread do ceifador verifica o cache.

Podemos usar a expiração mesmo em caches sem ela na configuração principal. O métodoput aceita mais argumentos:

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

Ou, em vez de uma vida útil fixa, podemos dar à nossa entrada um máximo deidleTime:

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

Usando -1 para o atributo de tempo de vida, o cache não sofrerá expiração, mas quando combinamos com 10 segundos deidleTime, dizemos ao Infinispan para expirar esta entrada a menos que seja visitado neste período de tempo.

4.3. Remoção de cache

No Infinispan podemos limitar o número de entradas em um determinado cache com oeviction configuration:

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

Neste exemplo, estamos limitando o máximo de entradas neste cache a um, o que significa que, se tentarmos inserir outro, ele será removido de nosso cache.

Novamente, o método é semelhante ao já apresentado aqui:

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

Vamos construir nosso teste:

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

Ao executar o teste, podemos ver o log de atividades do ouvinte:

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

Verifique como a primeira chave foi removida automaticamente do cache quando inserimos a segunda e, em seguida, a segunda também removida para dar espaço para a primeira chave novamente.

4.4. Cache de Passivação

Ocache passivation é um dos recursos poderosos do Infinispan. Ao combinar passivação e despejo, podemos criar um cache que não ocupa muita memória, sem perder informações.

Vamos dar uma olhada em uma configuração de passivação:

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

Estamos novamente forçando apenas uma entrada em nossa memória cache, mas dizendo ao Infinispan para passivar as entradas restantes, em vez de apenas removê-las.

Vamos ver o que acontece quando tentamos preencher mais de uma entrada:

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

Vamos construir nosso teste e executá-lo:

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

Agora, vamos dar uma olhada em nossas atividades de ouvinte:

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

Observe quantas etapas foram necessárias para manter nosso cache com apenas uma entrada. Observe também a ordem das etapas - passivação, despejo e carregamento, seguidas pela ativação. Vamos ver o que essas etapas significam:

  • Passivation – nossa entrada está armazenada em outro lugar, longe do armazenamento principal do Infinispan (neste caso, a memória)

  • Eviction – a entrada é removida, para liberar memória e manter o número máximo configurado de entradas no cache

  • Loading – ao tentar alcançar nossa entrada passivada, o Infinispan verifica o conteúdo armazenado e carrega a entrada na memória novamente

  • Activation – a entrada agora está acessível no Infinispan novamente

4.5. Cache Transacional

O Infinispan é fornecido com um poderoso controle de transações. Como a contraparte do banco de dados, é útil para manter a integridade enquanto mais de um encadeamento tenta gravar a mesma entrada.

Vamos ver como podemos definir um cache com recursos transacionais:

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

Para que seja possível testá-lo, vamos construir dois métodos - um que conclui sua transação rapidamente e outro que leva um tempo:

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

Agora vamos criar um teste que executa os dois métodos e verificar como o Infinispan se comportará:

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

Ao executá-lo, veremos as seguintes atividades em nosso console novamente:

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

Verifique a hora no encadeamento principal, aguardando o final da transação criada pelo método slow.

5. Conclusão

Neste artigo, vimos o que é o Infinispan e seus principais recursos e capacidades como um cache dentro de um aplicativo.

Como sempre, o código pode ser encontradoover on Github.