Ir al contenido
wannacode

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 · En líneabase de datos: tienda
  1. Caja

    SELECT stock FROM products WHERE id = 17;

    La caja de la tienda lee las existencias de café: 15.

  2. En línea

    SELECT stock FROM products WHERE id = 17;

    En el mismo segundo un pedido en línea lee lo mismo: 15.

  3. Caja

    UPDATE products SET stock = 14 WHERE id = 17;

    La caja vende 1 paquete y escribe 15 − 1 = 14.

  4. 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.

  5. 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 · En líneabase de datos: tienda
  1. 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.

  2. 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.

  3. 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 · En líneabase de datos: tienda
  1. 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.

  2. 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.

  3. 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.

SQLbase de datos: tienda

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

1. ¿Por qué UPDATE products SET stock = stock - 1 no pierde ventas, y «leer, calcular en el programa, escribir» sí?
2. ¿Qué es una «lectura sucia» (dirty read)?
3. Dos transacciones se esperan una a la otra: la primera tiene la fila 1 y quiere la fila 2, la segunda al revés. ¿Qué hace la base de datos?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·