Guava: Cache, uma poderosa estrutura de cache local

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.

  1. 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.

  2. 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:

  1. Configure atualização <expirar para reduzir a probabilidade de bloquear um grande número de threads;

  2. 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:

  1. Configure atualizar < expirar para reduzir a probabilidade de bloquear um grande número de threads.

  2. 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. 

Acho que você gosta

Origin blog.csdn.net/qq_63815371/article/details/135428100
Recomendado
Clasificación