Dando continuidade à série sobre otimização de back-end, chegou a hora de falar sobre cache. Enquanto o Load Balancer ajuda a distribuir requisições entre múltiplas instâncias, o Redis entra em cena para reduzir o tempo de resposta e desafogar o banco de dados relacional, servindo como memória de acesso ultrarrápido.
Redis é um banco de dados em memória com operações extremamente rápidas. Em cenários de alta concorrência, você não quer que cada requisição vá até o PostgreSQL. A sacada é: consultas repetitivas vão para o Redis, e só quando o dado não estiver lá é que se consulta o banco relacional.
+-------------------------+
| |
+----[não tem cache]-----> | PostgreSQL (em disco) |
| | |
| +-------------------------+
+-------------------+ | |
| | | |
| Spring Boot |---------+ [atualiza o cache]
| | | |
+-------------------+ | v
| +--------------------------+
| | |
+------[tem cache]------> | RedisDB (em memória) |
| |
+--------------------------+- Spring Boot pergunta pro Redis primeiro.
- Se Redis tem -> resposta rápida na memória.
- Se Redis não tem -> vai para o Postgres, pega, devolve a resposta e ainda atualiza o Redis pro próximo acesso.
services:
postgres:
image: bitnami/postgresql:17
environment:
POSTGRES_USER: ${POSTGRESQL_USERNAME}
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD}
POSTGRES_DB: ${POSTGRESQL_DATABASE}
ports:
- "5432:5432"
volumes:
- ./.database/persistent:/bitnami/postgresql/data
restart: always
networks:
- znet
redis:
volumes:
- ./.database/cache/:/data
networks:
- znet
api_server_a:
build:
context: .
dockerfile: Dockerfile
environment:
DB_URL: jdbc:postgresql://postgres:5432/${POSTGRESQL_DATABASE}
DB_USER: ${POSTGRESQL_USERNAME}
DB_PASSWORD: ${POSTGRESQL_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: ${REDIS_PORT}
REDIS_CACHE_EXPIRATION: ${REDIS_CACHE_EXPIRATION}
depends_on:
- postgres
- redis
networks:
- znet
networks:
- znetObs: este docker-compose.yml está abstraído. O restante da aplicação segue o padrão mostrado no post anterior.
O Spring já tem suporte nativo a cache, mas se usarmos apenas o sistema interno, cada instância teria o seu próprio cache isolado. Isso não escala em ambientes distribuídos. Por isso, utilizamos o Redis: um cache compartilhado entre todas as instâncias da API.
DemostrationApplication.java
package com.ffx64.demostration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // Ativa o mecanismo de cache do Spring
public class SasApplication {
public static void main(String[] args) {
SpringApplication.run(SasApplication.class, args);
}
}Para que o Spring Boot use o Redis como cache distribuído, precisamos configurar alguns parâmetros.
application.properties
# Adicione isso no seu application.properties
spring.cache.type=redis
spring.redis.host=${REDIS_HOST}
spring.redis.port=${REDIS_PORT}
spring.data.redis.repositories.enabled=false
app.cache.redis.expiration=${REDIS_CACHE_EXPIRATION}Para que o Spring Boot consiga usar o Redis como cache distribuído, precisamos configurar:
- Conexão com o Redis;
- Cache Manager com serialização adequada e tempo de expiração.
RedisConfig.java
package com.ffx64.demostration.infra.redis;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${app.cache.redis.expiration}")
private int expiration;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
return new LettuceConnectionFactory(config);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(expiration))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(serializer)
);
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
}Quando usamos o JdkSerializationRedisSerializer no Spring, todos os objetos que forem pro cache precisam ser serializáveis.
Isso significa que eles têm que poder ser transformados em bytes para o Redis guardar, e depois reconstruídos quando a aplicação pedir de volta.
No caso dos records, a boa notícia é que não precisamos de boilerplate nenhum: basta declarar implements Serializable.
PostsResponseDTO.java
package com.ffx64.demostration.dto;
import java.io.Serializable;
import java.time.OffsetDateTime;
public record PostsResponseDTO(
Long id,
String name,
String description,
String slug,
String author,
String text,
OffsetDateTime createdAt,
OffsetDateTime updatedAt,
) implements Serializable {}@Cacheable(value="posts", key="#slug")-> primeira vez busca no Postgres, salva no Redis, próximas vezes pega direto do cache;- Isso reduz latência, carga no banco e melhora a escalabilidade horizontal da aplicação.
PostsServices.java
@Cacheable(value="posts", key="#slug")
public PostsResponseDTO get(String slug) {
PostsEntity post = repository.findByGuid(guid).orElseThrow(PostsNotFoundException::new);
return toResponseDTO(post);
}Obs: Este é um post introdutório, portanto as configurações apresentadas foram simplificadas para fins de compreensão.