Tema representativo de entrevista

¿Cómo se diseña un outbox transaccional para lograr consistencia entre base de datos y mensajería?

BackendDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Un servicio de órdenes debe actualizar su base de datos y publicar un evento en una sola solicitud, pero la base de datos y el broker no pueden compartir una transacción distribuida. Diseña un outbox transaccional y explica el comportamiento del relay, los mensajes duplicados, el ordenamiento, la recuperación y la limpieza.

1. Enunciado

Cuando se crea una orden, el servicio de órdenes debe confirmar el estado y publicar un evento OrderCreated para los consumidores de inventario y notificaciones. La base de datos y el broker no tienen un commit de dos fases compartido. Diseña un outbox transaccional de modo que un fallo del proceso no pierda silenciosamente el evento, mientras que la entrega duplicada y el backlog del relay se mantengan manejables.

2. Restricciones y aclaraciones

  • Los datos de la orden y la tabla de outbox comparten una misma base de datos transaccional local.
  • El broker provee entrega at-least-once, sin ordenamiento global ni envíos transaccionales.
  • La consistencia eventual es aceptable; el consumidor de inventario debe ser idempotente.
  • Explicar el ordenamiento por agregado, si se necesita ordenamiento entre agregados, y las ventanas de retención/eliminación.

3. Enfoque principal

Escribe el cambio de la orden y una fila de outbox en la misma transacción de base de datos. La fila contiene un event_id único, la clave del agregado, el tipo de evento, la secuencia, el payload, la hora de creación y el estado de publicación. Un commit exitoso hace que tanto los datos de negocio como el evento pendiente sean duraderos; un rollback no expone ninguno de los dos, eliminando la ventana de escritura dual a nivel de aplicación.

Un relay independiente sondea o se suscribe al outbox, publica en el broker y luego marca la fila como enviada. Si el proceso falla entre el reconocimiento del broker y la actualización del estado, el evento puede publicarse nuevamente. Los consumidores, por tanto, deduplicarán por event_id en lugar de asumir entrega exactly-once.

4. Implementación de referencia

text
createOrder(command):
  begin transaction
  order = insert orders(...)
  event = insert outbox(
    event_id=uuid(), aggregate_id=order.id,
    aggregate_version=order.version, type="OrderCreated",
    payload=serialize(order), status="pending"
  )
  commit
  return order.id

relayBatch():
  rows = select pending outbox rows
         order by aggregate_id, aggregate_version, created_at
         for update skip locked limit BATCH_SIZE
  for row in rows:
    try:
      broker.publish(key=row.aggregate_id, id=row.event_id, body=row.payload)
      mark_sent(row.event_id)  // conditional update
    except transient_error:
      increment_attempts_and_schedule_retry(row.event_id)

consume(message):
  begin transaction
  inserted = insert processed_messages(message.id) on conflict do nothing
  if inserted:
    apply_business_change(message)
  commit

5. Confiabilidad y corrección

Si la transacción de negocio se confirma pero el relay falla antes de publicar, un escaneo posterior encontrará la fila pendiente. Si la publicación tiene éxito pero el proceso falla al actualizar el estado, la siguiente pasada la vuelve a publicar. La semántica de extremo a extremo es, por lo tanto, at least once; una tabla de deduplicación del consumidor o una clave de idempotencia de negocio deben compartir una transacción con la actualización de negocio del consumidor.

El ordenamiento por agregado puede usar una versión monotónica y particionamiento por clave de agregado; no se debe prometer ordenamiento global entre agregados. Los campos SELECT ... FOR UPDATE SKIP LOCKED o de lease evitan que múltiples relays reclamen la misma fila, pero no reemplazan la idempotencia del consumidor. Indexa el estado, el tiempo de reintento y la hora de creación, luego archiva o elimina de forma segura las filas antiguas para acotar el crecimiento de la tabla.

6. Seguimientos y trampas

  • Eliminar una fila inmediatamente después de una publicación "exitosa" puede crear una brecha irrecuperable si el reconocimiento se perdió; persiste el estado de envío o conserva un registro de auditoría primero.
  • Un timeout de reconocimiento del broker no prueba que el broker no recibió el mensaje, por lo que los reintentos deben tolerar duplicados.
  • Escribir primero en la base de datos y luego llamar al broker dentro del manejo de errores de la aplicación sigue teniendo una condición de carrera de escritura dual; un try/catch no puede hacerlo atómico.
  • Si el outbox y las tablas de negocio no pueden compartir un límite de transacción, usa CDC, mensajería transaccional o redefine la garantía de consistencia.

7. Lecturas adicionales

Compara los relays de polling con los relays de CDC: el polling es más simple de desplegar pero añade escaneos y latencia, mientras que CDC reduce la latencia a costa de la captura de log y dependencias operativas. Discute mensajes envenenados, backoff exponencial, colas de dead-letter, monitoreo de antigüedad de pendientes y compatibilidad de esquemas del consumidor.

8. Puntos de evaluación en la entrevista

Puede identificar la ventana de escritura dual

El candidato debe explicar por qué las transacciones locales ordinarias no pueden confirmar una actualización de base de datos y un envío al broker juntos, y luego colocar el cambio de la orden y la fila de outbox en una sola transacción.

Puede explicar at-least-once e idempotencia

Debe describir la ventana de fallo del relay que genera duplicados y lograr que el consumidor deduplique por ID de evento en la misma transacción que su actualización de negocio.

Puede manejar el ordenamiento y la concurrencia

Debe distinguir el orden por agregado del orden global y explicar cómo las claves de partición, versiones, bloqueos o leases limitan las reclamaciones concurrentes.

Puede cubrir los límites operativos

Debe proponer backoff de reintentos, dead letters, alertas de backlog, limpieza por archivado y evolución de esquemas, en lugar de detenerse en una definición de tabla.

Fuentes públicas

Preguntas relacionadas