Porque este artigo é útil
Este artigo apresenta em detalhes como funciona o Cache de 2º Nível no Hibernate. Embora seja uma API simples de se configurar e verificar imediatamente melhorias de desempenho em cenários triviais, sua ativação pode trazer alguns imprevistos no comportamento de uma aplicação, criando situações em que há degradação de performance ou inconsistências em processos transacionais se a configuração ou a forma de uso do Cache não estiver correta. Sendo assim, ao longo do artigo apresentaremos alguns aspectos teóricos que devem ser conhecidos para fazer o bom uso da API de Cache de 2º Nível, explorando algumas de suas opções de configuração e como as mesmas devem ser refletidas ao se parametrizar o EhCache como mecanismo de armazenamento.

Eventualmente toda aplicação encontra os famosos “gargalos”, ou seja, alguma funcionalidade que não provê tempos de resposta adequados para seus usuários, o que causa descontentamentos e frustrações aos mesmos e muitas vezes os leva à desistência da utilização da funcionalidade ou do produto como um todo.

Em muitas dessas situações, o “culpado” é o Banco de Dados, que, como qualquer programa, possui uma quantidade limitada de recursos para atender centenas de solicitações de forma concorrente. Dentre esses recursos os maiores limitadores em geral são os discos rígidos mecânicos, que são por natureza aparatos seriais: a “agulha” que lê o disco só pode acessar um setor por vez. Mesmo quando amparado por tecnologias de ponta, como soluções de storage e discos de estado sólido, um Banco de Dados precisa superar outra barreira de I/O para atender seus clientes: a rede.

Em um processo de longa duração, quando um subconjunto dos dados utilizados é frequentemente requisitado, muitas vezes a execução pode ser otimizada mantendo estes dados temporariamente em um nível mais próximo da aplicação, como uma forma de diminuir a latência no acesso aos mesmos. Esta técnica é conhecida como cache e é frequentemente utilizada em dezenas de pontos de uma aplicação, de forma simples (e.g. um HashMap) ou sofisticada (e.g. um cache distribuído), visando ora obter melhores resultados de desempenho, ora manter a consistência de um programa.

As próprias APIs do Java se beneficiam do uso de caches. Por exemplo, reutilizar uma referência de uma expressão regular criada via Pattern.compile(“..”) é muito mais eficiente do que solicitar frequentemente a sua criação; ClassLoaders devem manter em memória o histórico de classes já carregadas; especializações da classe Number (e.g. Long) mantêm um cache de suas instâncias para minimizar o efeito de boxing de seus tipos primitivos correspondentes.

Ao trabalhar com um projeto “real”, a decisão de incluir um Cache pode não ser trivial, dependendo dos frameworks e fronteiras transacionais envolvidas nos processos de negócio. Neste artigo, veremos como podemos configurar um cache de entidades e consultas de banco de dados em um “mashup” de frameworks frequentemente utilizados: Hibernate, Spring e EhCache.

Cache

A ideia essencial de um cache é guardar resultados que são custosos para se calcular ou obter, de tal forma que futuras solicitações pelos resultados possam ser obtidas mais rapidamente.

Em Java, quando se fala em cache, imediatamente somos remetidos a estruturas de dados do tipo Map, que associam chaves (parâmetros) a valores (resultados). Entretanto os Maps (que são distribuídos no JDK), embora estejam presentes no núcleo das implementações de um cache, não se preocupam com algumas características essenciais que são necessárias e inerentes a soluções profissionais de cache, que discutiremos a seguir.

Limitação do número de elementos

Memória e discos são limitados. Se um cache crescer indefinidamente, seus benefícios reverter-se-ão em prejuízos. Em especial, quando se trata de linguagens gerenciadas como o Java, o uso excessivo de memória pode causar pausas muito longas no garbage collector a ponto de inutilizar uma aplicação.

Políticas de expurgo

Políticas de expurgo definem critérios para remoção de objetos de um cache assim que o mesmo atinge o número máximo de objetos a serem mantidos, ou seja, quais itens são “antigos” para serem removidos de modo a liberar espaço para “novos” itens. Dentre as políticas mais comuns, podemos citar:

· LRU (Least Recently Used): expira-se elementos que tenham sido acessados pela última vez hámais tempo;

· LFU (Least Frequently Used): expiram-se elementos com o menor número de acessos;

· FIFO (First In First Out): expiram-se elementos mais antigos, ou seja, os que entraram no cache primeiro, como uma fila.

Algumas soluções de caching, como o JBoss Infinispan, oferecem políticas de expurgo mais sofisticadas como a LIRS (Low Inter-reference Recency Set), que é uma variação do LRU. A política a ser escolhida depende de como o cache será utilizado.

No geral, FIFO não é uma boa escolha para o cache de Entidades, pois o uso típico neste caso é manter em cache as que são mais frequentemente acessadas. Entretanto, FIFO pode ser uma escolha adequada em algoritmos cujo acesso aos elementos é praticamente aleatório e, na média, com a mesma frequência.

Quando utilizar e o que colocar em Cache?

Essas perguntas infelizmente não têm uma resposta simples. É necessário conhecer os processos de uma aplicação, suas frequências de utilização e identificar quais estruturas de dados são necessárias para executá-los. Só assim somos capazes de avaliar se é possível ou vale a pena utilizar uma solução de cache.

Como 'regra' geral, um ponto de partida é a chamada regra de Pareto, a qual se aplica em uma situação específica, porém comum, e ideal para utilização de caches: “Se 20% do conteúdo de uma aplicação é responsável por 80% das requisições de um sistema, ao minimizar a latência desses 20% estaremos minimizando 80% da latência do sistema”. Claro que esses números podem variar e eventualmente 5% podem representar centenas de gigabytes de dados, o que pode dificultar e influenciar bastante a decisão sobre o que se colocar em cache.

EhCache

O EhCache foi um dos primeiros projetos dedicados à construção de caches para Java. Sua primeira versão data do ano 2003 e em 2009 o projeto foi adquirido pela empresa Terracotta. Hoje o projeto do EhCache é distribuído de forma open source e também comercial, contando com alguns módulos extras, em sua maioria também open source.

Desde sua concepção até sua aquisição pela Terracotta, o EhCache evoluiu de uma “simples” solução de cache em memória para uma solução capaz de atender outras demandas não-funcionais além de performance, tais como capacidade (opção de salvar registros em disco) e escalabilidade (integração com produtos Terracotta). Atualmente o EhCache chama a atenção por oferecer capacidades de consultas NoSQL rápidas (integração com o projeto Lucene) e pela opção de armazenamento de centenas de gigabytes de objetos em memória fora do Heap da JVM (BigMemory).

Como o foco deste artigo é na integração do EhCache com o Cache de 2º Nível do Hibernate, vamos explorar apenas os aspectos mais simples de suas configurações, nos limitando apenas a caches em memória.

Caches no Hibernate

O Hibernate trabalha com dois tipos de cache, rotulados Cache de 1º Nível (C1N) e Cache de 2º Nível (C2N). Ao contrário do C1N que sempre é utilizado, o C2N precisa ser explicitamente habilitado na configuração da SessionFactory. Nas seções seguintes vamos detalhar primeiramente como funciona o C2N e posteriormente como configurá-lo.

Cache de 1º Nível

O conceito de Cache de 1º Nível (C1N) é um dos primeiros que desenvolvedores são obrigados a compreender quando começam a trabalhar com o Hibernate.

Essencialmente o C1N é um mapa em memória que mantém quais entidades já foram carregadas por uma Session, a qual é frequentemente referenciada como sendo o próprio C1N. Não há muito segredo com relação ao C1N, exceto que se deve lembrar que o mesmo é uma estrutura em memória, isto é, sempre que possível devemos evitar sessões muito longas e que carregam objetos demais, de forma a colocar pressão no Garbage Collector e degradar o desempenho geral da JVM.

Cache de 2º Nível

O Cache de 2º Nível (C2N) atua de maneira completamente diferente do C1N, não é habilitado por padrão e requer configuração adicional para funcionar. Porém, tão importante quanto saber configurar é compreender como funciona “por baixo dos panos” a API de C2N no Hibernate, de modo que possamos tirar o melhor proveito da tecnologia, seja modificando o código para trabalhar em favor do C2N, seja evitando situações em que o uso do C2N possa piorar o desempenho da aplicação.

Visibilidade global e ciclo de vida estendido

...
Quer ler esse conteúdo completo? Tenha acesso completo