Tema representativo de entrevista

Entrevista de Backend: ¿Cómo Mantienes la Consistencia entre una Base de Datos y un Caché?

BackendDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

PostgreSQL es la fuente de verdad y Redis almacena en caché los datos de visualización de productos. El sistema atiende 20,000 lecturas y 500 escrituras por segundo sobre aproximadamente 2,000 productos activos, con un máximo de 5 segundos de desactualización permitida para los datos de visualización. Diseña los caminos de lectura, escritura e invalidación, maneja los rellenos concurrentes y el retraso de réplicas, e identifica los datos que no deben depender de este caché.

Enunciado y Contexto Aplicable

PostgreSQL es la fuente de verdad para los datos de productos. Redis almacena una instantánea de visualización indexada por product_id. El sistema maneja aproximadamente 20,000 lecturas y 500 escrituras por segundo sobre alrededor de 2,000 productos activos. Los nombres, imágenes y textos de visualización pueden estar desactualizados por un máximo de 5 segundos. Las deducciones de inventario, los precios en el checkout, los permisos y los saldos están fuera de ese contrato de caché porque determinan la corrección del negocio.

Diseña los caminos de lectura, actualización e invalidación cache-aside. Cubre estos casos:

  • la base de datos confirma la transacción, pero el proceso falla antes de eliminar la entrada del caché;
  • un lector obtiene una versión antigua de la base de datos y completa su relleno solo después de que un escritor invalida el caché;
  • un relleno lee datos rezagados de una réplica asincrónica de la base de datos;
  • los eventos de invalidación se duplican, reordenan o acumulan;
  • Redis no está disponible temporalmente;
  • el equipo de producto quiere que "máximo 5 segundos de desactualización" sea un contrato monitoreado y probado.

El throughput, el conteo de claves activas y el presupuesto de 5 segundos son supuestos de la entrevista, no configuraciones universales. El material actual de entrevistas de backend y caché senior para 2026 cubre explícitamente cache-aside, invalidación tras escrituras y consistencia de caché. La habilidad central es el protocolo de backend y la semántica de fallos tras un commit en la base de datos, por lo que la categoría es backend. Esto es distinto de prevenir una estampida de caché cuando expira una clave popular: el control de estampidas limita las lecturas concurrentes a la fuente, mientras que este problema pregunta cuándo un valor antiguo puede sobrevivir una escritura, por qué sobrevive y cuánto tiempo puede permanecer.

Qué Evalúa el Entrevistador

Primero, ¿puede el candidato definir el objetivo de consistencia antes de elegir un patrón? "La base de datos y el caché siempre son consistentes" no especifica qué resultados de lectura son válidos. Una respuesta sólida separa la desactualización acotada de 5 segundos para datos de visualización, el read-your-writes para quien realizó la actualización, y las lecturas autoritativas para inventario o permisos. Esos contratos requieren caminos distintos.

Segundo, ¿pueden explicar el orden de escritura? Un camino de escritura cache-aside común confirma la base de datos y luego elimina la entrada del caché. Eliminar primero crea una condición de carrera clara: un lector falla entre las dos operaciones, obtiene el valor antiguo de la base de datos y lo vuelve a colocar en el caché. Hacer primero la base de datos tampoco es una escritura dual atómica. Existe una breve ventana entre el commit y la eliminación, y una eliminación fallida puede dejar una entrada antigua hasta que expire su TTL.

Tercero, ¿pueden trazar la línea de tiempo del relleno tardío desactualizado? Un lector puede obtener la versión 41 antes de una actualización en la base de datos. Un escritor confirma la versión 42 y elimina el caché; después, el lector antiguo vuelve a colocar la versión 41. Agregar una versión únicamente al valor en caché no siempre es suficiente. Tras la eliminación no hay versión en caché con la que comparar, por lo que una regla como "escribir cuando la versión de la fuente es al menos igual a la versión en caché" acepta la 41. El diseño necesita una valla de versión que sobreviva la eliminación del valor, un lease de relleno que las escrituras invaliden u otra verificación autoritativa de versión antes del relleno.

Cuarto, ¿pueden distinguir la invalidación confiable de un límite de tiempo finito? Un outbox transaccional o captura de cambios en los datos (CDC) cierra la brecha en la que la base de datos confirma pero ningún mensaje de invalidación queda registrado de forma duradera. La entrega at-least-once y la eliminación idempotente toleran duplicados. Los reintentos garantizan el procesamiento eventual; no prueban la finalización dentro de 5 segundos. La desactualización acotada necesita adicionalmente un TTL estricto, una compuerta de retraso de invalidación, o una regla que omita el caché una vez que el pipeline supera su presupuesto.

Quinto, ¿pueden manejar la fuente de verdad, las réplicas de la base de datos y la verificación operacional? Un commit exitoso en la base de datos crea el nuevo hecho. Rellenar desde una réplica rezagada puede reintroducir un valor antiguo después de una invalidación correcta. Una respuesta sólida restringe la fuente del relleno, observa las versiones de eventos y caché, y prueba líneas de tiempo de concurrencia controladas y fallos en lugar de reportar únicamente la tasa de aciertos del caché.

Preguntas para Clarificar Antes de Responder

  • ¿Cuál es el contrato de consistencia? Si se permiten hasta 5 segundos de desactualización, la invalidación asincrónica más un plazo estricto puede funcionar. Si se requiere read-your-writes, las solicitudes posteriores del escritor deben omitir brevemente el caché o llevar una versión mínima. Si ninguna lectura desactualizada es válida, lee el almacén autoritativo o usa un camino de almacenamiento con el contrato de consistencia requerido.
  • ¿Qué campos autorizan acciones irreversibles? Un nombre de visualización o imagen puede estar desactualizado. La validación de inventario, la elegibilidad para descuentos, los permisos, los saldos y los precios cobrados no deben tratar una instantánea en caché como autoridad. Si esos campos comparten un objeto, separa los contratos de lectura o vuelve a leer el estado autoritativo durante la acción crítica.
  • ¿Cuándo empieza el reloj de los 5 segundos? En este enunciado, comienza en el commit de la base de datos. Si comienza cuando el valor entra en Redis, una réplica que ya lleva 4 segundos de retraso puede almacenarse en caché por 5 segundos más, haciendo que la antigüedad real del dato sea de casi 9 segundos.
  • ¿Un solo servicio controla todos los caminos de escritura? Los trabajos por lotes, las herramientas de administración y otros servicios pueden omitir un DEL a nivel de API. Coloca un registro de invalidación en la misma transacción de la base de datos o captura cada cambio soportado desde el log de la base de datos.
  • ¿Los rellenos de un miss provienen del primario o de una réplica? Establece el límite de retraso de la réplica y si existe read-your-writes a nivel de sesión. Si no se puede demostrar que la réplica esté actualizada dentro del presupuesto, el primer relleno tras una invalidación debe usar el primario o requerir un resultado al menos tan nuevo como la versión mínima del solicitante.
  • ¿Cuánto tráfico de relleno puede absorber la base de datos? Con un TTL de 5 segundos, 2,000 claves activas con expiración uniforme que se leen todas producen aproximadamente 2000 / 5 = 400 rellenos por segundo en promedio. La distribución de accesos, el jitter y la coalescencia de solicitudes cambian el valor real. Si la base de datos no tiene ese presupuesto disponible, acortar el TTL no satisface mágicamente la consistencia.
  • Cuando Redis no está disponible, ¿es más importante la frescura o la disponibilidad? Los datos de visualización pueden degradarse dentro de un límite explícito de valor desactualizado. Los campos autoritativos deben usar un camino de fuente protegido. Si la base de datos no puede absorber todos los misses, aplica límites de tasa, bulkheads y fallos explícitos en lugar de un fallback sin límites.

Marco de Respuesta en 30 Segundos

"Primero defino los contratos por tipo de dato. Los datos de visualización del producto pueden estar desactualizados por un máximo de 5 segundos desde el commit de la base de datos, mientras que el inventario y los permisos siempre usan un camino autoritativo. Una lectura verifica Redis, coalesca los misses de la misma clave, obtiene una fila versionada de una fuente que satisface el requisito de frescura, y condiciona el relleno del caché. Una escritura actualiza la fila de negocio e inserta un registro de outbox con la versión de la fila en una sola transacción de PostgreSQL. Tras el commit, intenta una eliminación rápida del caché, mientras que el camino de outbox o CDC proporciona reparación con reintentos.

Confirmo la base de datos antes de eliminar del caché. Para evitar que una lectura antigua rellene después de esa eliminación, un miss captura una generación de relleno; la invalidación avanza una valla de versión y elimina el valor; el relleno tiene éxito solo si la generación no ha cambiado y la versión de la fuente cumple la valla. Los reintentos eventuales no prueban un límite de 5 segundos, por lo que el caché tiene un TTL estricto dentro del presupuesto y las lecturas lo omiten cuando el retraso de invalidación se acerca al límite. Un relleno tras invalidación usa el primario cuando el retraso de la réplica no puede cumplir el contrato. Verifico el diseño con pruebas de commit-y-fallo, relleno tardío, eventos duplicados y reordenados, y fallos de retraso de réplica, afirmando la antigüedad del dato desactualizado, las versiones monotónicas y la carga en la base de datos."

Análisis Paso a Paso en Profundidad

Paso 1: Convertir la consistencia en un contrato de resultados de lectura

Clasifica los datos por consecuencia de negocio antes de seleccionar un patrón de caché:

CaminoResultado válidoCamino de lectura recomendado
Visualización del productoMáximo 5 segundos desactualizado desde el commit de la base de datosCaché cache-aside de Redis, TTL estricto y compuerta de retraso de invalidación
Lectura posterior del escritorAl menos la versión recién confirmada por ese solicitanteLlevar min_version; usar el primario si el caché es más antiguo
Inventario, permisos, saldos, checkoutLa decisión debe usar el estado autoritativo actualOmitir el caché de visualización y validar en la transacción o servicio autoritativo

Esta clasificación guía el resto del diseño. Un caché acelera una copia reconstruible. No debe autorizar una deducción de inventario usando un valor que puede estar retrasado, desalojado o perdido. Una tasa de aciertos del 99.9% no dice nada sobre si el otro 0.1% produce sobreventas o accesos no autorizados.

Cada entrada del caché necesita al menos una versión de la fuente e información de tiempo. Lo siguiente es una pseudoestructura, no un tipo ejecutable en un lenguaje específico:

text
ProductCacheEntry {
  value
  source_version
  source_committed_at
  cached_at
}

source_committed_at mide la antigüedad real del dato desactualizado; cached_at indica solo cuándo Redis recibió el valor. La versión de la fuente puede ser una versión de fila que aumenta monotónicamente, una secuencia de commit u otra versión de dominio con un orden definido. Una marca de tiempo de reloj de pared por sí sola es una prueba de ordenamiento deficiente si los valores pueden coincidir o los relojes pueden desviarse.

Paso 2: Establecer los caminos básicos de cache-aside

El camino de lectura devuelve un acierto de caché solo cuando satisface tanto la versión mínima del solicitante como el presupuesto de antigüedad. En un miss, coalesca los rellenos para el mismo product_id para que 20,000 lecturas no lleguen a la base de datos al mismo tiempo. Obtén una fila versionada, luego intenta una escritura condicional en Redis. Lo siguiente es pseudocódigo de flujo:

text
read(product_id, min_version = none):
  entry = cache.get(product_id)
  if entry satisfies age_budget and min_version:
    return entry.value

  return singleflight(product_id):
    recheck cache
    row = read_authoritative_version(product_id)
    conditional_fill(product_id, row)
    return row.value

El camino de escritura actualiza la fila de negocio e inserta un registro de outbox en la misma transacción de PostgreSQL. Solo un COMMIT exitoso hace visible el cambio de negocio para otras transacciones y lo hace duradero. Luego la aplicación realiza un intento rápido de invalidación. Un relay separado o consumidor de CDC procesa el evento de invalidación durable para reintentos y reparación. El pseudocódigo del camino de escritura es:

text
transaction:
  row = update product and increment source_version
  insert outbox(product_id, source_version, committed_at)
commit

best_effort_invalidate(product_id, source_version)
return committed source_version

La invalidación directa reduce la ventana del caso común. El outbox cierra la brecha de pérdida de mensajes cuando el proceso falla tras el commit. Si ambos caminos manejan el mismo cambio, la eliminación debe ser idempotente: recibir la misma versión de la fuente nuevamente no puede causar un nuevo efecto secundario de negocio.

Paso 3: Analizar las tres ventanas de condición de carrera explícitamente

Ventana 1: entre el commit de la base de datos y la eliminación del caché.

text
W: COMMIT version 42
R: read cached version 41
W: DEL cache key

El lector ve brevemente la versión 41, lo cual es válido bajo desactualización acotada. Si ningún resultado desactualizado es aceptable, esperar una operación en el caché antes de devolver la respuesta de escritura aún no resuelve toda ambigüedad de red. Un contrato más claro envía la lectura crítica a la fuente de verdad o permite que el solicitante requiera min_version=42.

Ventana 2: la base de datos confirma pero la invalidación no se ejecuta.

text
W: COMMIT version 42 plus outbox record
W: process crashes before DEL
R: cache still contains version 41
relay: retries invalidation for version 42

Una tarea en memoria creada después de COMMIT puede perderse. El outbox transaccional confirma el cambio de negocio y la obligación de invalidar juntos. El relay puede reenviar, mientras que el consumidor elimina por clave y registra la versión procesada. Si el relay se detiene, el TTL estricto o la compuerta de frescura impone el límite de 5 segundos.

Ventana 3: una lectura antigua rellena tras la invalidación.

text
R: cache miss; captures generation 7
R: reads database version 41
W: commits version 42
W: advances generation to 8 and deletes cache value
R: tries to fill version 41 with generation 7; rejected

Esta es la condición de carrera que se pasa por alto con mayor frecuencia. Si el diseño elimina solo el valor, entonces version 41 >= no version pasa y resucita datos desactualizados. Una solución separa el valor de su valla. La invalidación avanza una clave de generación o versión mínima de corta duración. Un script de Redis verifica atómicamente que la generación del relleno aún coincida con la generación capturada en el momento del miss y que la versión de la fuente cumpla el mínimo antes de escribir el valor. En Redis Cluster, las claves de valor y valla deben usar el mismo hash slot para que el script pueda acceder a ambas atómicamente. Otra implementación emite un lease de miss que una actualización de la base de datos invalida. Releer la versión del primario antes de rellenar puede ayudar, pero agrega una lectura a la base de datos y aún necesita un límite atómico definido entre la verificación y la escritura en el caché.

Conserva la valla de versión el tiempo suficiente para cubrir la duración máxima del relleno, los reintentos, y las pausas del proceso o de la red. Eliminarla demasiado pronto reabre la ventana de relleno tardío. La valla protege el orden de escritura en el caché; no reemplaza el control de concurrencia de la base de datos ni le indica a un lector ordinario que la base de datos contiene una versión más nueva cuya invalidación aún no ha llegado.

Paso 4: Hacer la invalidación recuperable sin exagerar la entrega

La fila del outbox y la actualización de negocio se escriben en una transacción. Un relay envía la invalidación a un canal durable. El consumidor puede rastrear la versión más alta observada para cada product_id y aplicar estas reglas:

  1. Un evento fuera de orden por debajo de la versión más alta observada se reconoce idempotentemente.
  2. Una versión nueva avanza la valla de versión mínima y elimina el valor en caché.
  3. Un comando de caché fallido se reintenta; el trabajo agotado entra en una cola de cuarentena visible.
  4. Un trabajo de reconciliación compara las versiones de la base de datos, el progreso del outbox y las versiones del caché en muestreo.

La entrega at-least-once funciona bien con la eliminación porque un DEL duplicado no tiene ningún significado de negocio adicional. No se debe afirmar que los reintentos crean ejecución exactly-once. Un consumidor puede eliminar en Redis, fallar antes de reconocer el mensaje y eliminar de nuevo. La repetición segura y las brechas detectables son las propiedades útiles.

Observa el intervalo desde el commit de la base de datos hasta la invalidación exitosa, no solo la antigüedad de la cola del broker. Las métricas útiles incluyen invalidation_lag_seconds, fallos de invalidación y reintentos, antigüedad más alta en cuarentena, retraso de versión caché-a-fuente, bypasses del caché por exceso de presupuesto, QPS de relleno en la base de datos y el número de llamadas de la misma clave compartidas a través de singleflight.

Paso 5: Demostrar el límite de 5 segundos con un TTL y una compuerta

Un evento confiable eventualmente llega, pero "eventualmente" no tiene unidad de tiempo. Un contrato de máximo 5 segundos de desactualización desde el commit necesita una defensa independiente de tiempo finito:

  • Establece el TTL físico del caché por debajo de 5 segundos reservando margen para relojes, programación y detección, y calcula la antigüedad del dato desactualizado desde el tiempo de commit de la versión autoritativa.
  • Alternativamente, cuando el retraso de invalidación se acerque al presupuesto, deja de confiar en el caché globalmente o por partición afectada y lee una fuente que cumpla el requisito de frescura.
  • Si ninguna de las dos opciones es posible, el contrato es consistencia eventual y no debe seguir afirmando un límite de 5 segundos.

El TTL es un respaldo de límite, no el mecanismo primario de invalidación. Agrega jitter para que 2,000 claves no expiren al mismo tiempo, y coalesca los misses para cada clave. Si las 2,000 claves activas se leen al menos una vez por intervalo de 5 segundos, la expiración distribuida uniformemente produce aproximadamente 400 rellenos por segundo en promedio. Esa estimación no es una garantía de capacidad. La asimetría de popularidad, la expiración en lote, los fallos de Redis y las consultas lentas pueden crear picos, así que prueba contra el QPS seguro de la base de datos y los bulkheads de concurrencia.

Si un TTL de 5 segundos supera la capacidad de la base de datos, hay tres opciones honestas: agregar capacidad segura de relleno, reducir los datos activos que necesitan este contrato, o relajar el presupuesto de desactualización. Aumentar silenciosamente el TTL viola el enunciado.

Paso 6: Manejar el retraso de réplicas y el read-your-writes

Incluso después de una eliminación correcta, una réplica rezagada puede rellenar una versión antigua. Para un miss con un contrato estricto de 5 segundos, usa este orden de preferencia:

  1. Lee el primario para el primer relleno después de la invalidación.
  2. Usa una réplica solo cuando una posición de reproducción observable haya alcanzado la posición de commit requerida.
  3. Devuelve source_version de una escritura y permite que las lecturas posteriores lleven min_version; enruta al primario cuando el caché o la réplica están rezagados.
  4. Abre un circuit breaker de frescura cuando el retraso de la réplica o de la invalidación supere el presupuesto, y deja de devolver valores en caché sin calificación.

Un TTL de 5 segundos no prueba una antigüedad de datos de 5 segundos si el relleno provino de una réplica ya con 4 segundos de retraso. La antigüedad del dato comienza en el commit de la fuente de verdad, por lo que los retrasos de la réplica, del canal de eventos y del caché cuentan todos.

Paso 7: Comparar alternativas y sus límites

Actualizar el caché de forma síncrona. Escribir en Redis inmediatamente después de la base de datos puede mejorar el read-your-writes, pero dos sistemas independientes aún tienen fallos parciales. La base de datos puede confirmar mientras la actualización del caché falla, y escrituras concurrentes en la base de datos pueden llegar al caché en un orden diferente. Este camino aún necesita condiciones de versión de la fuente y reparación; llamarlo write-through no hace que las dos escrituras sean una transacción atómica.

Eliminar primero, luego actualizar la base de datos. Esto es simple pero permite que un lector rellene datos antiguos antes del commit de la base de datos. Una segunda eliminación retrasada reduce la probabilidad de un timing particular, pero un retraso fijo no puede cubrir el retraso ilimitado de réplicas, las pausas del proceso o los fallos de red. El proceso también puede fallar antes de la segunda eliminación. Puede ser una medida auxiliar, pero no prueba la desactualización acotada por sí sola.

Usar solo un TTL corto. Esta puede ser la opción adecuada más simple cuando las escrituras son poco frecuentes, el presupuesto de desactualización es generoso y la base de datos puede absorber los rellenos. El costo es que cada escritura puede ir seguida de lecturas desactualizadas durante el TTL completo, mientras que la expiración sincronizada puede amplificar la carga en la base de datos.

Leer todo desde la base de datos. Para datos de bajo throughput o críticos en cuanto a corrección, esta suele ser la solución más clara. Un caché es opcional. Cuando el costo de coordinación de consistencia es mayor que las lecturas de la base de datos que ahorra, eliminar el caché es razonable.

Paso 8: Probar invariantes y caminos de fallo

Haz más que actualizar manualmente una página después de una actualización. Usa barreras para reproducir el relleno tardío desactualizado. Pausa un lector después de que obtenga la versión 41. Deja que un escritor confirme la versión 42, avance la valla y elimine el valor. Libera al lector antiguo y afirma que su relleno condicional falla. Luego prueba estos casos:

  • terminar el escritor inmediatamente después de que PostgreSQL confirme y verificar que el outbox eventualmente invalida;
  • duplicar y reordenar un evento de invalidación y verificar que la versión más alta nunca retroceda;
  • fallar la eliminación en Redis y verificar reintentos, cuarentena y el respaldo del TTL;
  • pausar la reproducción de la réplica y verificar que los rellenos usen el primario o rechacen un resultado no calificado;
  • retrasar el consumo de invalidación más allá de su compuerta y verificar que las lecturas omitan el caché;
  • llevar 2,000 claves cerca de la expiración al mismo tiempo y verificar jitter, singleflight y bulkheads de la base de datos;
  • dejar Redis no disponible y verificar la degradación dentro del presupuesto para las lecturas de visualización mientras las decisiones autoritativas mantienen su camino requerido.

Los invariantes clave son: una versión del caché devuelta nunca es menor que el min_version del solicitante; una generación de relleno invalidada no puede escribir; una acción de negocio autoritativa nunca confía en el caché de visualización; la antigüedad del dato desactualizado no supera los 5 segundos; y los rellenos de la base de datos permanecen dentro del QPS seguro probado y la concurrencia.

Respuesta de Muestra de Alta Calidad

"No comenzaría prometiendo que PostgreSQL y Redis coinciden en cada instante. Separaría los contratos de lectura. Los nombres e imágenes de productos pueden estar desactualizados por un máximo de 5 segundos desde el commit de PostgreSQL. Las deducciones de inventario, los permisos, los saldos y las decisiones de checkout continúan usando el estado autoritativo. Cuando un escritor necesita read-your-writes, la API de escritura devuelve la versión de la fuente. Una lectura posterior proporciona esa versión mínima y usa el primario cuando Redis es más antiguo.

El patrón base es cache-aside. Una lectura devuelve un acierto solo cuando su antigüedad y versión califican. Un miss usa singleflight por product_id, obtiene una fila versionada monotónicamente e realiza un relleno condicional. Una transacción de PostgreSQL actualiza el producto, incrementa su versión e inserta una fila de outbox. Tras el commit, la aplicación intenta eliminar Redis de inmediato. Un relay o consumidor de CDC reintenta desde el evento durable, por lo que un fallo tras el commit no pierde permanentemente la obligación de invalidación.

El orden es primero la base de datos, luego la eliminación del caché. Eliminar primero permite que un lector restaure datos antiguos antes de que la base de datos confirme. Incluso con el orden correcto, persiste un relleno tardío: un lector obtiene la versión 41, un escritor confirma la 42 y elimina, luego el lector antiguo establece la 41. Una versión dentro del valor en caché por sí sola es insuficiente porque no queda la 42 después de la eliminación. Hago que el miss capture una generación. La invalidación avanza la generación y la versión mínima antes de eliminar el valor. Un script atómico rechaza el relleno a menos que la generación no haya cambiado y la versión de la fuente cumpla la valla. Si una réplica no puede demostrar que ha reproducido el commit requerido, el relleno tras invalidación lee el primario.

El outbox prueba la invalidación eventual recuperable, no un plazo de 5 segundos. Establezco un TTL estricto dentro del presupuesto con margen operacional y mido el retraso de commit a invalidación. Cuando ese retraso se acerca al presupuesto, las lecturas omiten Redis. Si se accede a las 2,000 claves activas cada 5 segundos, los rellenos uniformes promedian aproximadamente 400 por segundo, por lo que también uso jitter en el TTL, coalescencia de solicitudes por clave y un bulkhead de concurrencia en la base de datos.

Para la verificación, controlo el orden de los hilos e inyecto fallos de commit-y-fallo, lector tardío, eventos duplicados y reordenados, eliminación fallida en Redis y retraso de réplica. La aceptación significa que una generación antigua no puede rellenar, las versiones de la fuente no retroceden, la antigüedad del dato desactualizado permanece dentro de los 5 segundos, las acciones autoritativas omiten el caché de visualización y la carga en la base de datos permanece dentro del presupuesto medido."

Errores Comunes

  • Afirmar "consistencia fuerte entre base de datos y caché" → La respuesta no define lecturas válidas, plazos de fallo ni un límite de transacción entre los dos sistemas → Establece contratos separados para desactualización acotada, read-your-writes y lecturas autoritativas.
  • Eliminar el caché antes de actualizar la base de datos → Un miss entre esas operaciones lee y restaura el valor antiguo de la base de datos → Confirma la base de datos primero, luego invalida, con reparación durable.
  • Publicar solo un mensaje en memoria tras el commit → Un fallo antes de la publicación pierde permanentemente la invalidación → Escribe un outbox en la misma transacción o captura los cambios del log de la base de datos.
  • Asumir que DEL termina cada condición de carrera → Una lectura más antigua puede completar su set después de la eliminación → Rechaza los valores desactualizados tardíos con un lease de relleno o una valla de versión que sobreviva la eliminación del valor.
  • Poner una versión solo dentro del valor en caché → Un caché vacío no tiene una versión más nueva con la que comparar y puede aceptar un valor antiguo → Mantén la versión mínima o generación en una valla separada y verifica el relleno atómicamente.
  • Tratar el consumo duplicado como un error → Un fallo del consumidor antes del reconocimiento crea naturalmente la reentrega → Haz que la eliminación y el avance de la versión más alta sean idempotentes bajo entrega at-least-once.
  • Usar el outbox para prometer 5 segundos → La capacidad de reintento prueba el manejo eventual, no un retraso finito → Agrega un TTL estricto o una compuerta de bypass por exceso de presupuesto y mide el retraso de commit a invalidación de extremo a extremo.
  • Rellenar desde cualquier réplica de lectura → El retraso de la réplica puede repoblar datos antiguos después de una invalidación correcta → Verifica la posición de reproducción, usa el primario o requiere una versión mínima de la fuente.
  • Almacenar todos los campos en una instantánea de visualización → La tolerancia de desactualización de los datos de visualización se filtra hacia el inventario, los permisos y el checkout → Separa los contratos de datos y vuelve a leer el estado autoritativo para acciones irreversibles.
  • Enviar todo el tráfico a la base de datos cuando Redis falla → 20,000 lecturas por segundo pueden saturar la fuente de verdad → Protégela con bulkheads, límites de tasa, degradación explícita y recuperación controlada.
  • Usar una doble eliminación retrasada fija → Un sleep no puede cubrir el retraso ilimitado y la segunda eliminación también puede perderse → Trátala como una optimización de probabilidad mientras se mantiene la invalidación durable, una valla y un respaldo de tiempo finito.

Preguntas de Seguimiento y Respuestas

Seguimiento 1: ¿Qué pasa si los precios del producto también requieren read-your-writes?

Devuelve el source_version confirmado desde la API de escritura. Una lectura posterior en esa sesión envía min_version; si Redis es más antiguo, lee el primario y almacena condicionalmente solo la nueva versión. Si el escritor simplemente necesita mostrar el resultado, devuelve la fila confirmada directamente y omite brevemente el caché. El checkout aún debe revalidar el precio en la transacción autoritativa; el read-your-writes no es autorización de pago.

Seguimiento 2: ¿Por qué la valla de versión no puede vivir solo en el valor en caché?

La invalidación elimina ese valor, por lo que una comparación ordinaria ya no puede ver la versión 42. Una solicitud que leyó previamente la versión 41 puede entonces tratar a la 41 como un valor válido para una clave vacía. Una valla separada o un lease de miss sobrevive a la eliminación del valor y registra que existe la versión 42 o que el permiso de relleno antiguo ha sido revocado.

Seguimiento 3: ¿Puede un outbox transaccional enviar duplicados?

Sí. El relay puede fallar después de que el sistema downstream acepta un evento pero antes de marcar la fila del outbox como completada. El consumidor maneja (product_id, source_version) de forma idempotente: una versión antigua no avanza el estado, una versión nueva avanza el máximo y elimina el valor, y la eliminación duplicada es segura. La garantía útil es que no haya pérdida silenciosa de invalidación más reconciliación, no ejecución exactly-once.

Seguimiento 4: ¿Puede el TTL ser de 30 minutos si CDC maneja la invalidación?

Puede ser un intercambio válido cuando el negocio solo requiere consistencia eventual y acepta una interrupción prolongada de la invalidación. Este enunciado promete máximo 5 segundos de desactualización. Un TTL de 30 minutos viola ese límite cuando CDC se detiene, a menos que el sistema omita automáticamente el caché cuando el retraso de invalidación se acerque a los 5 segundos. De lo contrario, mantén un plazo estricto dentro del presupuesto.

Seguimiento 5: El retraso de réplica normalmente es solo de decenas de milisegundos. ¿Por qué usar el primario?

"Normalmente" no es un límite superior. Los despliegues, las particiones de red, las transacciones largas y la recuperación pueden aumentar el retraso. Una réplica sigue siendo utilizable cuando su posición de reproducción está demostrada que incluye el commit requerido o la lectura lleva una versión mínima. Si eso no puede probarse, enruta el miss crítico al primario. La elección sigue el contrato de 5 segundos y el presupuesto de capacidad de la base de datos.

Seguimiento 6: ¿Por qué no actualizar PostgreSQL y Redis mientras se sostiene un lock distribuido?

Un lock restringe solo a los participantes que siguen ese protocolo. No puede hacer que dos sistemas confirmen atómicamente, ni eliminar fallos del titular, expiración del lock, particiones de red o escritores externos en la base de datos. La transacción de la base de datos establece el hecho, y la invalidación durable repara el segundo sistema. Un lock puede reducir la concurrencia de la misma clave, pero no es una prueba de consistencia.

Seguimiento 7: ¿Cómo mantienes el límite de 5 segundos cuando Redis no está disponible?

Las lecturas de visualización omiten Redis, pero cada lectura de la fuente pasa por bulkheads y límites de tasa de la base de datos. Si la capacidad es insuficiente, devuelve un resultado degradado explícito o un fallo. Una copia local antigua del proceso puede servir brevemente mientras aún esté dentro del presupuesto de 5 segundos; después de eso queda descalificada. Recupera con calentamiento con límite de tasa mientras drenas las invalidaciones para que un caché frío no vuelva a saturar la base de datos.

Seguimiento 8: ¿Cómo demuestras que la producción no contiene valores desactualizados de larga duración?

Para claves en muestreo, lee la versión de la fuente de la base de datos y la versión en caché juntas, y registra tanto la distancia de versión como la antigüedad desde el commit de la fuente. Reconcilia los cambios en la base de datos, el progreso del outbox y las marcas de agua altas del consumidor. Alerta sobre el retraso de invalidación de extremo a extremo, el trabajo más antiguo en cuarentena y los bypasses por exceso de presupuesto. Inyecta regularmente fallos de commit-y-fallo, fallo de comando de Redis y pausa de réplica para demostrar que la compuerta y el TTL aún protegen los invariantes bajo fallos reales.

Fuentes públicas

Preguntas relacionadas