Jordi Henrique Silva
Posted on February 28, 2023
No decorrer da última década vivenciamos a grande ascensão da arquitetura de microsserviços, antes os times de tecnologia optavam por desenvolver grandes sistemas responsáveis por todas as funcionalidades do negócio. E hoje a principal solução é dividir o domínio do negócio em vários escopos menores e criar pequenos sistemas independentes que os atendam. As vantagens da arquitetura de microsserviços variam desde proporcionar que diferentes times desenvolvam software em ritmos isolados, em até escalar o desempenho de determinadas funcionalidades.
Na busca pela melhora de desempenho uma das ferramentas utilizadas é o cache que permite aumentar a velocidade de consumo a dados frequentemente acessados. Uma das principais estratégias para o uso de cache é armazenar os dados na memória local do servidor a fim de evitar consultas regulares no banco de dados, APIs externas ou até mesmo processamento de cálculos custosos.
Ao tratar de microsserviços é comum que dado um requisito de disponibilidade ou desempenho seja necessário mais de uma instância dando vida a um cluster. Junto ao ambiente clusterizado vem problemas na implementação de cache local, já que cada uma das instâncias é um processo independente, e a memória não é compartilhada, ocasionando que o cache fique inconsistente entre uma instância e outra. Neste tipo de situação a estratégia de cache local não é suficiente, pois agora quando um dado é armazenado em cache por uma instância, a outra instância deverá ter acesso em sua próxima consulta. Então se faz necessário a utilização de um provedor de cache distribuído como Hazelcast e Redis.
Redis é um banco de dados não relacional de estrutura de chave (key) e valor (value). Sua principal característica é a alta velocidade de acesso devido a sua implementação em memória. E uma das suas principais utilizações é como cache distribuído, já que é possível ter diversas conexões paralelas. Ao optar por utilizar Redis como provedor de cache em uma aplicação é necessário que seja utilizada suas APIs na base de código, gerando uma grande dependência, já que se em algum momento se torne necessário a troca de provedor, haveria um alto custo em manutenção. Problemas como esse são resolvidos adotando abstrações, independente se são construídas por você ou não. Em uma aplicação Spring Boot é possível se apoiar no módulo do Spring Cache.
Spring Cache é um módulo de extensão do Spring Boot que abstrai a maneira como os dados são inseridos, atualizados e removidos do cache através da sua API no modelo de anotações. Spring Cache oferece suporte ao caching de objetos, então facilita que em diferentes pontos da aplicação compartilhem o mesmo caching.
Spring Cache com Redis
Para colher os benefícios do uso do Spring Cache com Redis é necessário que uma série de procedimentos de configuração da ferramenta, me refiro desde a inserção de dependências no pom.xml ou qualquer outro gerenciador de dependências que você utilize. Até a definição de beans e properties. Mas não se preocupe! O intuito deste artigo é demostrar como você pode configurar e utilizar o Redis como cache distribuído através das abstrações Spring Cache.
Inicialmente é necessário adicionar a Dependência de um módulo do Spring Data para o Redis, em seguida configuraremos a aplicação para trabalhar com cache e definiremos o Redis como provedor. Após as configurações de conexão e definição do Redis como provedor é necessário que definir as configurações responsáveis por limpar o cache, e qual serializer será utilizado durante a comunicação.
Configurando a Aplicação
Iniciaremos adicionando a dependência do Spring Data Redis no projeto, esta dependência ira abstrair o uso do Redis na aplicação, algumas vantagens são client e conector predefinido, serializers personalizados para o tráfego de dados na rede, etc. Abaixo tem um exemplo utilizando Maven.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
O próximo passo é definir as properties de conexão com Redis, e também definir o redis como provedor de cache no arquivo application.yaml.
spring:
redis:
host: ${HOST:localhost}
port: ${PORT:6379}
password: ${PASSWORD:password}
cache:
type: redis
cache-names: user_payment_methods
Após a definição do Redis como provedor de cache é necessário que configuramos algumas propriedades como o tempo de vida de objetos no cache, e qual será a forma como os objetos serão serializadas. Por padrão no Spring Data Redis é utilizado o JdkSerializationRedisSerializer
que utiliza a serialização nativa da JVM, e esta biblioteca nativa é conhecida por permitir a execução de código remoto, que injetam bytecode não verificado que pode causar a execução de código indesejado. Dado a isso, a recomendação é que outros serializers sejam utilizados, como, por exemplo, o serializer para Json da biblioteca Jackson.
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration defaultCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(120))// 2 horas
.disableCachingNullValues()
.serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
Iniciamos o código acima, utilizando ao nível de classe a anotação @EnableCaching
responsável por habilitar o uso do módulo de cache em aplicações Spring Boot. Em seguida é utilizado a anotação @Configuration
para definir que a classe fornecerá bens para contexto do Spring. E o bean que ela fornecerá é o responsável por definir as configurações padrões de cache. Durante o método defaultCacheConfiguration()
é definido que os objetos irão permanecer no cache em até 2 horas, e também é definido que valores nulos não serão cacheados, e por fim que os objetos sejam serializados em Json através do GenericJackson2JsonRedisSerializer
.
Cacheando Resultados
Ao finalizar as configurações estamos prontos para provisionar o uso do cache na aplicação. Então para melhor entendimento, imagine um Market Place, onde um usuário pode ter diferentes formas de pagamento por estabelecimento. E o sistema deve oferecer uma forma de consultar quais são as formas de pagamento deste usuário. Então na primeira vez em que um usuário consultar as formas, o resultado deverá ser armazenado em cache para usos futuros. Veja um exemplo de implementação no código abaixo:
@Service
public class PaymentMethodsUserService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "user_payment_methods", key = "#userId")
public List<PaymentMethod> getPaymentMethodsToUserById(UUID userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not exists"));
if (user.getPaymentMethods().isEmpty()) {
return defaultMethods();
}
return user.getPaymentMethods();
}
private List<PaymentMethod> defaultMethods() {
return List.of(
new PaymentMethod("Cash", "Money")
);
}
}
Para habilitar o cache no método getPaymentMethodsToUserById(UUID userId)
é necessário que anotação @Cacheble
seja utilizada ao nível de método e que os argumentos value e key sejam definidos. O argumento value é responsável por identificar qual cache sera consultado. Já o argumento key é responsável por definir como a chave do cache será criada. Por padrão o primeiro argumento é utilizado, porém, caso necessário é possível criar chaves mais elaboradas através de Spring Expression Language (SpEL). Para saber mais sobre como as keys são geradas consulte a documentação. Dado que um método é anotado com @Cacheable
o seguinte comportamento é incorporado, primeiro é feito uma consulta se existe a chave no cache, o valor é retornado e o código do corpo do método não é executado. Caso contrario, o código do corpo do método é executado e seu resultado no fim é armazenado em cache para consultas futuras.
Invalidando o Cache
Nosso projeto já consegue armazenar novas informações no cache, e realizar consultas, porém, também precisamos identificar a situação onde o cache deve ser invalidado, para que as informações sejam atualizadas. Imagine agora que o sistema está evoluindo e começou a aceitar novas formas de pagamento, e agora o usuário poderá adicionar as mesmas em sua coleção. Então, sempre que o usuário adicionar uma nova forma de pagamento, o seu registro de formas no cache deverá ser apagado. Veja um exemplo de implementação no código abaixo.
@Service
public class PaymentMethodsUserService {
@Autowired
private UserRepository userRepository;
@CacheEvict(value = "user_payment_methods", key = "#userId")
public void addPaymentMethods(UUID userId, PaymentMethod paymentMethod) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not exists"));
user.add(paymentMethod);
userRepository.save(user);
}
}
Se observamos no método addPaymentMethods()
foi utilizado a anotação @CacheEvict
que possui os mesmo argumentos que a anotação @Cachable
. Quando um método é anotado @CacheEvict
o mesmo se torna um gatilho para invalidação do cache, e então caso sua execução seja realizada sem interrupção de alguma exceção o cache é sera invalidado. Caso contrario o cache não sofre alterações.
Então agora é possível utilizar este cache em diversos pontos do sistema, basta apenas que as anotações e o contrato dos métodos sejam equivalentes a estes. E a aplicação poderá rodar em diversas instâncias e compartilhar o mesmo cache, mantendo a consistência dos dados.
Conclusão
A utilização de cache permite que o aumento de desempenho em um sistema, já que dados que tem alta frequência de acesso podem ser recuperados mais rapidamente, já que não é necessário um processamento custoso, ou aguardar a finalização de operações de I/O. A utilização de cache em memória local é uma estratégia interessante quando apenas uma instância do serviço será executada, pois, a partir do momento em que o ambiente se torna clusterizado a memória de cache não é compartilhada deixando os dados inconsistentes. O que justifica a utilização de um provedor de cache distribuído. Redis é um provedor de cache distribuído que facilita o compartilhamento de dados na rede, e oferece suporte para uso em diferentes linguagens.
A utilização de cache em uma aplicação Spring Boot pode ser feita através da Spring Cache que permite que o uso de cache seja abstraído por anotações. Spring Cache oferece suporte ao Redis como provedor, e através de configurações é possível rapidamente utilizá-lo como cache distribuído. A anotação @Cacheable
permite que um método faça a consulta por objetos no cache e @CacheEvict
permite que invalidar o cache. Tão importante como inserir, remover e atualizar os dados no cache é definir o tempo de duração para não ser surpreendido com problemas indesejáveis.
Referências
Posted on February 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.