Turbinando seus Microsserviços com Spring Cache e Redis

jordihofc

Jordi Henrique Silva

Posted on February 28, 2023

Turbinando seus Microsserviços com Spring Cache e Redis

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()));
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Spring Data Redis
  2. Spring Cache 3.2, aqui fornece mais detalhes que a versão atual
  3. Spring Cache atual
  4. Customizer TTL Redis Cache
  5. Spring Cache With Redis: Baeldung
  6. Principais Aprendizados Spring Cache Redis
💖 💪 🙅 🚩
jordihofc
Jordi Henrique Silva

Posted on February 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related