Transacciones simultáneas
En resumen: Cuando dos transacciones leen y luego escriben los mismos datos, una puede sobrescribir el cambio de la otra (una actualización perdida); un único UPDATE condicional o un bloqueo de fila lo evita.
Problema
Los últimos paquetes de café
Hay 15 paquetes de café en grano. La caja de la tienda y la tienda en línea venden un paquete cada una en el mismo segundo. Los dos programas hacen lo mismo: leer las existencias, restar 1 y escribir el número nuevo.
Ejecuta los pasos uno a uno. Cada consulta se ejecuta de verdad, sobre una base de datos compartida:
Caja
SELECT stock FROM products WHERE id = 17;La caja de la tienda lee las existencias de café: 15.
En línea
SELECT stock FROM products WHERE id = 17;En el mismo segundo un pedido en línea lee lo mismo: 15.
Caja
UPDATE products SET stock = 14 WHERE id = 17;La caja vende 1 paquete y escribe 15 − 1 = 14.
En línea
UPDATE products SET stock = 14 WHERE id = 17;El pedido en línea también vende 1 y también escribe 14. No sabe nada de la caja.
Caja
SELECT stock FROM products WHERE id = 17;Se vendieron 2 paquetes, pero las existencias bajaron solo 1. Un cambio se perdió.
La idea
Una actualización perdida
Esto es una actualización perdida (lost update). Cada consulta por separado es correcta. La culpa es del hueco entre la lectura y la escritura: durante él el otro programa escribió algo que después se sobrescribió.
La protección más simple es no tener hueco. Deja que la base de datos lea y escriba ella misma, en una sola consulta:
Caja
UPDATE products SET stock = stock - 1 WHERE id = 17 AND stock >= 1;La caja le dice a la base de datos «quita 1, si hay». Leer y escribir son una sola consulta.
En línea
UPDATE products SET stock = stock - 1 WHERE id = 17 AND stock >= 1;El pedido en línea hace lo mismo. La base de datos ejecuta las dos una tras otra, cada una sobre el valor actual real.
Caja
SELECT stock FROM products WHERE id = 17;15 − 2 = 13. No se pierde nada.
La condición stock >= 1 también protege de vender algo que no hay. Cuántas filas cambió la consulta le dice al programa si vendió:
Caja
UPDATE products SET stock = stock - 5 WHERE id = 23 AND stock >= 5;El juego de cuchillos: hay 5. La caja vende los 5 de una vez (un regalo de empresa): 1 fila modificada.
En línea
UPDATE products SET stock = stock - 5 WHERE id = 23 AND stock >= 5;El pedido en línea quiere 5. La condición ya no se cumple: 0 filas modificadas. El programa le dice al cliente «sin existencias» en lugar de vender cuchillos que no existen.
En línea
SELECT stock FROM products WHERE id = 23;Las existencias no bajaron de cero.
Pruébalo
Tres paquetes, si hay tres
Escribe una consulta que venda 3 paquetes de café (producto 17) solo si hay al menos 3.
Solución
UPDATE products SET stock = stock - 3
WHERE id = 17 AND stock >= 3;UPDATE products SET stock = stock - 3 WHERE id = 17 AND stock >= 3; El café baja a 12. Si hubiera 2, la consulta no cambiaría nada, y el programa lo sabría por el número de filas modificadas.
La idea
Niveles de aislamiento
No todo cabe en una sola consulta. Por eso las bases de datos mantienen las transacciones separadas unas de otras, por niveles. Cuanto más alto el nivel, menos cosas raras ves, pero más se espera:
| Nivel | Lectura sucia | Lectura no repetible | Fantasmas |
|---|---|---|---|
| READ UNCOMMITTED | posible | posible | posible |
| READ COMMITTED | no | posible | posible |
| REPEATABLE READ | no | no | posible |
| SERIALIZABLE | no | no | no |
- Lectura sucia (dirty read): ves un cambio que la otra transacción todavía no ha confirmado (y que quizá deshaga).
- Lectura no repetible (non-repeatable read): lees una fila dos veces en tu transacción y obtienes valores distintos, porque alguien la cambió entre medias.
- Fantasma (phantom): repites una consulta con una condición y aparecen filas nuevas que alguien añadió.
PostgreSQL trabaja por defecto con READ COMMITTED, MySQL (InnoDB) con REPEATABLE READ. SQLite es el más estricto: solo una transacción escribe a la vez, y las demás esperan.
Más precisamente, para curiosos
La tabla viene del estándar. Las bases de datos reales hacen más de lo que exige: en PostgreSQL READ UNCOMMITTED se comporta como READ COMMITTED, y REPEATABLE READ tampoco permite fantasmas.
La idea
Bloqueos y deadlock
Para cumplir estas promesas, la base de datos bloquea: una transacción que cambia una fila la retiene hasta que dice COMMIT o ROLLBACK. Quien quiera la misma fila, espera.
A veces dos se esperan una a la otra: la primera transacción tiene el café y quiere el té, la segunda tiene el té y quiere el café. Esto es un deadlock (interbloqueo). La base de datos lo detecta y detiene una de las dos con un error. El programa debe estar preparado para reintentar.
Cómo hacerlo menos frecuente: transacciones cortas (sin esperar a una persona a mitad de camino) y todos los programas bloqueando en el mismo orden, por ejemplo por id de producto creciente.
Más precisamente, para curiosos
En PostgreSQL y MySQL, SELECT … FOR UPDATE bloquea de inmediato las filas que lee, de modo que «leer, calcular, escribir» dentro de una transacción pasa a ser seguro. SQLite no tiene FOR UPDATE. Allí, BEGIN IMMEDIATE bloquea toda la base de datos para escritura al inicio de la transacción.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: WHERE: filtrar filas · UPDATE: cambiar valores · ACID: todo o nada · BEGIN, COMMIT, ROLLBACK