Masterizando cache em aplicações – Introdução

Quando falamos de cache na área da computação, podemos simplificar dizendo que cache é uma memória de alta velocidade que armazena dados temporários frequentemente acessados e/ou utilizados. Existem diversos tipos de cache, cada um com uma função específica: cache de navegador, sistema operacional, aplicativos e hardware.

Essas operações muitas vezes passam despercebidas em nosso cotidiano, como quando acessamos várias vezes o feed das redes sociais em um curto espaço de tempo e a página não se atualiza. Isso pode ser um indicativo de que o cache está atuando.

O cache é amplamente utilizado para melhorar a performance, diminuindo o tempo de resposta e a escrita na memória principal. Com o benefício secundário de reduzir custos.

Nosso foco nessa série de publicações é aprofundar o conhecimento de cache em aplicações, abordando estratégias de cache em memória, cache distribuído e estratégias de substituição, além de implementações usando spring e java, e a análise de métricas e resultados.

Tipos de armazenamento

  • Em memória (Local)

Ao falarmos de armazenamento em memória ou local, estamos nos referindo a dados armazenados em cache que são visíveis e acessíveis apenas pela instância da aplicação que os mantém. Uma parcela da memória da aplicação é utilizada para o cache.

A seguir, um exemplo simples: toda vez que queremos consultar um usuário, verificamos primeiro se ele já existe na tabela hash userCache. Caso não exista, é feita a busca em um recurso externo.

public class UserService {

    private final Map<Integer, String> userCache = new Hashtable<>();

    public User retrieveUser(Integer id, String name) {

        User user;
        if (userCache.containsKey(id)) {
            user = new User(id, userCache.get(id));
        } else {
            // Get user from external source
            user = new User(id, getUser(id));
            userCache.put(id, name);
        }

        return user;
    }
}

A tabela hash é uma estrutura de dados chave-valor muito eficiente para o armazenamento em cache, pois as operações nessa estrutura têm complexidade de tempo O(1) na média, quando não há colisões de hash. Redis e Memcached são exemplos de sistemas que utilizam tabelas hash para armazenar dados.

O exemplo anterior serve apenas para ilustrar. Atualmente, existem diversas bibliotecas no mercado que implementam e auxiliam na gestão de caches, utilizando técnicas sofisticadas como políticas de evicção, atualização e tempo de vida.

Libraries de cache em memoria:

Curiosidade sobre o Spring Cache: Se nenhum provedor de cache em memória for configurado, o Spring utilizará internamente o ConcurrentHashMap para armazenar os dados em cache. O ConcurrentHashMap é uma implementação thread-safe do Map do Java, o que o torna adequado para ambientes multithread, garantindo a consistência dos dados mesmo em cenários de alta concorrência.

Essa abordagem é recomendada para aplicações de instância única que podem ser escaladas verticalmente, adicionando mais memória e CPU. É indicada para cenários simples, como o cache de CEPs, onde a frequência de atualização dos dados é baixa.

  • Distribuído

A principal diferença entre o cache distribuído e o cache em memória reside no local onde os dados são armazenados. Enquanto o cache em memória é limitado ao escopo de uma única aplicação, o cache distribuído permite que múltiplas aplicações acessem os mesmos dados a partir de um armazenamento externo, como um servidor dedicado ou um serviço em nuvem.

Dessa forma, o cache distribuído oferece maior compartilhamento e escalabilidade, permitindo que diversas instâncias de uma aplicação ou até mesmo aplicações diferentes se beneficiem dos mesmos dados em cache.

Essa abordagem é recomendada para aplicações distribuídas e escaláveis horizontalmente, como microserviços e aplicações nativas em nuvem. É indicada para cenários que exigem alta disponibilidade e alto volume de dados.

Provedores de nuvem como a AWS ElasticCache oferecem serviços de cache gerenciados que eliminam a necessidade de configurar e gerenciar a infraestrutura subjacente. Embora os custos possam variar dependendo de fatores como o tipo de instância, o volume de dados e a região, a flexibilidade e a escalabilidade desses serviços geralmente compensam o custo adicional. Ao abstrair a complexidade da gestão do cache, esses serviços permitem que os desenvolvedores se concentrem em suas aplicações, otimizando o desempenho e a escalabilidade.

Prós e Contras

A seleção do tipo de armazenamento ideal envolve uma análise cuidadosa das necessidades da aplicação. Cada solução apresenta características únicas, com pontos fortes e fracos. É fundamental ponderar fatores como o volume de dados, a latência, a consistência e o custo ao tomar essa decisão.

Na próxima publicação, abordaremos as diversas estratégias de cache e suas respectivas implementações.