Guava Cache é uma excelente estrutura de cache local.
1. Configuração clássica
A estrutura de dados do Guava Cache é semelhante ao ConcurrentHashMap do JDK1.7. Ele fornece três estratégias de reciclagem baseadas em tempo, capacidade e referência, além de funções como carregamento automático e estatísticas de acesso.
Configuração básica
@Test
public void testLoadingCache() throws ExecutionException {
CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println("加载 key:" + key);
return "value";
}
};
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
//最大容量为100(基于容量进行回收)
.maximumSize(100)
//配置写入后多久使缓存过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//配置写入后多久刷新缓存
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(cacheLoader);
cache.put("Lasse", "穗爷");
System.out.println(cache.size());
System.out.println(cache.get("Lasse"));
System.out.println(cache.getUnchecked("hello"));
System.out.println(cache.size());
}
No exemplo, a capacidade máxima do cache é definida como 100 ( reciclagem com base na capacidade ) e a política de invalidação e a política de atualização são configuradas .
1. Estratégia de falha
Quando configurados expireAfterWrite
, os itens de cache expiram dentro de um período de tempo especificado após serem criados ou atualizados pela última vez.
2. Estratégia de atualização
Configure refreshAfterWrite
o tempo de atualização para que novos valores possam ser recarregados quando os itens armazenados em cache expirarem.
Neste exemplo, alguns alunos podem ter dúvidas: Por que precisamos configurar a estratégia de atualização? Não basta apenas configurar a estratégia de invalidação ?
Claro que é possível, mas em cenários de alta simultaneidade, configurar a estratégia de atualização será milagroso. A seguir, escreveremos um caso de teste para facilitar a compreensão de todos sobre o modelo de thread do Gauva Cache.
2. Entenda o modelo de thread
Simulamos a operação de "expiração de cache e execução do método de carregamento" e "atualização e execução do método de recarga" em um cenário multithread.
@Test
public void testLoadingCache2() throws InterruptedException, ExecutionException {
CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(Thread.currentThread().getName() + "加载 key" + key);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "value_" + key.toLowerCase();
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
System.out.println(Thread.currentThread().getName() + "加载 key" + key);
Thread.sleep(500);
return super.reload(key, oldValue);
}
};
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
//最大容量为20(基于容量进行回收)
.maximumSize(20)
//配置写入后多久使缓存过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//配置写入后多久刷新缓存
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(cacheLoader);
System.out.println("测试过期加载 load------------------");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
long start = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "开始查询");
String hello = cache.get("hello");
long end = System.currentTimeMillis() - start;
System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
cache.put("hello2", "旧值");
Thread.sleep(2000);
System.out.println("测试重新加载 reload");
//等待刷新,开始重新加载
Thread.sleep(1500);
ExecutorService executorService2 = Executors.newFixedThreadPool(5);
// CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < 5; i++) {
executorService2.execute(new Runnable() {
@Override
public void run() {
try {
long start = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "开始查询");
//cyclicBarrier.await();
String hello = cache.get("hello2");
System.out.println(Thread.currentThread().getName() + ":" + hello);
long end = System.currentTimeMillis() - start;
System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
Thread.sleep(9000);
}
Os resultados da execução são mostrados na figura abaixo
Os resultados da execução mostram que: O Guava Cache não possui um thread de tarefa em segundo plano para executar de forma assíncrona o método de carregamento ou recarregamento.
-
Estratégia de invalidação :
expireAfterWrite
permite que um thread execute o método de carregamento, enquanto outros threads bloqueiam e esperam.Quando um grande número de threads obtém valores armazenados em cache com a mesma chave, apenas um thread entrará no método de carregamento, enquanto outros threads aguardam até que o valor armazenado em cache seja gerado. Isso também evita o risco de quebra do cache. Em cenários de alta simultaneidade, isso ainda bloqueará um grande número de threads.
-
Estratégia de atualização :
refreshAfterWrite
permite que um thread execute o método de carregamento e outros threads retornem o valor antigo.Sob simultaneidade de chave única, o uso de refreshAfterWrite não bloqueará, mas se várias chaves expirarem ao mesmo tempo, ainda exercerá pressão sobre o banco de dados.
Para melhorar o desempenho do sistema, podemos otimizar os dois aspectos a seguir:
-
Configure atualização <expirar para reduzir a probabilidade de bloquear um grande número de threads;
-
Adote uma estratégia de atualização assíncrona , ou seja, o thread carrega os dados de forma assíncrona, durante a qual todas as solicitações retornam o valor antigo do cache para evitar avalanches de cache.
A figura abaixo mostra o cronograma do plano de otimização:
3. Duas maneiras de implementar atualização assíncrona
3.1 Substituir o método reload
ExecutorService executorService = Executors.newFixedThreadPool(5);
CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(Thread.currentThread().getName() + "加载 key" + key);
//从数据库加载
return "value_" + key.toLowerCase();
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
System.out.println(Thread.currentThread().getName() + "异步加载 key" + key);
return load(key);
});
executorService.submit(futureTask);
return futureTask;
}
};
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
//最大容量为20(基于容量进行回收)
.maximumSize(20)
//配置写入后多久使缓存过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//配置写入后多久刷新缓存
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(cacheLoader);
3.2 Implementar o método asyncReloading
ExecutorService executorService = Executors.newFixedThreadPool(5);
CacheLoader.asyncReloading(
new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(Thread.currentThread().getName() + "加载 key" + key);
//从数据库加载
return "value_" + key.toLowerCase();
}
}
, executorService);
4. Atualização assíncrona + cache multinível
Cenas :
Uma empresa de comércio eletrônico precisa otimizar o desempenho da interface da página inicial do aplicativo. O autor levou cerca de dois dias para concluir toda a solução, usando um modo de cache de dois níveis e o mecanismo de atualização assíncrona do Guava.
A arquitetura geral é mostrada na figura abaixo:
O processo de leitura do cache é o seguinte :
1. Quando o gateway de negócios acaba de ser iniciado, não há dados no cache local. Leia o cache Redis. Se não houver dados no cache Redis, chame o serviço de guia de compras por meio de RPC para ler os dados e, em seguida, grave o dados para o cache local e Redis; se o cache Redis Se não estiver vazio, os dados armazenados em cache serão gravados no cache local.
2. Como o cache local foi aquecido na etapa 1, as solicitações subsequentes leem diretamente o cache local e o retornam ao usuário.
3. O Guava é configurado com um mecanismo de atualização, que chamará o pool de threads LoadCache personalizado (5 threads no máximo, 5 threads principais) de vez em quando para sincronizar dados do serviço de guia de compras para o cache local e Redis.
Após a otimização, o desempenho é muito bom, o consumo médio de tempo é de cerca de 5ms e a frequência de aplicação do GC é bastante reduzida.
Esta solução ainda apresenta falhas: uma noite descobrimos que os dados exibidos na página inicial do aplicativo eram ora iguais, ora diferentes.
Ou seja: embora o thread LoadCache esteja chamando a interface para atualizar as informações do cache, os dados no cache local de cada servidor não são completamente consistentes.
Isso ilustra dois pontos muito importantes:
1. O carregamento lento ainda pode causar inconsistência de dados em várias máquinas;
2. O número de pools de threads do LoadCache não está configurado de maneira razoável, resultando em um acúmulo de tarefas.
A solução sugerida é :
1. A atualização assíncrona combina o mecanismo de mensagem para atualizar os dados do cache, ou seja: quando a configuração do serviço de guia de compras muda, o gateway de negócios é notificado para extrair novamente os dados e atualizar o cache.
2. Aumente adequadamente os parâmetros do pool de threads do LoadCache e enterre os pontos no pool de threads para monitorar o uso do pool de threads.Quando o thread está ocupado, um alarme pode ser emitido e, em seguida, os parâmetros do pool de threads podem ser modificados dinamicamente.
5. Resumo
O Guava Cache é muito poderoso. Ele não possui um thread de tarefa em segundo plano para executar o método de carregamento ou recarregamento de forma assíncrona. Em vez disso, ele executa operações relacionadas por meio de threads de solicitação.
Para melhorar o desempenho do sistema, podemos lidar com isso a partir dos dois aspectos a seguir:
-
Configure atualizar < expirar para reduzir a probabilidade de bloquear um grande número de threads.
-
Adote uma estratégia de atualização assíncrona , ou seja, o thread carrega os dados de forma assíncrona, durante a qual todas as solicitações retornam o antigo valor armazenado em cache .
No entanto, ainda precisamos considerar questões de cache e consistência do banco de dados ao usar essa abordagem.