Niveles de aislamiento

El nivel de aislamiento determina cómo las transacciones concurrentes interactúan entre sí. MySQL soporta los cuatro niveles definidos por el estándar SQL.

Problemas de concurrencia

Cuando múltiples transacciones se ejecutan simultáneamente, pueden ocurrir estos problemas:

ProblemaDescripción
Lectura sucia (dirty read)Leer datos que otra transacción aún no ha confirmado
Lectura no repetible (non-repeatable read)Obtener resultados diferentes al leer la misma fila dos veces
Lectura fantasma (phantom read)Obtener filas diferentes al ejecutar la misma consulta dos veces

Los cuatro niveles

NivelLectura suciaLectura no repetibleLectura fantasma
READ UNCOMMITTEDPosiblePosiblePosible
READ COMMITTEDNoPosiblePosible
REPEATABLE READNoNoPosible
SERIALIZABLENoNoNo

El nivel predeterminado en MySQL (InnoDB) es REPEATABLE READ.

Ver y cambiar el nivel de aislamiento

-- Ver el nivel actual
SELECT @@transaction_isolation;
@@transaction_isolation
REPEATABLE-READ
-- Cambiar para la próxima transacción
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
 
-- Cambiar para la sesión
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
 
-- Cambiar globalmente (requiere privilegios)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

READ UNCOMMITTED

El nivel más permisivo. Permite lecturas sucias: una transacción puede ver cambios no confirmados de otra.

Conexión 1:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
 
START TRANSACTION;
UPDATE productos SET precio = 1.00 WHERE id = 1;
-- No hacemos COMMIT todavía

Conexión 2:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
 
START TRANSACTION;
-- Lee el precio NO confirmado (lectura sucia)
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1.00 (dato no confirmado)
COMMIT;

Conexión 1:

-- Revierte el cambio
ROLLBACK;
-- La conexión 2 leyó un dato que nunca existió realmente

Este nivel es peligroso y rara vez se usa en producción.

READ COMMITTED

Cada lectura ve solo datos confirmados. Pero si otra transacción confirma cambios durante tu transacción, los verás.

Conexión 1:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
 
START TRANSACTION;
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1299.99

Conexión 2:

START TRANSACTION;
UPDATE productos SET precio = 1399.99 WHERE id = 1;
COMMIT;

Conexión 1:

-- La misma consulta ahora devuelve un valor diferente
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1399.99 (lectura no repetible)
COMMIT;
-- Restaurar
UPDATE productos SET precio = 1299.99 WHERE id = 1;

REPEATABLE READ (predeterminado)

Las lecturas dentro de una transacción siempre devuelven los mismos datos, independientemente de los cambios que otras transacciones confirmen.

Conexión 1:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
 
START TRANSACTION;
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1299.99

Conexión 2:

START TRANSACTION;
UPDATE productos SET precio = 1499.99 WHERE id = 1;
COMMIT;

Conexión 1:

-- La misma consulta devuelve el mismo resultado
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1299.99 (lectura repetible)
COMMIT;
 
-- Fuera de la transacción, se ve el cambio
SELECT precio FROM productos WHERE id = 1;
-- Resultado: 1499.99
-- Restaurar
UPDATE productos SET precio = 1299.99 WHERE id = 1;

Lecturas fantasma en REPEATABLE READ

InnoDB usa bloqueos de rango (next-key locking) para prevenir la mayoría de lecturas fantasma, incluso en REPEATABLE READ:

Conexión 1:

START TRANSACTION;
SELECT COUNT(*) FROM productos WHERE categoria_id = 1;
-- Resultado: 5

Conexión 2:

START TRANSACTION;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nuevo fantasma', 100.00, 1, 1);
COMMIT;

Conexión 1:

-- InnoDB previene el fantasma con su snapshot
SELECT COUNT(*) FROM productos WHERE categoria_id = 1;
-- Resultado: 5 (no ve la nueva fila)
COMMIT;
DELETE FROM productos WHERE nombre = 'Nuevo fantasma';

SERIALIZABLE

El nivel más estricto. Las transacciones se ejecutan como si fueran secuenciales. Todas las lecturas se convierten en SELECT ... FOR SHARE.

Conexión 1:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
 
START TRANSACTION;
SELECT * FROM productos WHERE categoria_id = 1;
-- InnoDB coloca un bloqueo compartido en las filas leídas

Conexión 2:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
 
START TRANSACTION;
-- Esta operación se BLOQUEA hasta que la conexión 1 termine
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Serial test', 100.00, 1, 1);
-- Espera... (puede generar timeout)

Conexión 1:

COMMIT;
-- Ahora la conexión 2 puede continuar

Comparación de rendimiento

NivelConcurrenciaConsistenciaBloqueos
READ UNCOMMITTEDAltaBajaMínimos
READ COMMITTEDMedia-AltaMediaPocos
REPEATABLE READMediaAltaModerados
SERIALIZABLEBajaMáximaMáximos

Cuándo usar cada nivel

NivelCaso de uso
READ UNCOMMITTEDReportes aproximados donde la velocidad importa más que la precisión
READ COMMITTEDAplicaciones web típicas con mucha lectura y escritura
REPEATABLE READPredeterminado de MySQL, adecuado para la mayoría de aplicaciones
SERIALIZABLEOperaciones financieras críticas donde la consistencia es absoluta

Bloqueos de fila: FOR UPDATE y FOR SHARE

Dentro de una transacción, puedes solicitar bloqueos explícitos:

-- FOR UPDATE: bloqueo exclusivo (nadie más puede leer ni escribir)
START TRANSACTION;
SELECT stock FROM productos WHERE id = 1 FOR UPDATE;
-- Otras transacciones no pueden modificar ni bloquear esta fila
UPDATE productos SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- FOR SHARE: bloqueo compartido (otros pueden leer pero no escribir)
START TRANSACTION;
SELECT stock FROM productos WHERE id = 1 FOR SHARE;
-- Otras transacciones pueden leer pero no modificar
COMMIT;
Tipo de bloqueoPermite lecturaPermite escrituraUso
FOR SHARENoVerificar que un dato no cambie
FOR UPDATENoNoModificar el dato después de leerlo

Deadlocks

Cuando dos transacciones se bloquean mutuamente, MySQL detecta el deadlock y revierte automáticamente una de ellas:

Conexión 1:

START TRANSACTION;
UPDATE productos SET stock = stock - 1 WHERE id = 1;
-- Tiene bloqueo en producto 1

Conexión 2:

START TRANSACTION;
UPDATE productos SET stock = stock - 1 WHERE id = 2;
-- Tiene bloqueo en producto 2
UPDATE productos SET stock = stock - 1 WHERE id = 1;
-- Espera el bloqueo de la conexión 1...

Conexión 1:

UPDATE productos SET stock = stock - 1 WHERE id = 2;
-- Espera el bloqueo de la conexión 2...
-- DEADLOCK detectado: MySQL revierte una transacción
-- Error 1213: Deadlock found when trying to get lock

Para prevenir deadlocks, accede siempre a las tablas y filas en el mismo orden.

Restaurar nivel predeterminado

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
 
-- Verificar
SELECT @@transaction_isolation;
@@transaction_isolation
REPEATABLE-READ

En el siguiente artículo veremos el bloqueo de tablas (TABLE LOCKING).

Escrito por Eduardo Lázaro