El problema fundamental que aborda este artículo es la gestión de la concurrencia y la consistencia del inventario en sistemas de comercio electrónico de alto volumen. Específicamente, cómo garantizar que múltiples compradores no adquieran la misma última unidad de un producto, evitando tanto la sobreventa (overselling) como la subventa (underselling). La solución tradicional a menudo implica sistemas de caché distribuidos o bases de datos NoSQL para manejar la alta tasa de contención.
Sin embargo, el artículo demuestra que, con un diseño de esquema adecuado y el uso de características modernas de bases de datos relacionales como SKIP LOCKED de MySQL 8, es posible consolidar la lógica de reserva y el ledger de inventario en una única base de datos transaccional. Esto simplifica la arquitectura, elimina problemas de consistencia entre sistemas distribuidos y reduce la sobrecarga operativa, desafiando la suposición de que las bases de datos relacionales no pueden manejar cargas extremas de contención en escenarios de inventario.
La relevancia actual radica en la continua búsqueda de simplicidad arquitectónica y la consolidación de datos. A medida que las bases de datos relacionales evolucionan con características como SKIP LOCKED, se vuelven más capaces de manejar patrones de carga que antes requerían soluciones distribuidas más complejas, lo que permite a los arquitectos reevaluar decisiones de diseño pasadas y optimizar el stack tecnológico.
Arquitectura del Sistema
El sistema de protección contra sobrevendidos de Shopify se rediseñó para operar completamente dentro de MySQL. La arquitectura central se basa en un modelo de 'una fila por unidad' de inventario, donde cada unidad vendible de un producto se representa como una fila individual en una tabla. Por ejemplo, un artículo con 10 unidades disponibles tiene 10 filas distintas. Esto contrasta con el modelo anterior de 'una fila por artículo' con una columna de cantidad, que sufría de alta contención en una única fila.
La operación de reserva implica seleccionar y bloquear un número específico de estas filas unitarias dentro de una transacción. La clave para la escalabilidad es el uso de SELECT ... FOR UPDATE SKIP LOCKED, que permite a MySQL omitir filas que ya están bloqueadas por otras transacciones y devolver solo las filas disponibles. Esto reduce significativamente la contención y el tiempo de espera, ya que las transacciones no se bloquean mutuamente por las mismas filas.
Para evitar que la tabla de unidades crezca excesivamente para artículos con grandes inventarios (ej. 50,000 unidades), se implementa un 'pool acotado' de filas disponibles, limitado a 1,000 unidades por combinación de artículo/ubicación. Las reservas consumen filas de este pool, y un proceso de reabastecimiento asíncrono rellena el pool desde el ledger de inventario principal. Si el pool se agota durante una venta flash extrema, el proceso de reserva activa un reabastecimiento en línea, con un bloqueo para asegurar que solo una transacción lo realice, evitando el 'thundering herd problem'. Las transacciones en espera proceden una vez que el pool se ha rellenado. La consistencia ACID se mantiene al realizar las operaciones de reserva y reclamo (deducción permanente del inventario) dentro de la misma transacción de MySQL, eliminando los problemas de consistencia que existían con el sistema Redis/MySQL anterior.
Flujo de Reserva de Inventario con SKIP LOCKED
- 1 Inicio de Pago El comprador inicia el proceso de pago.
- 2 Reserva de Unidades La aplicación ejecuta `SELECT ... FOR UPDATE SKIP LOCKED` para seleccionar un...
- 3 Movimiento de Unidades Las filas seleccionadas se mueven del pool de unidades disponibles a una tabl...
- 4 Pool Agotado? Si el pool de unidades está vacío para un ítem, se activa el reabastecimiento.
- 5 Reabastecimiento Se insertan nuevas filas en el pool desde el ledger de inventario principal. ...
- 6 Fin de Transacción La transacción de reserva se completa, marcando las unidades como reservadas.
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | MySQL 8 | Base de datos transaccional principal para el ledger de inventario y las reservas. Su característica `SKIP LOCKED` es fundamental para la gestión de concurrencia de alto volumen. vs Redis, PostgreSQL (con `FOR NO KEY UPDATE SKIP LOCKED`) Nivel de aislamiento `READ COMMITTED` para transacciones de reserva/reabastecimiento. Aumento de la concurrencia de hilos de InnoDB. |
| cache | Redis | Sistema de reservas anterior, usado para gestionar la disponibilidad de inventario. Reemplazado por MySQL para consolidar la lógica y asegurar atomicidad. |
| networking | ProxySQL | Proxy de base de datos utilizado para enrutamiento de consultas, balanceo de carga y, crucialmente, para la monitorización del uso de conexiones por proceso de negocio. vs HAProxy, Envoy Configuración para parsear tags de comentarios SQL y medir el tiempo de retención de conexión por caller. |
Trade-offs
Ganancias
- ▲ Consistencia de datos (ACID)
- ▲ Simplicidad arquitectónica
- ▲ Reducción de sobreventas/subventas
- ▲ Throughput de reservas
Costes
- △ Complejidad del diseño de esquema (una fila por unidad)
- △ Mayor uso de almacenamiento (más filas)
- △ Latencia ocasional en reservas si el pool se agota y se activa el reabastecimiento en línea
SELECT id, inventory_item_id FROM reservation_units
WHERE shop_id = ? AND inventory_item_id = ? AND inventory_group_id = ?
LIMIT ? FOR UPDATE SKIP LOCKED;(
SELECT id, inventory_item_id FROM reservation_units
WHERE shop_id = ? AND inventory_item_id = ? AND inventory_group_id = ?
LIMIT ? FOR UPDATE SKIP LOCKED
)
UNION ALL
(
SELECT id, inventory_item_id FROM reservation_units
WHERE shop_id = ? AND inventory_item_id = ? AND inventory_group_id = ?
LIMIT ? FOR UPDATE SKIP LOCKED
);/* conn_tag:checkout_completion */ SELECT * FROM orders WHERE id = ?;Fundamentos Teóricos
El problema de la gestión de inventario concurrente y la garantía de atomicidad en un entorno distribuido se relaciona directamente con los principios de las bases de datos transaccionales y la teoría de la concurrencia. El uso de SELECT ... FOR UPDATE para bloquear recursos es un patrón clásico en bases de datos relacionales para implementar exclusión mutua a nivel de fila, un concepto fundamental en el control de concurrencia.
La característica SKIP LOCKED de MySQL 8, aunque una adición relativamente reciente, aborda el problema de la contención de bloqueos de una manera que se alinea con los principios de sistemas de colas distribuidas y 'work stealing'. Al permitir que las transacciones ignoren los recursos bloqueados y procedan con los disponibles, se mejora el throughput en escenarios de alta contención, similar a cómo los algoritmos de 'lock-free' o 'wait-free' buscan minimizar los puntos de contención en estructuras de datos concurrentes. La elección del nivel de aislamiento READ COMMITTED para evitar 'gap locks' y 'supremum locks' se basa en una comprensión profunda de la semántica de bloqueo de InnoDB, un tema extensamente cubierto en la literatura académica sobre sistemas de gestión de bases de datos y control de concurrencia, como se detalla en trabajos sobre el algoritmo de bloqueo de dos fases (2PL) y sus variaciones.