Tipos de caché con Redis: estrategias para elegir la correcta
Remote Dictionary Server. Base de datos en memoria, de código abierto, que soporta estructuras como strings, hashes, listas y sets. Se usa ampliamente como capa de caché, broker de mensajes y almacén de sesiones.
Cache Reactivo (Cache-Aside / Lazy Loading)
Cache-Aside: flujo de lectura
# 1. Consultar Redis
GET user:123
# (nil) — cache miss
# 2. Consultar la base de datos y guardar en Redis con TTL de 1 hora
SET user:123 '{"id":123,"name":"Ana","email":"ana@example.com"}' EX 3600
# 3. En el siguiente request, el dato ya está en caché
GET user:123
# '{"id":123,"name":"Ana","email":"ana@example.com"}' — cache hit// Repositorio de Redis que encapsula las operaciones de caché
@Repository
class UserCacheRepository(
private val redisTemplate: RedisTemplate<String, String>
) {
private val objectMapper = ObjectMapper()
fun findById(userId: Long): User? {
val data = redisTemplate.opsForValue().get("user:$userId") ?: return null
return objectMapper.readValue(data, User::class.java)
}
fun save(user: User, ttl: Duration = Duration.ofHours(1)) {
redisTemplate.opsForValue().set(
"user:${user.id}",
objectMapper.writeValueAsString(user),
ttl
)
}
}
// El servicio usa el repositorio de caché, no RedisTemplate directamente
@Service
class UserService(
private val userCacheRepository: UserCacheRepository,
private val userRepository: UserRepository
) {
fun getUser(userId: Long): User? {
// Intentar desde caché
userCacheRepository.findById(userId)?.let { return it }
// Cache miss: consultar la base de datos
val user = userRepository.findById(userId).orElse(null)
if (user != null) {
userCacheRepository.save(user)
}
return user
}
}El trade-off del cold start
Cache en Caliente (Hot Cache / Warm Cache)
Hot Cache: precarga antes del tráfico
# Precalentar el caché con el catálogo de productos antes de una venta flash
HSET product:101 name "Mechanical keyboard" price "89.99" stock "250"
HSET product:102 name "27-inch monitor" price "349.99" stock "80"
HSET product:103 name "Ergonomic chair" price "199.99" stock "120"
# Verificar que están cargados
HGETALL product:101// Repositorio de Redis que encapsula las operaciones de caché
@Repository
class ProductCacheRepository(
private val redisTemplate: RedisTemplate<String, String>
) {
fun saveAll(products: List<Product>, ttl: Duration = Duration.ofHours(2)) {
redisTemplate.executePipelined { connection ->
for (product in products) {
val key = "product:${product.id}"
val map = mapOf(
"name" to product.name,
"price" to product.price.toString(),
"stock" to product.stock.toString()
)
connection.hashCommands().hSet(
key.toByteArray(),
map.mapKeys { it.key.toByteArray() }.mapValues { it.value.toByteArray() }
)
connection.keyCommands().expire(key.toByteArray(), ttl.seconds)
}
null
}
}
}
// El servicio usa el repositorio de caché, no RedisTemplate directamente
@Service
class CatalogWarmingService(
private val productCacheRepository: ProductCacheRepository,
private val productRepository: ProductRepository
) {
fun warmCatalog(productIds: List<Long>) {
// Se ejecuta minutos antes de que abra la venta flash
val products = productIds.mapNotNull { productRepository.findById(it).orElse(null) }
productCacheRepository.saveAll(products)
println("Cache warmed: ${products.size} products loaded.")
}
}Pipelines de Redis
Cache Proactivo (Prefetch Cache)
Cache Proactivo: precarga predictiva
// Repositorio de Redis que encapsula las operaciones de caché
@Repository
class PageCacheRepository(
private val redisTemplate: RedisTemplate<String, String>
) {
private val objectMapper = ObjectMapper()
fun find(page: Int): List<Article>? {
val data = redisTemplate.opsForValue().get("search:results:page:$page") ?: return null
return objectMapper.readValue(data, object : TypeReference<List<Article>>() {})
}
fun save(page: Int, articles: List<Article>, ttl: Duration = Duration.ofSeconds(300)) {
redisTemplate.opsForValue().set(
"search:results:page:$page",
objectMapper.writeValueAsString(articles),
ttl
)
}
fun exists(page: Int): Boolean {
return redisTemplate.hasKey("search:results:page:$page") == true
}
}
// El servicio usa el repositorio de caché, no RedisTemplate directamente
@Service
class PaginationService(
private val pageCacheRepository: PageCacheRepository,
private val articleRepository: ArticleRepository
) {
fun getPage(page: Int, size: Int = 20): List<Article> {
pageCacheRepository.find(page)?.let {
// Precargar la siguiente página de forma asíncrona
prefetchNextPage(page + 1, size)
return it
}
val offset = (page - 1) * size
val data = articleRepository.findAllOrderByDateDesc(size, offset)
pageCacheRepository.save(page, data)
return data
}
@Async
fun prefetchNextPage(page: Int, size: Int) {
if (!pageCacheRepository.exists(page)) {
val offset = (page - 1) * size
val data = articleRepository.findAllOrderByDateDesc(size, offset)
pageCacheRepository.save(page, data)
}
}
}No todo vale la pena precargarlo
Write-Through Cache
Write-Through: escritura sincrónica
# Actualizar el stock de un producto
# Escrito en Redis y en la DB en la misma transacción de negocio
SET product:101:stock 249
# Simultáneamente → UPDATE products SET stock = 249 WHERE id = 101// Repositorio de Redis que encapsula las operaciones de caché
@Repository
class StockCacheRepository(
private val redisTemplate: RedisTemplate<String, String>
) {
fun update(productId: Long, stock: Int, ttl: Duration = Duration.ofHours(1)) {
redisTemplate.opsForValue().set(
"product:$productId:stock",
stock.toString(),
ttl
)
}
}
// El servicio usa el repositorio de caché, no RedisTemplate directamente
@Service
class StockService(
private val stockCacheRepository: StockCacheRepository,
private val productRepository: ProductRepository
) {
@Transactional
fun updateStock(productId: Long, newStock: Int) {
// Write-through: primero la DB, luego el caché
productRepository.updateStock(productId, newStock)
// Si la transacción falla, nunca llegamos a esta línea
stockCacheRepository.update(productId, newStock)
}
}Write-Behind Cache (Write-Back)
Write-Behind: escritura asíncrona
# Incrementar el contador de likes de un artículo
# Solo se escribe en Redis; la DB se actualiza en batch cada 30 segundos
INCR article:456:likes
# Redis responde en < 1 ms — la DB todavía no sabe nada// Repositorio de Redis que encapsula las operaciones de caché
@Repository
class LikeCacheRepository(
private val redisTemplate: RedisTemplate<String, String>
) {
fun increment(articleId: Long): Long {
return redisTemplate.opsForValue().increment("article:$articleId:likes") ?: 0
}
fun markPending(articleId: Long) {
redisTemplate.opsForSet().add("likes:pending", articleId.toString())
}
fun getPendingIds(): Set<String> {
return redisTemplate.opsForSet().members("likes:pending") ?: emptySet()
}
fun getCount(articleId: String): Long? {
return redisTemplate.opsForValue().get("article:$articleId:likes")?.toLongOrNull()
}
fun clearPending() {
redisTemplate.delete("likes:pending")
}
}
// El servicio usa el repositorio de caché, no RedisTemplate directamente
@Service
class LikeService(
private val likeCacheRepository: LikeCacheRepository,
private val articleRepository: ArticleRepository
) {
// Escritura: solo Redis
fun recordLike(articleId: Long): Long {
val newTotal = likeCacheRepository.increment(articleId)
likeCacheRepository.markPending(articleId)
return newTotal
}
// Flush periódico (se ejecuta cada 30 segundos)
@Scheduled(fixedRate = 30_000)
@Transactional
fun flushLikesToDb() {
val pendingIds = likeCacheRepository.getPendingIds()
if (pendingIds.isEmpty()) return
for (articleId in pendingIds) {
val total = likeCacheRepository.getCount(articleId) ?: continue
articleRepository.updateLikes(articleId.toLong(), total)
}
likeCacheRepository.clearPending()
println("Flush complete: ${pendingIds.size} articles updated.")
}
}El riesgo real del write-behind
Comparación rápida
| Estrategia | Cuándo cargar | Consistencia | Caso de uso ideal |
|---|---|---|---|
| Cache Reactivo | Al primer miss | Eventual (TTL) | Datos con acceso impredecible |
| Cache en Caliente | Antes del tráfico | Alta (precarga manual) | Ventas flash, eventos masivos |
| Cache Proactivo | Antes del request, por predicción | Eventual | Paginación, recomendaciones |
| Write-Through | En cada escritura | Fuerte | Inventarios, datos financieros |
| Write-Behind | Redis ahora, DB después | Eventual (riesgo de pérdida) | Contadores, métricas de alta frecuencia |