Usar Anotaciones de Cache de Spring con Backend Redis
Aplica las anotaciones @Cacheable, @CachePut y @CacheEvict de Spring con un cache manager Redis para caching declarativo en aplicaciones Java.
Descripcion general
Spring Framework proporciona caching declarativo a traves de anotaciones: @Cacheable, @CachePut, @CacheEvict y @Caching. Estas anotaciones interceptan llamadas a metodos, verifican el cache antes de la ejecucion, y almacenan resultados despues de la ejecucion — todo sin modificar la logica de negocio. Con un backend Redis, el cache se comparte entre todas las instancias de la aplicacion. A continuacion: configurar Spring Cache con Redis, usar cada anotacion, caching condicional, operaciones multi-cache y gestion de TTL. Ver también Configurar headers HTTP Cache-Control para APIs y assets.
Cuando Usar Esto
-
For alternatives, see Implement Redis Cache Invalidation in Node.js.
-
Aplicaciones Spring Boot que necesitan caching sin acoplar logica de negocio a codigo de cache
-
Metodos de capa de servicio con consultas de base de datos costosas o computaciones
-
Despliegues multi-instancia que necesitan un cache compartido (backend Redis)
-
Cualquier proyecto Spring donde el caching declarativo reduzca boilerplate
Prerrequisitos
- Java 17+
- Spring Boot 3+
- Servidor Redis
Solucion
1. Agregar Dependencias
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. Configurar Redis Cache Manager
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
// Configuracion de TTL por cache
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("users", config.entryTtl(Duration.ofMinutes(5)));
cacheConfigs.put("products", config.entryTtl(Duration.ofMinutes(30)));
cacheConfigs.put("config", config.entryTtl(Duration.ofHours(24)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
}
3. @Cacheable — Saltar Metodo en Cache Hit
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(String id) {
// Solo ejecutado en cache miss
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Cache con clave compuesta
@Cacheable(value = "users", key = "#email")
public User getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
// Cache condicional — solo cachear si el resultado es active
@Cacheable(value = "users", key = "#id", condition = "#result.active == true")
public User getUserByIdConditional(String id) {
return userRepository.findById(id).orElseThrow();
}
// Cache a menos que el resultado sea null
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findUserById(String id) {
return userRepository.findById(id).orElse(null);
}
}
4. @CachePut — Siempre Ejecutar, Actualizar Cache
@Service
public class UserService {
// El metodo siempre se ejecuta, el resultado reemplaza la entrada de cache
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
// Actualizar cache despues de crear
@CachePut(value = "users", key = "#result.id")
public User createUser(UserDto dto) {
User user = new User(dto.getEmail(), dto.getName());
return userRepository.save(user);
}
}
5. @CacheEvict — Remover del Cache
@Service
public class UserService {
// Evictar entrada unica
@CacheEvict(value = "users", key = "#id")
public void deleteUser(String id) {
userRepository.deleteById(id);
}
// Evictar todas las entradas del cache
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// No-op — solo limpia el cache
}
// Evictar antes de la ejecucion del metodo (ej., antes de una actualizacion masiva)
@CacheEvict(value = "users", allEntries = true, beforeInvocation = true)
public void bulkUpdateUsers(List<User> users) {
userRepository.saveAll(users);
}
// Eviction condicional
@CacheEvict(value = "users", key = "#user.id", condition = "#user.active == false")
public void deactivateUser(User user) {
user.setActive(false);
userRepository.save(user);
}
}
6. @Caching — Multiples Operaciones de Cache
@Service
public class UserService {
@Caching(
evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "users", key = "#user.email"),
@CacheEvict(value = "userList", allEntries = true),
}
)
public User updateUserAndEvictCaches(String id, UserDto dto) {
User user = userRepository.findById(id).orElseThrow();
user.setEmail(dto.getEmail());
user.setName(dto.getName());
return userRepository.save(user);
}
}
7. Key Generator Personalizado
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param.hashCode()).append(":");
}
return sb.toString();
};
}
}
Uso:
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUser(String id, String tenantId) {
return userRepository.findByIdAndTenant(id, tenantId);
}
8. Multi-Cache con @CacheConfig
@Service
@CacheConfig(cacheNames = {"users", "userList"}) // Nombres de cache por defecto para la clase
public class UserService {
@Cacheable(key = "#id")
public User getUserById(String id) {
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(allEntries = true)
public void clearAll() {}
}
Como Funciona
- Proxy AOP: Spring envuelve beans
@Cacheableen un proxy. Cuando se llama a un metodo, el proxy intercepta, verifica el cache, y retorna el valor cacheado o ejecuta el metodo y almacena el resultado. @Cacheable: Antes de la ejecucion del metodo, el proxy verifica el cache para la clave. En hit, el valor cacheado se retorna y el metodo se salta. En miss, el metodo se ejecuta y el resultado se cachea.@CachePut: El metodo siempre se ejecuta. Despues de la ejecucion, el resultado se coloca en el cache, sobrescribiendo cualquier entrada existente. Usar para updates donde quieres datos frescos tanto en DB como en cache.@CacheEvict: Remueve entradas del cache.allEntries = truelimpia todo el cache.beforeInvocation = trueevicta antes de que el metodo se ejecute (util para seguridad de rollback).- Resolucion de clave: Por defecto, Spring usa todos los parametros del metodo como clave. Usa
key = "#id"para especificar un solo parametro, okeyGeneratorpara logica personalizada.
Variantes
Cache Multi-Nivel Caffeine + Redis
@Configuration
public class CacheConfig {
@Bean
@Primary
public CompositeCacheManager cacheManager(
RedisCacheManager redisManager,
CaffeineCacheManager caffeineManager) {
CompositeCacheManager manager = new CompositeCacheManager(
caffeineManager, // L1 — verificado primero
redisManager // L2 — verificado segundo
);
manager.setFallbackToNoOpCache(false);
return manager;
}
}
Cache con TTL por Entrada (Redis TTL)
@Cacheable(value = "users", key = "#id")
@CacheEvict(value = "users", key = "#id", condition = "#result.updatedAt > T(java.time.Instant).now().minusSeconds(300)")
public User getUser(String id) {
return userRepository.findById(id).orElseThrow();
}
Cache Async (Spring 6+)
@Cacheable(value = "users", key = "#id")
public CompletableFuture<User> getUserAsync(String id) {
return CompletableFuture.supplyAsync(() ->
userRepository.findById(id).orElseThrow()
);
}
Acceso Programatico al Cache
@Service
public class UserService {
@Autowired
private CacheManager cacheManager;
public void manualCacheOperation(String userId) {
Cache cache = cacheManager.getCache("users");
if (cache != null) {
// Put manual
cache.put(userId, new User(userId, "Alice"));
// Get manual
User cached = cache.get(userId, User.class);
// Evict manual
cache.evict(userId);
}
}
}
Mejores Practicas
- Usar
@Cacheablepara lecturas,@CachePutpara writes,@CacheEvictpara deletes: Cada anotacion tiene un proposito especifico. No uses@Cacheableen metodos que mutan. - Establecer TTLs por cache: Diferentes datos tienen diferentes requerimientos de frescura. Configura TTLs en el
RedisCacheManager, no globalmente. - Usar
unlesspara saltar caching de nulls o resultados no deseados:unless = "#result == null"previene cachear valores null que enmascararian misses reales. - Evictar en writes: Despues de
updateodelete, evicta la entrada de cache. De lo contrario, datos stale se sirven hasta que el TTL expira. - Usar
allEntries = truecon moderacion: Limpiar todo el cache causa un pico en cache misses. Evicta claves especificas cuando sea posible. - Deshabilitar caching de
null: UsadisableCachingNullValues()para prevenir cachear resultadosnull, que pueden enmascarar errores de base de datos.
Errores Comunes
- Usar
@Cacheableen metodos de update:@Cacheablesalta el metodo en cache hit — los updates nunca se ejecutan. Usa@CachePutpara writes. - Falta
@EnableCaching: Sin esta anotacion en una clase@Configuration, las anotaciones de cache son no-ops. - Colisiones de claves entre metodos: Dos metodos con
@Cacheable("users")y diferentes parametros pueden colisionar si las claves se superponen. Usa expresiones de clave distintas. - Cachear objetos mutables: Si el objeto cacheado se modifica despues de cachear, el cache mantiene una referencia al objeto mutado. Usa DTOs inmutables o deep copies.
- No manejar fallos de cache: Si Redis cae, las operaciones de cache lanzan excepciones. Configura un fallback o usa
errorHandlerpara loguear y continuar.
Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Configurar Caffeine Cache en Java con Politicas de Eviction
Configura Caffeine cache en una aplicacion Java con politicas de eviction por tamano, tiempo y peso para caching local de alto rendimiento.
RecipeImplementar Invalidation de Cache Redis en Node.js
Invalida entradas de cache Redis en Node.js con expiracion TTL, eliminacion explicita, limpieza por patron e invalidacion distribuida con pub/sub.
GuideEstrategias de Versionado de APIs
Versiona APIs REST y GraphQL con estrategias de URI, header, query param y content negotiation. Cubre deprecación, sunset y patrones de migración.
RecipeCachear Respuestas HTTP con httpx y CacheControl en Python
Cachear respuestas HTTP en Python usando httpx con CacheControl para caching compatible con HTTP, manejo de ETag y peticiones condicionales.