Volver a Spring Boot Intermedio
Spring Boot
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.