El dolor: un endpoint que nos estaba ahogando
Teníamos un problema. Un endpoint crítico para un dashboard de cliente tardaba entre 2 y 3 segundos en responder. Cada vez que el usuario refrescaba, la base de datos sufría con una consulta compleja llena de joins y agregaciones. Era insostenible y la experiencia de usuario era pésima. La primera tentación fue optimizar la query, pero ya estaba bastante pulida. El problema no era la query en sí, sino la frecuencia con la que se ejecutaba sobre un dataset que no cambiaba tan a menudo.
La estrategia: Cache-Aside, directo y efectivo
La solución era clara: necesitábamos un cache. En lugar de reinventar la rueda, me fui directo por el patrón Cache-Aside. La lógica es simple: la aplicación primero busca el dato en el cache (en mi caso, Redis). Si lo encuentra (cache hit), lo devuelve de inmediato. Si no (cache miss), va a la base de datos, obtiene el dato, lo guarda en el cache para la próxima vez y lo devuelve. Simple, elegante y brutalmente efectivo.
Paso 1: Configurando Redis en Quarkus
Lo primero es conectar Quarkus con nuestro servidor Redis. Es ridículamente fácil gracias al sistema de configuración de Quarkus. Solo añadí esto a mi application.properties:
# application.properties
# Redis Host
quarkus.redis.hosts = redis://127.0.0.1:6379
# Opcional: si tienes varias instancias de Redis
# quarkus.redis.client-name = mi-api-cache
# Timeout para las conexiones
quarkus.redis.timeout = 2000ms
Paso 2: Inyectando el cliente de Redis en el servicio
Quarkus, con su inyección de dependencias (CDI), hace que usar el cliente de Redis sea un paseo. En el servicio que contenía la lógica de negocio pesada, inyecté el RedisDataSource.
import io.quarkus.redis.client.RedisClient;
import io.quarkus.redis.client.reactive.ReactiveRedisClient;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ApplicationScoped
public class DashboardService {
@Inject
RedisClient redisClient;
// ... resto del servicio
}
Paso 3: Implementando la lógica Cache-Aside
Aquí está el núcleo de la solución. Modifiqué el método original para que primero consulte Redis. Usé una clave de cache predecible, como dashboard:data:userId:123.
public JsonObject getDashboardData(String userId) {
String cacheKey = "dashboard:data:userId:" + userId;
// 1. Intenta obtener los datos del cache
Response cachedData = redisClient.get(cacheKey);
if (cachedData != null) {
// Cache HIT: ¡Éxito! Lo encontramos en el cache
return new JsonObject(cachedData.toString());
}
// 2. Cache MISS: No está en el cache, vamos a la base de datos
JsonObject freshData = fetchDataFromDatabase(userId);
// 3. Guardamos los datos frescos en Redis con un TTL (Time To Live)
// En este caso, el cache expira en 15 minutos (900 segundos)
redisClient.set(Arrays.asList(cacheKey, freshData.toString(), "EX", "900"));
return freshData;
}
private JsonObject fetchDataFromDatabase(String userId) {
// Aquí va la lógica original que consulta la base de datos...
// ...simulamos una operación lenta.
System.out.println("Accediendo a la base de datos... operación costosa.");
return new JsonObject().put("user", userId).put("value", Math.random() * 1000);
}
Lecciones aprendidas en la trinchera
El resultado fue inmediato: pasamos de 2-3 segundos a un promedio de 50 milisegundos por request. Un mundo de diferencia.
- La clave es la clave: Definir una buena estrategia para nombrar las claves de cache es fundamental para evitar colisiones y facilitar la depuración.
- El TTL es tu mejor amigo: No caches datos para siempre. Establecer un TTL (Time To Live) adecuado es crucial. Para nosotros, 15 minutos era el balance perfecto entre frescura de datos y rendimiento.
- Monitorea tu Hit Rate: Es vital saber qué tan efectivo es tu cache. Herramientas como Grafana y Prometheus te ayudan a monitorear el ratio de cache hits vs. misses. Si tu hit rate es bajo, quizás tu estrategia de cache no es la correcta.
- Cache Invalidation es el verdadero reto: En este caso, un TTL era suficiente. Pero en sistemas más complejos, invalidar el cache activamente cuando los datos subyacentes cambian es el verdadero desafío arquitectónico. Pero eso, es tema para otro post.