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.