Introdução à cafeína
1. Introdução
Neste artigo, vamos dar uma olhada emCaffeine - ahigh-performance caching library for Java.
Uma diferença fundamental entre um cache e umMap é que um cache despeja itens armazenados.
Umeviction policy decides which objects should be deleted em qualquer momento. Esta políticadirectly affects the cache’s hit rate - uma característica crucial das bibliotecas de cache.
O Caffeine usa a política de despejoWindow TinyLfu, que fornece umnear-optimal hit rate.
2. Dependência
Precisamos adicionar a dependênciacaffeine ao nossopom.xml:
com.github.ben-manes.caffeine
caffeine
2.5.5
Você pode encontrar a versão mais recente decaffeineon Maven Central.
3. Preenchendo Cache
Vamos nos concentrar emthree strategies for cache population do Caffeine: manual, carregamento síncrono e carregamento assíncrono.
Primeiro, vamos escrever uma classe para os tipos de valores que armazenaremos em nosso cache:
class DataObject {
private final String data;
private static int objectCounter = 0;
// standard constructors/getters
public static DataObject get(String data) {
objectCounter++;
return new DataObject(data);
}
}
3.1. Preenchimento manual
Nesta estratégia, colocamos valores manualmente no cache e os recuperamos mais tarde.
Vamos inicializar nosso cache:
Cache cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
Agora,we can get some value from the cache using the getIfPresent method. Este método retornaránull se o valor não estiver presente no cache:
String key = "A";
DataObject dataObject = cache.getIfPresent(key);
assertNull(dataObject);
Podemospopulate the cache manualmente usando o métodoput:
cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
assertNotNull(dataObject);
We can also get the value using the get method, que levaFunction junto com uma chave como argumento. Esta função será usada para fornecer o valor de fallback se a chave não estiver presente no cache, que seria inserida no cache após o cálculo:
dataObject = cache
.get(key, k -> DataObject.get("Data for A"));
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());
O métodoget executa o cálculo atomicamente. Isso significa que o cálculo será feito apenas uma vez - mesmo que vários threads solicitem o valor simultaneamente. É por isso queusing get is preferable to getIfPresent.
Às vezes, precisamosinvalidate some cached values manualmente:
cache.invalidate(key);
dataObject = cache.getIfPresent(key);
assertNull(dataObject);
3.2. Carregamento Síncrono
Este método de carregar o cache leva umFunction, que é usado para inicializar valores, semelhante ao métodoget da estratégia manual. Vamos ver como podemos usar isso.
Primeiro de tudo, precisamos inicializar nosso cache:
LoadingCache cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
Agora podemos recuperar os valores usando o métodoget:
DataObject dataObject = cache.get(key);
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
Também podemos obter um conjunto de valores usando o métodogetAll:
Map dataObjectMap
= cache.getAll(Arrays.asList("A", "B", "C"));
assertEquals(3, dataObjectMap.size());
Os valores são recuperados da inicialização de backend subjacenteFunction que foi passada para o métodobuild. This makes it possible to use the cache as the main facade for accessing values.
3.3. Carregamento Assíncrono
Esta estratégiaworks the same as the previous but performs operations asynchronously and returns a CompletableFuture mantendo o valor real:
AsyncLoadingCache cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> DataObject.get("Data for " + k));
Podemosuse the get and getAll methods, da mesma maneira, levando em consideração o fato de que eles retornamCompletableFuture:
String key = "A";
cache.get(key).thenAccept(dataObject -> {
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
});
cache.getAll(Arrays.asList("A", "B", "C"))
.thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));
CompletableFuture tem uma API rica e útil, sobre a qual você pode ler mais sobrein this article.
4. Despejo de valores
A cafeína temthree strategies for value eviction: com base no tamanho, com base no tempo e com base na referência.
4.1. Despejo com base no tamanho
Este tipo de despejo assume queeviction occurs when the configured size limit of the cache is exceeded. Existemtwo ways of getting the size - contando objetos no cache ou obtendo seus pesos.
Vamos ver como poderíamoscount objects in the cache. Quando o cache é inicializado, seu tamanho é igual a zero:
LoadingCache cache = Caffeine.newBuilder()
.maximumSize(1)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
Quando adicionamos um valor, o tamanho obviamente aumenta:
cache.get("A");
assertEquals(1, cache.estimatedSize());
Podemos adicionar o segundo valor ao cache, o que leva à remoção do primeiro valor:
cache.get("B");
cache.cleanUp();
assertEquals(1, cache.estimatedSize());
Vale a pena mencionar que nóscall the cleanUp method before getting the cache size. Isso ocorre porque a remoção do cache é executada de forma assíncrona e este métodohelps to await the completion of the eviction.
Também podemospass a weigherFunction para obter o tamanho do cache:
LoadingCache cache = Caffeine.newBuilder()
.maximumWeight(10)
.weigher((k,v) -> 5)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
cache.get("A");
assertEquals(1, cache.estimatedSize());
cache.get("B");
assertEquals(2, cache.estimatedSize());
Os valores são removidos do cache quando o peso é superior a 10:
cache.get("C");
cache.cleanUp();
assertEquals(2, cache.estimatedSize());
4.2. Despejo com base no tempo
Esta estratégia de despejo ébased on the expiration time of the entrye tem três tipos:
-
Expire after access - a entrada expira após o período decorrido desde a última leitura ou gravação ocorrida
-
Expire after write - a entrada expira após o período decorrido desde a última gravação
-
Custom policy - um tempo de expiração é calculado para cada entrada individualmente pela implementaçãoExpiry
Vamos configurar a estratégia de expiração após acesso usando o métodoexpireAfterAccess:
LoadingCache cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
Para configurar a estratégia de expiração após gravação, usamos o métodoexpireAfterWrite:
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
Para inicializar uma política personalizada, precisamos implementar a interfaceExpiry:
cache = Caffeine.newBuilder().expireAfter(new Expiry() {
@Override
public long expireAfterCreate(
String key, DataObject value, long currentTime) {
return value.getData().length() * 1000;
}
@Override
public long expireAfterUpdate(
String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(
String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
}).build(k -> DataObject.get("Data for " + k));
4.3. Remoção baseada em referência
Podemos configurar nosso cache para permitirgarbage-collection of cache keys and/or values. Para fazer isso, configuraríamos o uso deWeakRefence para chaves e valores, e podemos configurarSoftReference para coleta de lixo apenas de valores.
O uso deWeakRefence permite a coleta de lixo de objetos quando não há nenhuma referência forte ao objeto. SoftReference permite que os objetos sejam coletados como lixo com base na estratégia global de uso mínimo recente da JVM. Mais detalhes sobre referências em Java podem ser encontradoshere.
Devemos usarCaffeine.weakKeys(),Caffeine.weakValues(),eCaffeine.softValues() para habilitar cada opção:
LoadingCache cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.softValues()
.build(k -> DataObject.get("Data for " + k));
5. Refrescante
É possível configurar o cache para atualizar as entradas após um período definido automaticamente. Vamos ver como fazer isso usando o métodorefreshAfterWrite:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
Aqui devemos entender adifference between expireAfter and refreshAfter. Quando a entrada expirada é solicitada, uma execução é bloqueada até que o novo valor tenha sido calculado pelo buildFunction.
Mas se a entrada for elegível para a atualização, o cache retornará um valor antigo easynchronously reload the value.
6. Estatisticas
A cafeína tem uma média derecording statistics about cache usage:
LoadingCache cache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");
assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());
Também podemos passar para o fornecedorrecordStats, que cria uma implementação doStatsCounter.. Este objeto será enviado com cada alteração relacionada à estatística.
7. Conclusão
Neste artigo, nos familiarizamos com a biblioteca de cache de cafeína para Java. Vimos como configurar e preencher um cache, bem como escolher uma política de expiração ou atualização apropriada de acordo com nossas necessidades.
O código-fonte mostrado aqui está disponívelover on Github.