Poner Redis delante de tu base de datos es fácil. Que esa caché aguante cuando Redis se cae, cuando la carga se dispara o cuando los TTL expiran todos a la vez, ya es otra historia. Este artículo va de eso: no del cache miss básico, sino de lo que ocurre cuando las cosas se tuercen.
Spring Cache: la capa de abstracción
Spring Cache te permite cachear el resultado de un método sin escribir una línea de lógica de caché. Solo necesitas activarlo con @EnableCaching en tu clase de configuración y anotar los métodos.
@Configuration
@EnableCaching
public class CacheConfig { }
Las tres anotaciones principales:
@Cacheable: si el valor está en caché, lo devuelve directamente. Si no, ejecuta el método y guarda el resultado.@CachePut: siempre ejecuta el método y actualiza la caché con el resultado. Útil para operaciones de escritura.@CacheEvict: invalida una entrada o toda una caché. Llámalo tras un delete o cuando los datos queden obsoletos.
@Cacheable(value = "productos", key = "#id")
public ProductoDTO findById(Long id) {
return productoRepository.findById(id)
.map(this::toDTO)
.orElseThrow(() -> new ProductoNotFoundException(id));
}
@CachePut(value = "productos", key = "#result.id")
public ProductoDTO actualizar(Long id, ProductoRequest request) {
// ... lógica de actualización
}
@CacheEvict(value = "productos", key = "#id")
public void eliminar(Long id) {
productoRepository.deleteById(id);
}
TTL con RedisCacheManager
Por defecto Spring no pone TTL a las entradas en Redis. Sin TTL, los datos viven indefinidamente y puedes quedarte con información obsoleta para siempre. Configúralo así:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("productos", config.entryTtl(Duration.ofMinutes(5)))
.build();
}
Puedes asignar TTL distintos por caché. Los productos que cambian con frecuencia merecen un TTL corto; los datos más estáticos pueden aguantar más tiempo.
El problema del cache stampede
Imagina que tienes 10.000 usuarios activos y la caché de la página de inicio expira. En ese mismo instante, todas las peticiones ven un cache miss y van directas a la base de datos. La BD recibe una carga diez veces superior a la habitual. Esto se llama cache stampede o thundering herd, y puede tumbar tu aplicación.
Hay dos soluciones principales.
Probabilistic early expiration. En lugar de esperar a que el TTL expire, cada petición tiene una pequeña probabilidad de refrescar la caché antes de que caduque. Cuanto más cerca está el vencimiento, mayor es la probabilidad. Así el refresco se distribuye en el tiempo y nunca hay un golpe masivo a la BD.
Request coalescing (singleflight pattern). Cuando se produce un cache miss, solo dejas pasar una petición a la BD. El resto esperan a que esa primera petición complete y rellene la caché. En Java puedes implementarlo con CompletableFuture y un ConcurrentHashMap que actúe como registro de peticiones en vuelo.
private final ConcurrentHashMap<Long, CompletableFuture<ProductoDTO>> inflight = new ConcurrentHashMap<>();
public CompletableFuture<ProductoDTO> findByIdCoalesced(Long id) {
return inflight.computeIfAbsent(id, key ->
CompletableFuture.supplyAsync(() -> {
try {
return cargarDesdeBD(key);
} finally {
inflight.remove(key);
}
})
);
}
Cache-aside vs read-through
Son dos patrones distintos de interacción con la caché, y conviene no confundirlos.
En cache-aside (el patrón que usa Spring Cache por defecto), tu aplicación gestiona la caché. Primero mira en Redis; si no hay nada, va a la BD y guarda el resultado en Redis. Es el patrón que mencionamos al hablar de los patrones de diseño en Java, incluido el patrón Cache-Aside.
En read-through, es la propia caché quien va a buscar el dato a la BD cuando no lo tiene. Tu aplicación solo habla con Redis, nunca directamente con la BD. Redisson implementa esto con RReadThroughMap.
Cache-aside es más flexible y el más habitual en proyectos Spring Boot. Read-through simplifica el código de la aplicación pero requiere que la caché soporte este modo (no todos los proveedores lo hacen).
Resiliencia: qué pasa cuando Redis cae
Este es el punto que más se suele ignorar en los tutoriales básicos. Si Redis se cae y no tienes un plan, tu aplicación puede fallar por completo aunque la base de datos siga funcionando perfectamente.
Spring Cache lanza excepción cuando no puede conectar con Redis. Tienes dos opciones para manejarlo.
Opción 1: capturar errores de caché. Implementa CacheErrorHandler para que los fallos de Redis sean silencios y la petición caiga directamente a la BD:
@Configuration
public class CacheErrorConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.warn("Error al leer caché {}: {}", cache.getName(), e.getMessage());
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.warn("Error al escribir caché {}: {}", cache.getName(), e.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { }
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) { }
};
}
}
Opción 2: circuit breaker. Con Resilience4j puedes abrir el circuito cuando Redis falla demasiadas veces seguidas y redirigir todas las peticiones a la BD durante un periodo de recuperación, sin que cada llamada tenga que esperar el timeout de conexión.
Spring Data Redis: RedisTemplate y serialización
Cuando necesitas más control que el que ofrece @Cacheable, usas RedisTemplate directamente:
@Autowired
private RedisTemplate<String, ProductoDTO> redisTemplate;
public void guardar(ProductoDTO producto) {
redisTemplate.opsForValue().set(
"producto:" + producto.getId(),
producto,
Duration.ofMinutes(10)
);
}
public Optional<ProductoDTO> recuperar(Long id) {
ProductoDTO valor = redisTemplate.opsForValue().get("producto:" + id);
return Optional.ofNullable(valor);
}
La serialización por defecto de Spring Data Redis usa Java serialization, que es lenta y produce claves ilegibles. Configura Jackson para serializar a JSON:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
Redisson para casos avanzados
Redisson es un cliente Redis para Java que añade abstracciones de alto nivel: locks distribuidos, colas, semáforos y caché con read-through. Es útil cuando necesitas coordinar múltiples instancias de tu aplicación:
RLock lock = redissonClient.getLock("lock:producto:" + id);
lock.lock(10, TimeUnit.SECONDS);
try {
// sección crítica: solo un nodo entra aquí a la vez
} finally {
lock.unlock();
}
Configuración en application.yml
spring:
data:
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 1000ms
El timeout de 2 segundos es clave: sin él, un Redis que no responde puede bloquear tus hilos durante el tiempo de timeout de TCP (que puede ser de minutos).
Métricas con Micrometer
Saber si tu caché funciona bien requiere métricas. Con Micrometer y Spring Boot Actuator puedes ver la tasa de aciertos (hit rate), la tasa de fallos y la latencia de Redis sin escribir código adicional. Solo necesitas añadir la dependencia del registry correspondiente (Prometheus, Datadog...) y habilitar el endpoint de métricas.
Las métricas clave a monitorizar son cache.gets con el tag result=hit/miss y spring.data.redis.command.latency. Una tasa de aciertos por debajo del 80% en una caché de productos suele indicar un TTL demasiado corto o claves de caché mal diseñadas.
Una caché sin métricas es una caja negra. No sabes si ayuda o si simplemente añade complejidad sin beneficio real. Instrumenta desde el primer día.
Imagen: Pexels / panumas nikhomkhai
