Cache distribuído com Redis no Spring Boot

Cache é uma das abordagens para otimizar acesso à dados dos sistemas, onde evitamos requisições repetitivas nas fontes originais dos dados, que geralmente são grandes estruturas, complexas e nem sempre performáticas, assim com cache, passamos a consultar locais mais otimizados, que provêm acessos rápidos através de chaves.

Há diversas tecnologias de cache para utilizarmos nas aplicações Java, como: EHCache, Redis, Infinispan, Caffeine, etc, porém quando começamos a se preocupar com escalabilidade das nossas aplicações, consequentemente em aumentar o número de instâncias simultâneas das nossas aplicações, precisamos pensar em provedores que nos forneçam a possibilidade de cache distribuído, de forma que as informações armazenadas em cache possam ser compartilhada entre as instâncias, assim aprimorando o uso dos cache entre as aplicações, além de evitar problemas de validade dos caches entre as aplicações concorrentes.

Nesse post vamos utilizar o Redis, que é uma solução open source para armazenamento de estrutura de dados em memória, o qual pode ser utilizada como banco de dados, cache ou message broker.

Exemplo

No exemplo a seguir vamos configurar uma aplicação Spring Boot para utilizar o Redis como provedor de cache distribuído, assim a aplicação possui seu banco de dados, exemplificado na tecnologia do H2 e utiliza o Redis como provedor cache, dessa forma, o gerenciamento do cache não fica dentro da aplicação e sim no Redis, possibilitando que outras aplicações reaproveitem a mesma fonte de cache, caracterizando o cache como distribuído.

Redis

Vamos começar pela inicialização do Redis, o qual podemos subir através de uma imagem docker.

docker run -it \
    --name redis \
    -p 6379:6379 \
    redis:5.0.3

Configuração do Projeto

Vamos adicionar as seguintes dependências no projeto Spring Boot: starter-web para disponibilizar os serviços, starter-data-jpa pois vamos utilizar o banco relacional h2 para armazenamento definitivo dos dados.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
    <scope>runtime</scope>
</dependency>

Obs: Como é apenas um exemplo foi utilizado o h2 para simplificar, mas poderia ser qualquer banco de dados no lugar do h2 para armazenar os dados.

Após a configuração do banco de dados, vamos adicionar a dependência starter-data-redis que será responsável por ativar as funcionalidades de cache na aplicação Spring Boot e definir a implementação do Redis.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

Em termos de configurações, precisamos configurar o tipo de cache e o endereço do Redis. Caso não especificado o tipo de cache, o Spring Boot utiliza por padrão um ConcurrentHashMap para armazenar os caches, seguindo a abstração da especificação JSR-107 (JCache).

server.port=8080

spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379

spring.datasource.url=jdbc:h2:mem:db

Por fim, é necessário habilitar o cache na aplicação, com a anotação @EnableCaching.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class AppConfig {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

Utilização

Após as configurações, o projeto está pronto para utilizar cache com Redis, para isso, vamos adicionar cache na camada de serviço que possibilita maior reaproveitamento dos caches, onde podem ser chamados através de endpoints (Controllers) ou por outros serviços locais.

Como mencionado anteriormente, o Spring Boot utiliza a JSR-107 como padrão, com isso as anotações de cache: @Cacheable, @CacheEvict e @CachePut são as mesmas independente do provedor de cache, então o exemplo abaixo também funciona com outras soluções de cache, bastando apenas alterar o spring.cache.type no application properties.

Iniciando pelo caso mais clássico para adicionar cache, uma consulta que lista todos os registro de um domínio elegível para cache, vamos mapear com a anotação @Cacheable, definindo o nome do cache (cacheName) e chave do cache (key).

import br.com.emmanuelneri.controller.exceptions.EntityNotFoundException;
import br.com.emmanuelneri.model.Company;
import br.com.emmanuelneri.repository.CompanyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CompanyService {

    @Autowired
    private CompanyRepository companyRepository;

    @Cacheable(cacheNames = "Company", key="#root.method.name")
    public List<Company> findAll() {
        return companyRepository.findAll();
    }

 ...
}

Com base na configuração acima, na primeira chamada no método findAll será acionado o repositório, o qual faz a busca no banco de dados relacional, e partir das próximas requisições o cache será requisitado ao invés do repositório até que o cache seja inválido por uma alteração ou pelo tempo de expiração configurado.

Por baixo dos panos, após a primeira requisição será alocado um espaço em memória no Redis com o identificador “Company::findAll” para armazenar todos os registros retornados na consulta, o qual é identificado conforme a configuração cacheNames + key.

Seguindo a mesma linha, podemos ter consultas que buscam por alguma chave única, com isso podemos configurar o cache para armazenar esses registros para que não façamos as consultas em banco de dados a todo momento. Assim, a configuração é a mesma da anterior, a diferença que a chave do cache é dinâmica, ou seja, para cada identificador da Company será criado alocado uma área no Redis.

...

    @Cacheable(cacheNames = "Company", key="#identifier")
    public Company findbyIdentifier(final String identifier) {
        return companyRepository.findById(identifier)
                .orElseThrow(() -> new EntityNotFoundException("Identifier not found: " + identifier));
    }

....

Exemplo: “Company:001”, “Company:002”

Porém, nem sempre os dados que armazenamos em cache são imutáveis, por exemplo os dados de uma empresa pode mudar qualquer momento, assim fica a nosso cargo invalidar os dados em cache. Exemplo, na criação de um registro no banco de dados podemos invalidar todo cache como no exemplo abaixo.

...

    @CacheEvict(cacheNames = "Company", allEntries = true)
    public Company create(final Company company) {
        return companyRepository.save(company);
    }
...

No exemplo acima foi utilizado a propriedade allEntries = true, o que faz com que todos dados armazenados o no cache “Company” serão expirados no Redis, fazendo que com que as próximas requisições acessem o banco de dados novamente.

Também podemos otimizar nossos caches, onde em atualizações expiramos apenas o registro alterado e não toda região de cache, como no exemplo anterior, desse modo, utilizamos o @CachePut que faz expiração do cache após a atualização do registro de acordo com a chave.

...

    @CachePut(cacheNames = Company.CACHE_NAME, key="#company.getIdentifier()")
    public Company update(final Company company) {
        if(company.getIdentifier() == null) {
            throw new EntityNotFoundException("Identifier is empty");
        }

        return companyRepository.save(company);
    }
...
}

Por fim, também precisamos limpar nossos caches quando os registros vão ser removidos do banco de dados, com isso podemos usar a anotação @CacheEvict passando uma chave para remover um único registro do cache.

...

    @CacheEvict(cacheNames = Company.CACHE_NAME, key="#identifier")
    public void delete(final String identifier) {
        if(identifier == null) {
            throw new EntityNotFoundException("Identifier is empty");
        }

        companyRepository.deleteById(identifier);
    }

... 

Observação: Na entidade não é necessária nenhuma configuração porque o cache está no nível do serviço, apenas é cenário que a entidade implemente Serializable.

Conclusão

Concluindo, o Redis é uma boa solução para realizar cache distribuídos em aplicações Java, além de apresentar uma fácil integração através das dependências do spring-data e spring-data-redis utilizando a abstração de cache do Spring Boot.

O código fonte dos exemplos estão disponíveis no github.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.