Criando cache de objetos com Google Guava

Podemos definir cache como um mecanismo utilizado para acelerar a busca por informações em um sistema. Em nossas aplicações realizamos consultas ao banco de dados, buscamos informações em arquivos, executamos cálculos que muitas vezes retornam sempre os mesmos valores não sendo necessário acessar a todo o momento o banco de dados, por exemplo, para buscar determinada informação.

Outra situação, imagine o custo de abrir um arquivo “.txt” de milhões de linhas para buscar uma informação depois fechar esse arquivo e quando necessitar da mesma informação, que pode ocorrer várias vezes, ter que repetir esse processo novamente.

São nessas situações que se recomenda o uso de cache, fazendo com que o ganho de desempenho da aplicação seja notório, pois os maiores problemas de performance em uma aplicação estão ligados as operações de leitura de um recurso (arquivo, banco de dados, sistemas de arquivo, etc)

Neste post, vamos entender como criar cache de objetos utilizando a biblioteca Google Guava. Antes de tudo, para utilizar o Google Guava em nossa aplicação devemos incluir a dependência do mesmo em nosso pom.xml como mostrado abaixo:

<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>18.0</version>
</dependency>
Listagem 1 – Configuração do pom.xml para incluir o Google Guava

O Guava fornece uma maneira bem flexível e poderosa de realizar cache em memória dos objetos de uma aplicação. Podemos definir, por exemplo, o tamanho máximo do cache, ou seja, a quantidade de objetos que podem ficar armazenados em cache e o seu tempo de expiração.

Uma observação importante aqui é: Sempre que criarmos uma estrutura de cache é necessário definir um tempo máximo para que esse cache permaneça ativo em memória e uma quantidade máxima de objetos que podem ser inseridos nessa estrutura.

Devemos tomar esse cuidado, não permitindo que um cache acumule uma quantidade desnecessária de informação e fique ativo por um tempo muito longo, podendo consumir uma quantidade de memória além do necessário. Uma estrutura de cache criada com o objetivo de trazer ganhos de desempenho para a aplicação, quando utilizada sem o devido critério pode trazer sérios problemas.

Após essa explicação, vamos entender na prática como criar e buscar informações armazenadas em cache.
O Google Guava possui a classe CacheBuilder, utilizada para criar uma estrutura de cache como podemos ver na Listagem 2.

     LoadingCache pessoaCache =
               CacheBuilder.newBuilder().maximumSize(10) // cache irá armazenar no máximo 10 objetos
	             .expireAfterAccess(30, TimeUnit.MINUTES)  // cache irá expirar depois de 30 minutos
		     .build(new CacheLoader() { // construir o cache  loader
		         @Override
			 public Pessoa load(Long pessoaId) throws Exception {
				// carrega uma pessoa que nao está no cache
				return getPessoaFonteDados(pessoaId);
			}
    });
Listagem 2 – Criando a estrutura de cache de objetos

Nessa listagem, estamos criando uma estrutura de cache “pessoaCache” para objetos do tipo Pessoa. Para fins de exemplo, a classe Pessoa contém apenas os atributos Id e Nome.

Ao instanciarmos a classe CacheBuilder, definimos no método maximumSize() que a quantidade máxima de objetos do tipo Pessoa em cache será 10. Na sequência, através do método expireAfterAccess(), configuramos que o cache irá expirar depois de 30 minutos.

Depois de realizadas essas configurações o método build() é executado para que a estrutura de cache seja criada. Um detalhes importante, é que esse método recebe como parâmetro um instancia da classe CacheLoader.

Essa classe é responsável por buscar a informação desejada diretamente no cache de objetos. Se a busca no cache encontrar a informação desejada a mesma será retornada, caso contrário o método load(), será executado para que a busca seja feita na fonte de dados da aplicação.

Em nosso exemplo, o método getPessoaFonteDados(), da Listagem 3, irá simular uma busca em uma fonte de dados fora da estrutura de cache. Como podemos observar na Listagem 3 estamos utilizando um Map como repositório de dados, mas poderia ser uma tabela no banco de dados, por exemplo.

private static Pessoa getPessoaFonteDados(Long pessoaId) {
     Pessoa pessoa1 = new Pessoa(1l, "Maria");
     Pessoa pessoa2 = new Pessoa(2l, "Joao");
     Pessoa pessoa3 = new Pessoa(3l, "Paulo");
     Pessoa pessoa4 = new Pessoa(3l, "Mariana");

     Map<Long, Pessoa> dadosPessoa = new HashMap<Long, Pessoa>();

     dadosPessoa.put(1L, pessoa1);
     dadosPessoa.put(2L, pessoa2);
     dadosPessoa.put(3L, pessoa3);
     dadosPessoa.put(4L, pessoa4);

     System.out.println("Buscando informacao na Fonte de Dados: " + pessoaId);
     return database.get(pessoaId);
}
Listagem 3 – Método que realiza a busca por Pessoas

Agora que temos nossa estrutura de cache criada vamos testá-la para garantir que está funcionando como esperado.

Dessa forma, sempre que formos consultar uma Pessoa devemos buscar o objeto no cache, como mostra o método buscarPessoaPorId() da Listagem 4, pois como vimos anteriormente se a informação estiver em cache será retornada, senão uma consulta será realizada na fonte de dados da aplicação.

private Pessoa buscarPessoaPorId(Long pessoaId) throws ExecutionException {
       return pessoaCache.get(pessoaId);
}
Listagem 4 – Método que busca no cache uma Pessoa por ID

Na Listagem 5, chamamos o método buscarPessoaPorId() passando o ID da pessoa que queremos buscar como parâmetro.

        //Informação nao esta em cache
        System.out.println(buscarPessoaPorId(1l));
         
        //Informação esta em cache
        System.out.println(buscarPessoaPorId(1l));
Listagem 5 – Execução da busca por Pessoa no cache da aplicação

Na primeira linha, buscamos o objeto Pessoa pelo ID 1. Nesse momento, como estamos buscando as informações de Pessoa pela primeira vez o cache não contém nenhum objeto. Portanto, a busca é direcionada para o método getPessoaFonteDados(), da Listagem 3. A saída no console será:

Buscando informação na Fonte de Dados. ID Pessoa: 1
Pessoa [id=1, nome=Maria]

Já na segunda linha, quando buscamos o mesmo objeto Pessoa de ID 1, a saída no console será:

Pessoa [id=3, nome=Mariana]

Como podemos observar, na segunda saída do console a Pessoa de ID 1 veio do cache e não da fonte de dados. Isso aconteceu porque na primeira linha quando a busca foi realizada na fonte de dados o resultado da busca ficou gravado no cache “pessoaCache” (ver Listagem 2).

Assim, na segunda vez que pesquisamos uma Pessoa por ID 1 esse objeto foi encontrado no cache e retornado, não sendo necessário buscar a informação fora da estrutura de cache.

Além de resultados de consultas, estruturas de cache podem ser utilizadas para guardar resultados de cálculos matemáticos complexos e demorados que sempre retornam um mesmo resultado dado um parâmetro específico como, por exemplo, o cálculo da sequência de Fibonacci.

Na Listagem 6, temos um exemplo completo de como utilizar o cache do Google Guava para guardar o resultado dos cálculos da sequência de Fibonacci.

      
        //Estrutura de cache para Fibonacci
	LoadingCache<Long, Long> fibonacciCache =
            CacheBuilder.newBuilder().maximumSize(10).expireAfterAccess(30, TimeUnit.MINUTES) 
			.build(new CacheLoader<Long, Long>() { 
				@Override
				public Long load(Long n) throws Exception {
				       // carrega um valor de fibonacci que nao está no cache
					System.out.println("Calculando Sequencia Fibonacci ...");
					return fibo(n);
				}
	});
	
	//calcula um novo valor de fibonacci
	private Long fibo(Long n) {		
		if (n < 2) {
			return n;
		} else {
			return fibo(n - 1) + fibo(n - 2);
		}		
	}

	//Busca fibonacci do cache
	private Long calcularFibonacci(Long n) {
		try {
		  return fibonacciCache.get(n);
		} catch (ExecutionException e) {
		  e.printStackTrace();
		}
		return null;
	}
	
	public static void main(String[] args) {
		FibonacciCache fibonacciCache = new FibonacciCache();
		Long fib5 = fibonacciCache.calcularFibonacci(5l);
		System.out.println(fib5);
		
		Long fib4 = fibonacciCache.calcularFibonacci(4l);
		System.out.println(fib4);
		
		Long fib5Cache = fibonacciCache.calcularFibonacci(5l);
		System.out.println(fib5Cache);
	}
Listagem 6 – Criando cache dos resultados do cálculo da Sequência de Fibonacci

O código da Listagem 6 é bem parecido com os demonstrados nas listagens anteriores. Primeiro, criamos uma estrutura de cache do Guava com a classe CacheBuilder(), definimos a quantidade de objetos a serem armazenados em cache e seu tempo de expiração.

Na sequência definimos, que se o valor da sequência de Fibonacci não estiver em cache o método fibo(), responsável por calcular a sequência de Fibonacci, será executado por meio do método load() da classe CacheLoader.

Já no método principal da classe (método void main()) chamamos o método calcularFibonacci(), algumas vezes, passando como parâmetro um valor para ser calculado.

O método calcularFibonacci() tenta recuperar do cache o valor da sequência de Fibonacci através da chamada fibonacciCache.get(n). Se esse valor já estiver armazenado em cache nenhum cálculo matemático é realizado e o retorno da informação é imediato.

O intuito desse exemplo, não é entrar em detalhes de como é realizada a lógica para calcular a sequência de Fibonacci, mas sim chamar a atenção do leitor de que os recursos de cache do Google Guava, podem ser utilizados para guardar e recuperar resultados de cálculos matemáticos que muitas vezes podem ser lentos e complexos de serem realizados.

Nesse Post, demonstramos que o Google Guava fornece uma maneira simples e intuitiva de criar caches de objetos.

Entretanto, existem vários outros recursos disponíveis na biblioteca para incrementarmos ainda mais a criação de caches que não poderiam ser abordados em um único Post. Sendo assim, convido o leitor a consultar a documentação da biblioteca e estudar um pouco mais sobre esse importante recurso que pode trazer ganhos de performance reais para nossas aplicações.

O código fonte utilizado nos exemplos está disponível nas referências.

REFERÊNCIAS

https://code.google.com/p/guava-libraries/

http://www.infoq.com/br/news/2010/10/google-guava

https://github.com/google/guava/wiki

Códigos Fonte de exemplo

Por EDMAR BREGAGNOLI

Postado em: 14 de outubro de 2015

Confira outros artigos do nosso blog

[Webinar] Profile de aplicações Java com Oracle Mission Control e Flight Recorder

24 de julho de 2017

Danival Calegari

Criando Mocks de serviços REST com SoapUI

27 de junho de 2017

Monise Costa

Three laws that enable agile software development

09 de março de 2017

Celso Gonçalves Junior

Medindo performance de uma API REST

21 de fevereiro de 2017

Monise Costa

Deixe seu comentário