/redis-spring-tutorial

Tutorial de como integrar Redis com aplicação Spring Boot

Primary LanguageJava

Redis com Spring Boot

Requisitos

O que é Redis?

Redis é um armazenamento de estrutura de dados que pode ser utilizado como banco de dados, cache, broker de mensageria e motor de streaming.

Redis provê escrita e leitura atômica de estruturas de dados como listas, conjuntos, maps, strings e outras. Além disso, possui replicação nativa, execução de script em Lua, políticas de limpeza de cache, transações, persistência e alta disponibilidade com Redis Sentinel e particionamento automático com Redis Cluster.

Redis na prática

Para praticar o Redis, vamos criar uma aplicação Spring Boot, utilizando Spring Initializr, conforme a imagem abaixo: Spring Initializr

Criando as classes do projeto

Para esse tutorial, iremos exemplificar com um método de listagem de notícias para uma página principal em que o conteúdo é estático e deve ser reconstruído a cada 2 minutos. Como o conteúdo é estático, não precisamos obter as mesmas notícias para casa usuário. Se fizéssemos isso, poderíamos onerar o banco de dados desnecessariamente, uma vez que podemos salvar esses dados em cache.

Nossa estrutura de classes ficará:

├── controller
│   └── NewsController.java
├── entities
│   └── News.java
├── repository
│   └── NewsRepository.java
├── RedisTutorialApplication.java
  • A classe News é uma classe modelo de como uma notícia é estruturada.
  • A classe NewsRepository é a classe que faz a obtenção dos dados das notícias na fonte lenta.
  • A classe NewsController ficará responsável por expor os dados por HTTP.
  • A classe RedisTutorialApplication é a classe principal que executará o framework.

News

@Getter
@Builder
@Jacksonized
public class News {
    private final String title;
    private final String content;
    private final String author;
}

NewsController

@RestController
@RequestMapping("/news")
@RequiredArgsConstructor
public class NewsController {

    private final NewsRepository newsRepository;

    @GetMapping
    public List<News> findAll() {
        return newsRepository.findAll();
    }

}

NewsRepository

Para simular uma consulta lenta, teremos um Thread.sleep() de 4 segundos no método findAll().

Adicionaremos um banco de dados fake apenas para simular a obtenção de dados, com a biblioteca Faker.

@Component
public class NewsRepository {
private final Faker FAKER = new Faker();
private final List<News> DATABASE = new ArrayList<>();
private static final int NEWS_DB_SIZE = 100;

    @PostConstruct
    public void setup() {
        for (int index = 0; index < NEWS_DB_SIZE; index++) {
            DATABASE.add(News.builder()
                    .author(FAKER.name().fullName())
                    .title(FAKER.lorem().characters(10, 20))
                    .content(FAKER.lorem().characters(1000, 10_000))
                    .build());
        }
    }

    public List<News> findAll() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return DATABASE;
    }
}

Como integrar o Redis com Spring

No arquivo pom.xml, é possível integrar com o Redis através de bibliotecas criadas para o ecossistema Spring Boot:

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

Com essa biblioteca, poderemos conectar com a instância Redis. Para tanto, precisamos iniciar a instância e configurar os parâmetros de acesso, serialização e cache eviction.

Iniciar instância Redis local

Para iniciar o Redis na máquina local, utilizaremos Docker.

Faça download da imagem do Redis:

$ docker pull redis:7.0.0

Inicie um container:

$ docker run -d -p 6379:6379 --name redis-tutorial-local redis:7.0.0

Podemos testar a conectividade através do comando telnet

$ telnet localhost 6379

Parâmetros de acesso

Para definir o hostname e porta, adicionaremos as seguintes linhas no arquivo application.properties:

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

Configuração na aplicação

Para configurar a serialização e limpeza de cache, criaremos um pacote config e a classe RedisConfiguration.java:

@Slf4j
@Configuration
@EnableCaching
@EnableScheduling
public class RedisConfiguration {
    public static final String NEWS_KEY = "news";

    @CacheEvict(allEntries = true, value = NEWS_KEY)
    @Scheduled(fixedDelayString = "${cache.ttl.ms}")
    public void evictCache() {
        log.info("Cache cleared");
    }

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

Note que a classe contém um Bean de configuração para desabilitar cache de valores nulos e para serializar objetos Java em JSON utilizando uma classe utilitária.

Além disso, é utilizado a anotação @EnableCaching na declaração da classe para habilitar o uso de cache na aplicação Spring.

Com o uso da anotação @EnableScheduling, é possível limpar o cache num intervalo de tempo definido no application.properties:

cache.ttl.ms=120000

Com essa configuração, o cache será limpo a cada 2 minutos.

Ativar cache em método

Como visto anteriormente, na classe NewsRepository, simulamos uma lentidão de busca no banco de dados com Thread.sleep(4000) que faz com que o endpoint responda no mínimo com 4 segundos de tempo de resposta.

public class NewsRepository {
    private final List<News> DATABASE = ArrayList<>;
    
    public List<News> findAll() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException ignored) {
        }
        
        return DATABASE;
    }
}

Se testarmos uma chamada na API que busca esses dados, temos o seguinte resultado:

$ curl -o /dev/null -s -w 'Total: %{time_total}s\n' localhost:8080/news
Total: 4.136567s

Se adicionarmos a anotação @Cacheable(value = NEWS_KEY) na assinatura do método

@Cacheable(value = NEWS_KEY)
public List<News> findAll() { ... }

Temos o seguinte resultado:

$ curl -o /dev/null -s -w 'Total: %{time_total}s\n' localhost:8080/news
Total: 4.159841s

$ curl -o /dev/null -s -w 'Total: %{time_total}s\n' localhost:8080/news
Total: 0.110986s

$ curl -o /dev/null -s -w 'Total: %{time_total}s\n' localhost:8080/news
Total: 0.093111s

Perceba que na primeira execução, não existia o valor em memória cache, então a aplicação precisou obter o valor da fonte original, que em nosso caso é lenta. Nas próximas execuções, já obteve o valor da memória Redis, reduzindo drasticamente o tempo de resposta. Conseguimos reduzir de 4150 ms para 110ms, redução de aproximadamente 38 vezes do tempo de resposta.

O que de fato está acontecendo, é que a aplicação está validando se o dado existe no Redis, e se existir, retorna imediatamente. Caso não exista, ele obterá da fonte original lenta e salvará no Redis para processamentos futuros.

Rodando o tutorial

Rode a aplicação e a instância Redis com o comando e teste em sua máquina o que foi comentado nesse tutorial:

$ docker-compose up -d

Acesse pela rota http://localhost:8080/news