Un estadio tiene 60 000 asientos. Cuando sale a la venta una gran gira, millones de personas llegan en el mismo minuto, y todas quieren los mismos pocos miles de buenos asientos. En noviembre de 2022, Ticketmaster informó de 3,5 mil millones de solicitudes totales al sistema durante la preventa del Eras Tour, cuatro veces su pico anterior. Guardar 60 000 filas es trivial. Arbitrar millones de solicitudes que compiten por esas filas, de forma justa y sin vender nunca un asiento dos veces, es la parte difícil.
Este artículo diseña un sistema de venta de entradas para ese tipo de apertura de venta: requisitos, la forma del tráfico, una sala de espera virtual, bloqueos de asientos con caducidad, la transacción de reserva, los pagos, los asientos codiciados y los bots, con un diagrama de arquitectura.
Requisitos
Funcionales:
- Explorar eventos y un mapa de asientos con disponibilidad.
- Elegir asientos y bloquearlos mientras se paga.
- Pagar, recibir las entradas y liberar los asientos si el pago no se realiza.
No funcionales:
- No vender nunca un asiento dos veces. Es la única regla que no se puede doblar.
- Tratar a la gente con justicia: orden de llegada o un sorteo publicado, no quien recargue más rápido.
- Aguantar tráfico de uno o dos órdenes de magnitud por encima de lo normal durante los primeros minutos de una apertura de venta.
- Fallar rápido y con claridad: un «agotado» rápido es mejor que una página que no deja de cargar.
La forma de una apertura de venta
Supón 60 000 asientos y 2 millones de personas que llegan en los diez minutos siguientes al inicio. Son unas 3300 llegadas nuevas por segundo, y más de 30 personas por asiento. Ninguna capacidad da una entrada a todo el mundo. El verdadero trabajo del sistema es dejar comprar a la vez a un número sostenible de personas, decir que no rápido al resto y mantener el inventario correcto bajo una contención extrema.
Eso divide el diseño en dos partes: una puerta que absorbe a la multitud y un núcleo de reservas que solo ve tantos compradores como puede manejar.
La sala de espera virtual
Pon una cola delante del flujo de reserva:
- Los fans que llegan antes del inicio reciben una posición aleatoria entre todos los que llegaron pronto, así que abrir cien pestañas a las 9:59 no sirve de nada. Los que llegan después hacen cola por orden.
- La sala de espera es una página estática servida desde un CDN, así que millones de navegadores esperando no cuestan casi nada.
- Los fans entran por tandas, a un ritmo ajustado a la capacidad de reserva medida, por ejemplo 2000 por minuto.
- Cada fan admitido recibe un token firmado y de corta duración, vinculado a su sesión. La API de reservas rechaza las peticiones sin un token válido.
Productos gestionados como Cloudflare Waiting Room y Queue-it hacen esto. Una versión propia es un sorted set de Redis con la hora de llegada como puntuación, donde la posición es el rango del miembro:
ZADD queue:evt42 1793606400123 session:9f2c # score = arrival time in ms
ZRANK queue:evt42 session:9f2c # position, 0-based
ZPOPMIN queue:evt42 2000 # admit the next batch
Para el grupo de los que llegaron pronto, asigna puntuaciones aleatorias en lugar de horas de llegada. En cualquier caso, la cola convierte un pico sin límite en un flujo constante para el que está dimensionado el resto del sistema.
El modelo de inventario
Los asientos numerados necesitan una fila por asiento:
| Columna | Ejemplo | Propósito |
|---|---|---|
event_id, seat_id |
evt42, S112-R7-14 |
Identidad; únicos en conjunto |
status |
available, held, sold |
Estado actual |
hold_id, held_until |
h_81f, marca de tiempo |
Quién lo bloquea y hasta cuándo |
order_id |
ord_5521 |
Se asigna al venderse |
La entrada general es más sencilla: un contador por zona de precio, que se decrementa con una actualización condicional, en lugar de asientos individuales.
Bloqueos de asientos con caducidad
Un bloqueo reserva asientos para un comprador durante unos minutos mientras paga. El lugar más seguro para los bloqueos es la misma base de datos que registra las ventas, usando una actualización condicional:
UPDATE seats
SET status = 'held', hold_id = $3, held_until = now() + interval '10 minutes'
WHERE event_id = $1
AND seat_id = ANY($2)
AND (status = 'available' OR (status = 'held' AND held_until < now()));
-- If fewer rows changed than seats requested, roll back: someone else got one.
Dos compradores pueden enviar esta sentencia para el mismo asiento en el mismo milisegundo. La base de datos serializa las actualizaciones sobre esa fila: una la cambia, y la otra ve cero filas y recibe una respuesta clara de «asiento recién ocupado». Los bloqueos caducados se recuperan en la misma sentencia, así que la caducidad de un bloqueo nunca depende de que un trabajo en segundo plano se ejecute a tiempo. Dos peticiones sobre conjuntos de asientos que se solapan pueden provocar de vez en cuando un deadlock; la base de datos lo detecta y aborta una, así que reintenta una vez ante un error de deadlock.
A muy gran escala, algunos equipos ponen delante un filtro rápido: SET seat:evt42:S112-R7-14 h_81f NX PX 600000 en Redis, que crea la clave solo si no existe, con una caducidad de diez minutos. Quita carga a la base de datos, pero la replicación de Redis es asíncrona, así que un failover puede perder bloqueos. Trata Redis como una optimización; quien decide sigue siendo la base de datos.
La última línea de defensa es una restricción de unicidad sobre (event_id, seat_id) en la tabla de entradas. Aunque todas las capas superiores tengan un bug, la base de datos se niega a emitir la segunda entrada.
Asientos codiciados y mejor disponible
Todo el mundo quiere las primeras filas, así que miles de peticiones apuntan a las mismas pocas centenas de filas, y la mayoría fallan. Tres técnicas reducen la contención:
- Mejor disponible. En lugar de dejar que todos elijan en el mapa, deja que los compradores elijan una sección y una cantidad, y asigna los asientos en el servidor con
SELECT … FOR UPDATE SKIP LOCKED, de modo que las peticiones concurrentes tomen filas distintas en lugar de hacer cola sobre las mismas. - Particiona el inventario por sección, para que secciones distintas bloqueen filas distintas y puedan vivir en shards de base de datos distintos.
- Cachea el mapa de asientos con una caducidad corta y acepta que puede ir unos segundos atrasado. La autoridad sobre la disponibilidad es el bloqueo, no el mapa.
Pago sin cobros dobles
- Crea un pedido en estado
pendingvinculado al bloqueo. - Llama al proveedor de pagos con una clave de idempotencia derivada del ID del pedido. En Stripe, por ejemplo, las claves de idempotencia tienen hasta 255 caracteres, y una petición reintentada con la misma clave devuelve el resultado original en lugar de cobrar otra vez.
- Si sale bien, en una sola transacción de base de datos, marca los asientos como vendidos, asócialos al pedido y marca el pedido como pagado.
- Escucha los webhooks del proveedor para detectar el caso en que el pago se completó pero tu servicio nunca vio la respuesta.
La caducidad del bloqueo no debe competir con el pago. Amplía el bloqueo cuando empiece el pago, o excluye de la caducidad los asientos con un pedido pendiente. Si no, un asiento puede caducar, venderse a otra persona y que después su primer pago salga bien. El diagrama de integración de una pasarela de pagos trata en detalle los webhooks y la conciliación.
Liberar bloqueos caducados
Los bloqueos caducan de forma perezosa, mediante la condición held_until < now() de arriba, y de forma activa, mediante un barrido que se ejecuta cada pocos segundos y marca como disponibles los bloqueos caducados para que el mapa de asientos se ponga al día. No construyas la caducidad sobre las notificaciones de keyspace de Redis: Redis documenta su Pub/Sub como «fire and forget», así que un suscriptor que se desconecta pierde eventos.
Bots y abusos
La sala de espera es la defensa principal, porque hace que la velocidad no importe. Añade límites de compra por cuenta, límites de tasa por cuenta, IP y dispositivo, registro previo a la apertura de venta para eventos de alta demanda y desafíos en la sala de espera. El diseño de un rate limiter trata la limitación en sí.
El diagrama de venta de entradas
Contenido del diagrama
- Fans
- Waiting room (static, on CDN)
- Queue positions + admissions
- API gateway: token check
- Booking service
- Seats, holds, orders
- Hold sweeper, every 5 s
- Payment provider
- order.confirmed events
- Ticket delivery
- arrive at 10:00
- ZADD / ZRANK
- batch + signed token
- admitted?
- hold seats, pay
- hold: conditional UPDATE, 10 min
- release expired holds
- charge + Idempotency-Key
- webhook
- order paid
- send tickets
Cómo leerlo:
- Los fans llegan a la sala de espera en el CDN, que los retiene y los admite por tandas con tokens firmados.
- La puerta de enlace de API y el servicio de reservas solo ven a los fans admitidos. Redis guarda las posiciones en la cola y el estado de admisión.
- Postgres guarda los asientos. La actualización condicional crea los bloqueos, y la restricción de unicidad protege las ventas.
- Al proveedor de pagos se le llama con una clave de idempotencia, y sus webhooks vuelven al servicio de reservas.
- La función de barrido libera los bloqueos caducados; la cola de pedidos alimenta la entrega de entradas por correo electrónico y en la app.
- El bloque Markdown enumera las cifras de la apertura de venta y las reglas de bloqueo.
Dibujar sistemas de reservas para que los revisores encuentren las condiciones de carrera
- Dibuja la puerta separada del núcleo. La sala de espera y el servicio de reservas tienen modelos de escalado y responsables distintos.
- Escribe la duración del bloqueo y la regla de unicidad en el diagrama. Esos dos datos explican el diseño.
- Pon la clave de idempotencia en la arista del pago. Es la razón por la que los reintentos son seguros.
- Muestra el camino de caducidad. Donde vuelven los asientos es donde se esconden muchos bugs de venta doble.
Para el método general, consulta el ejemplo de diagrama de diseño de sistemas. Los eventos de pedido pueden publicarse con el patrón transactional outbox, y los correos de entradas a través de un pipeline como el del diseño de un sistema de notificaciones.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Añade una Persona del pack Modelo C4 para los fans y un nodo CDN del pack Nube para la sala de espera.
- Añade una Puerta de enlace de API y un nodo Instancia de cómputo del pack Nube para el servicio de reservas.
- Añade un nodo Redis del pack Datos para el estado de la cola y un nodo Postgres para asientos, bloqueos y pedidos.
- Añade un Sistema externo del pack Modelo C4 para el proveedor de pagos, una Función del pack Nube para el barrido de bloqueos, una Cola para los eventos de pedido y otro nodo Instancia de cómputo para la entrega de entradas.
- Etiqueta las aristas con los tokens, las reglas de bloqueo y la clave de idempotencia, y añade un bloque Markdown del pack Básico con las cifras.
El ejemplo usa 11 nodos de diagrama, dentro del plan Free.
Preguntas frecuentes
¿Cómo evitan la doble reserva los sistemas de venta de entradas? Con una actualización condicional sobre la fila del asiento, de modo que solo una petición pueda pasarlo de disponible a bloqueado, y una restricción de unicidad en la tabla de entradas como salvaguarda final. Las cachés y las colas reducen la carga, pero decide la base de datos.
¿Cuánto debe durar el bloqueo de un asiento? Lo bastante para pagar con calma y lo bastante poco para que los asientos abandonados vuelvan rápido. De cinco a diez minutos es un rango habitual. Amplía el bloqueo cuando empiece el pago para que la caducidad no pueda competir con un cobro correcto.
¿Qué es una sala de espera virtual? Una cola delante del flujo de reserva que admite usuarios al ritmo que el sistema puede soportar. Suele ser una página estática en un CDN, con posiciones asignadas al azar a quienes llegan pronto y por orden después.
¿Los bloqueos de asientos deben vivir en Redis o en la base de datos? La base de datos debe ser la fuente de verdad, porque la replicación de Redis es asíncrona y un failover puede perder claves. Redis funciona bien como filtro rápido delante, siempre que la base de datos siga imponiendo la unicidad.
¿Cómo gestionas un pago que se completa después de que caducara el bloqueo? Evítalo ampliando los bloqueos durante el pago. Si aun así ocurre, reembolsa automáticamente u ofrece un asiento equivalente, y usa los webhooks del proveedor para detectarlo.