Altamar Labs
Volver a Blog
arquitectura· 3 min de lectura

Concurrencia en sistemas de pujas: cuando dos usuarios no pueden ganar al mismo tiempo

Un sistema de subastas en tiempo real concentra, en un solo problema, casi todos los retos clásicos de escrituras concurrentes. Lo que enseña sobre diseñar para el momento exacto en que dos personas compiten por lo mismo.

Pocos problemas de ingeniería exponen tan rápido los errores de diseño en concurrencia como un sistema de pujas en tiempo real. A diferencia de la mayoría de operaciones de escritura, donde dos usuarios rara vez modifican exactamente el mismo recurso en el mismo instante, una subasta popular está diseñada, por definición, para que múltiples personas intenten hacer lo mismo (ganar la puja actual) en la ventana de tiempo más pequeña posible.

El problema no es la puja individual, es la carrera

Registrar una puja aislada es trivial: validar que el monto sea mayor al actual, guardarla, listo. El problema aparece cuando dos pujas por montos distintos llegan casi al mismo tiempo, separadas por milisegundos. Si el sistema lee el monto actual, lo compara, y luego escribe la nueva puja como tres pasos independientes sin ningún tipo de bloqueo, ambas pujas pueden leer el mismo monto "actual" antes de que ninguna termine de escribir, y ambas pueden pasar la validación de "soy mayor que el monto actual" simultáneamente. El resultado es una condición de carrera clásica que, en un sistema que maneja dinero real, no es un bug cosmético: es una puja ganadora perdida o un monto incorrecto registrado como el más alto.

Por qué "usar una transacción" no basta por sí solo

Envolver la operación en una transacción de base de datos ayuda, pero no resuelve el problema completo si dos transacciones concurrentes pueden leer el mismo estado antes de que cualquiera de las dos confirme sus cambios, dependiendo del nivel de aislamiento configurado. La solución real requiere una de dos estrategias explícitas: un bloqueo pesimista, que fuerza a la segunda puja a esperar a que la primera termine antes de siquiera leer el monto actual, o un bloqueo optimista, que permite que ambas lean el mismo estado pero rechaza y obliga a reintentar a la que llegue a escribir en segundo lugar, detectando el conflicto en el momento de guardar.

Para un sistema de pujas, donde la ventana de conflicto es breve pero el volumen de intentos puede ser alto en los últimos segundos de una subasta, la elección entre estas dos estrategias no es un detalle de implementación. Determina si el sistema se comporta de forma predecible exactamente en el momento de mayor tráfico y mayor valor de negocio: el cierre de la subasta.

El segundo problema: el tiempo también es un recurso compartido

Una subasta bien diseñada extiende automáticamente su tiempo de cierre si llega una puja en los últimos segundos, para evitar que alguien gane simplemente por cronometrar su oferta al milisegundo final. Esa extensión de tiempo es, en sí misma, otro recurso mutado concurrentemente: si dos pujas de último segundo llegan casi juntas, el sistema tiene que decidir de forma consistente cuánto se extiende el cierre, sin que ambas peticiones calculen la extensión de forma independiente y produzcan un resultado inconsistente entre los distintos clientes conectados en tiempo real.

La lección

Un sistema de pujas concentra, en un espacio de código relativamente pequeño, casi todos los retos clásicos de escritura concurrente: lecturas y escrituras que compiten por el mismo estado, la necesidad de decidir entre bloqueo pesimista y optimista según el patrón de contención esperado, y una ventana de tiempo que es, ella misma, un dato mutable bajo la misma presión concurrente. Cualquier ingeniero que haya resuelto esto correctamente una vez tiene, en la práctica, un mapa mental que se aplica directamente a inventarios limitados, reservas de asientos, o cualquier recurso escaso que múltiples usuarios intentan reclamar al mismo tiempo.