Cache de goiaba

Cache de goiaba

1. Visão geral

Neste tutorial, daremos uma olhada na implementação deGuava Cache - uso básico, políticas de despejo, atualização do cache e algumas operações em massa interessantes.

Por fim, examinaremos as notificações de remoção que o cache pode enviar.

2. Como usar o cache de Guava

Vamos começar com um exemplo simples - vamos armazenar em cache a forma maiúscula das instânciasString.

Primeiro, vamos criar oCacheLoader - usado para calcular o valor armazenado no cache. A partir disso, usaremos os úteisCacheBuilder para construir nosso cache usando as especificações fornecidas:

@Test
public void whenCacheMiss_thenValueIsComputed() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals(0, cache.size());
    assertEquals("HELLO", cache.getUnchecked("hello"));
    assertEquals(1, cache.size());
}

Observe como não há valor no cache para a nossa chave "olá" - e, portanto, o valor é calculado e armazenado em cache.

Observe também que estamos usando a operaçãogetUnchecked() - isso calcula e carrega o valor no cache, se ainda não existir.

3. Políticas de Despejo

Todo cache precisa remover valores em algum momento. Vamos discutir o mecanismo de despejo de valores do cache - usando critérios diferentes.

3.1. Despejo por tamanho

Podemoslimit the size of our cache usandomaximumSize(). Se o cache atingir o limite, os itens mais antigos serão despejados.

No código a seguir, limitamos o tamanho do cache a 3 registros:

@Test
public void whenCacheReachMaxSize_thenEviction() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
    LoadingCache cache;
    cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("forth");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("FORTH", cache.getIfPresent("forth"));
}

3.2. Despejo por Peso

Também podemoslimit the cache size usando uma função de peso personalizada. No código a seguir, usamoslength como nossa função de peso personalizada:

@Test
public void whenCacheReachMaxWeight_thenEviction() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    Weigher weighByLength;
    weighByLength = new Weigher() {
        @Override
        public int weigh(String key, String value) {
            return value.length();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder()
      .maximumWeight(16)
      .weigher(weighByLength)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("LAST", cache.getIfPresent("last"));
}

Nota: O cache pode remover mais de um registro para deixar espaço para um novo grande.

3.3. Despejo por tempo

Além de usar o tamanho para remover registros antigos, podemos usar o tempo. No exemplo a seguir, personalizamos nosso cache pararemove records that have been idle for 2ms:

@Test
public void whenEntryIdle_thenEviction()
  throws InterruptedException {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterAccess(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());

    cache.getUnchecked("hello");
    Thread.sleep(300);

    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

Também podemosevict records based on their total live time. No exemplo a seguir, o cache removerá os registros após 2ms de serem armazenados:

@Test
public void whenEntryLiveTimeExpire_thenEviction()
  throws InterruptedException {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterWrite(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());
    Thread.sleep(300);
    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

4. Chaves fracas

A seguir, vamos ver como fazer com que nossas chaves de cache tenham referências fracas - permitindo que o coletor de lixo colete chaves de cache que não são referenciadas em outro lugar.

Por padrão, as chaves e os valores de cache têm referências fortes, mas podemos fazer com que nosso cache armazene as chaves usando referências fracas usandoweakKeys() como no exemplo a seguir:

@Test
public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder().weakKeys().build(loader);
}

5. Valores Suaves

Podemos permitir que o coletor de lixo colete nossos valores em cache usandosoftValues() como no exemplo a seguir:

@Test
public void whenSoftValue_thenRemoveFromCache() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder().softValues().build(loader);
}

Nota: Muitas referências suaves podem afetar o desempenho do sistema - é preferível usarmaximumSize().

6. Lidar com valores denull

Agora, vamos ver como lidar com os valores denull do cache. Por padrão,Guava Cache lançará exceções se você tentar carregar um valornull - já que não faz sentido armazenar em cache umnull.

Mas se o valornull significa algo em seu código, então você pode fazer bom uso da classeOptional como no exemplo a seguir:

@Test
public void whenNullValue_thenOptional() {
    CacheLoader> loader;
    loader = new CacheLoader>() {
        @Override
        public Optional load(String key) {
            return Optional.fromNullable(getSuffix(key));
        }
    };

    LoadingCache> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals("txt", cache.getUnchecked("text.txt").get());
    assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
    int lastIndex = str.lastIndexOf('.');
    if (lastIndex == -1) {
        return null;
    }
    return str.substring(lastIndex + 1);
}

7. Atualizar o Cache

A seguir, vamos ver como atualizar nossos valores de cache. Podemos atualizar nosso cache automaticamente usandorefreshAfterWrite().

No exemplo a seguir, o cache é atualizado automaticamente a cada 1 minuto:

@Test
public void whenLiveTimeEnd_thenRefresh() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1,TimeUnit.MINUTES)
      .build(loader);
}

Nota: Você pode atualizar um registro específico manualmente usandorefresh(key).

8. Pré-carregar o cache

Podemos inserir vários registros em nosso cache usando o métodoputAll(). No exemplo a seguir, adicionamos vários registros em nosso cache usando umMap:

@Test
public void whenPreloadCache_thenUsePutAll() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder().build(loader);

    Map map = new HashMap();
    map.put("first", "FIRST");
    map.put("second", "SECOND");
    cache.putAll(map);

    assertEquals(2, cache.size());
}

9. RemovalNotification

Às vezes, você precisa realizar algumas ações quando um registro é removido do cache; então, vamos discutirRemovalNotification.

Podemos registrar umRemovalListener para receber notificações de um registro sendo removido. Também temos acesso à causa da remoção - por meio do métodogetCause().

No exemplo a seguir, umRemovalNotification é recebido quando o quarto elemento no cache devido ao seu tamanho:

@Test
public void whenEntryRemovedFromCache_thenNotify() {
    CacheLoader loader;
    loader = new CacheLoader() {
        @Override
        public String load(final String key) {
            return key.toUpperCase();
        }
    };

    RemovalListener listener;
    listener = new RemovalListener() {
        @Override
        public void onRemoval(RemovalNotification n){
            if (n.wasEvicted()) {
                String cause = n.getCause().name();
                assertEquals(RemovalCause.SIZE.toString(),cause);
            }
        }
    };

    LoadingCache cache;
    cache = CacheBuilder.newBuilder()
      .maximumSize(3)
      .removalListener(listener)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
}

10. Notas

Finalmente, aqui estão algumas notas rápidas adicionais sobre a implementação do cache do Guava:

  • é seguro para threads

  • você pode inserir valores manualmente no cache usandoput(key,value)

  • você pode medir o desempenho do cache usandoCacheStats (hitRate(),missRate(), ..)

11. Conclusão

Passamos por várias casos de uso do Guava Cache neste tutorial - do uso simples à remoção de elementos, atualização e pré-carregamento do cache e notificações de remoção.