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.