Lecciones del módulo (2/4)
Condiciones de carrera
Comprender cómo funciona una base de datos de forma aislada en un único terminal es simple, pero en un escenario web real habrá cientos de procesos "Nodo/PHP/Go" simultáneos enviando solicitudes. Aquí es donde nacen las famosas Condiciones de Carrera.
El problema de la "Actualización perdida"
La actualización perdida ocurre cuando dos subprocesos leen la misma fila anterior, hacen algunos cálculos en el servidor JavaScript y luego envían la ACTUALIZACIÓN para guardar el resultado.
Guión:
- El producto #5 tiene 10 unidades.
- El usuario A compra uno (el servidor A lee "10", calcula "9").
- En el mismo milisegundo, el usuario B compra otro antes de que la compra de A finalice la fase COMMIT.
- El servidor B también lee "10" y calcula "9".
- El servidor A envía
UPDATE: set quantity = 9. - El servidor B envía
UPDATE: set quantity = 9. - Ambos creen que disminuyeron el número con éxito. ¡Pero el valor final debería haber sido 8 unidades! Se ha perdido una compra fantasma.
Solución sin transacciones pesadas (Actualizaciones atómicas)
En lugar de leer y hacer los cálculos afuera, deje que la base de datos lo haga durante la actualización, que siempre está sincronizada.
-- Wrong
UPDATE products SET stock = 9 WHERE id = 5;
-- Correct and natively thread-safe
UPDATE products SET stock = stock - 1 WHERE id = 5;Anomalías más extremas (lecturas sucias y fantasmas)
- Lectura sucia: Lees los datos temporales de otra persona que aún no han sido confirmados por un COMMIT (y de repente podrían pasar a ROLLBACK invalidándote).
- Lecturas fantasma: haces un escaneo de todas las filas, mientras un segundo hilo inserta un nuevo registro, luego haces otro escaneo durante la misma transacción y mágicamente descubres que el nuevo registro apareció de la nada, comprometiendo los informes financieros calculados.
Modifique atómicamente el registro de clientes para una pequeña manipulación de cadena. Digamos que queremos cambiar el nombre de la 'ciudad' de un cliente para agregar un asterisco. Hágalo aprovechando el valor propio de la fila para la que tiene id=10, concatenando '- Old' con el valor actual (use la tubería de concatenación || de postgres).
Mostrar pista
ACTUALIZAR clientes SET ciudad = ciudad || '- Antiguo' DONDE id = 10;
Solución disponible después de 3 intentos
Se produce una clásica pérdida de actualización en pagos y precios. Digamos que hay un aumento de precios de emergencia: aumente el 'precio unitario' de la tabla 'order_items' (para el producto 1 del pedido 1) en un 20 % de forma atómica, es decir, multiplíquelo * 1,2 en lugar de insertar el nuevo precio calculado previamente manualmente en JS.
Mostrar pista
ACTUALIZAR artículos_pedido SET precio_unidad = precio_unidad * 1.2 DONDE id_pedido = 1 Y id_producto = 1;
Solución disponible después de 3 intentos