Volver a Spring Boot Intermedio

Estrategias de Caché en Spring Boot

El caché reduce la carga en la base de datos y mejora los tiempos de respuesta almacenando datos accedidos frecuentemente en almacenamiento rápido.

Abstracción de Caché de Spring

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
@SpringBootApplication @EnableCaching public class Aplicacion { ... }

Anotaciones Básicas de Caché

@Service public class ServicioProductos { // Cachear resultado — la clave es por defecto los parámetros del método @Cacheable("productos") public Producto buscarPorId(Long id) { return productoRepositorio.findById(id).orElseThrow(); } // Clave personalizada con SpEL @Cacheable(value = "productos", key = "#id", condition = "#id > 0") public Producto buscarPorIdCondicional(Long id) { return productoRepositorio.findById(id).orElseThrow(); } // Actualizar caché cuando cambia la entidad @CachePut(value = "productos", key = "#result.id") public Producto guardar(Producto producto) { return productoRepositorio.save(producto); } // Evictar entrada específica @CacheEvict(value = "productos", key = "#id") public void eliminar(Long id) { productoRepositorio.deleteById(id); } // Evictar todas las entradas del caché @CacheEvict(value = "productos", allEntries = true) public void evictarTodo() {} // Combinar múltiples operaciones de caché @Caching( put = @CachePut(value = "productos", key = "#result.id"), evict = @CacheEvict(value = "lista-productos", allEntries = true) ) public Producto actualizar(Long id, Producto producto) { producto.setId(id); return productoRepositorio.save(producto); } }

Redis como Backend de Caché

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
spring: redis: host: localhost port: 6379 password: secreto cache: type: redis redis: time-to-live: 3600000 # 1 hora en ms cache-null-values: false
@Configuration public class ConfiguracionRedis { @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) .serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer( new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); Map<String, RedisCacheConfiguration> configs = Map.of( "productos", config.entryTtl(Duration.ofMinutes(30)), "categorias", config.entryTtl(Duration.ofHours(24)), "sesiones", config.entryTtl(Duration.ofMinutes(15)) ); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(configs) .build(); } }

Operaciones Directas con Redis

@Service public class ServicioCacheSesion { private final RedisTemplate<String, Object> redisTemplate; private static final String PREFIJO_SESION = "sesion:"; public void guardarSesion(String sesionId, SesionUsuario sesion, Duration ttl) { String clave = PREFIJO_SESION + sesionId; redisTemplate.opsForValue().set(clave, sesion, ttl); } public Optional<SesionUsuario> obtenerSesion(String sesionId) { SesionUsuario sesion = (SesionUsuario) redisTemplate.opsForValue() .get(PREFIJO_SESION + sesionId); return Optional.ofNullable(sesion); } public void eliminarSesion(String sesionId) { redisTemplate.delete(PREFIJO_SESION + sesionId); } // Limitación de tasa con incremento atómico public boolean estaLimitado(String usuarioId, int maxPeticiones, Duration ventana) { String clave = "ratelimit:" + usuarioId; Long contador = redisTemplate.opsForValue().increment(clave); if (contador == 1) { redisTemplate.expire(clave, ventana); } return contador > maxPeticiones; } }

Patrón Cache Aside

@Service public class ServicioCatalogo { private final ProductoRepositorio productoRepositorio; private final RedisTemplate<String, List<Producto>> redisTemplate; private static final String CLAVE_CATALOGO = "catalogo:todos"; public List<Producto> obtenerCatalogo() { // 1. Primero intentar caché List<Producto> enCache = redisTemplate.opsForValue().get(CLAVE_CATALOGO); if (enCache != null) return enCache; // 2. No está en caché — cargar desde BD List<Producto> productos = productoRepositorio.findAll(); // 3. Guardar en caché redisTemplate.opsForValue().set(CLAVE_CATALOGO, productos, Duration.ofHours(1)); return productos; } public void invalidarCatalogo() { redisTemplate.delete(CLAVE_CATALOGO); } }

Caffeine (Caché En Memoria)

Para aplicaciones de instancia única o como caché L1:

<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
@Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager("productos", "categorias"); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(15)) .recordStats()); return manager; }

Métricas de Caché

management: endpoints: web: exposure: include: health,info,metrics,caches

Accede a /actuator/caches para ver todas las estadísticas del caché en tiempo real.