Imagina una transferencia bancaria: se descuenta dinero de la cuenta de origen y se acredita en la cuenta destino. Son dos operaciones que deben ocurrir juntas o no ocurrir ninguna. Si el servidor cae entre la primera y la segunda, el dinero desaparece. Las transacciones de base de datos existen exactamente para evitar este tipo de escenario: garantizan que un grupo de operaciones se ejecute como una unidad indivisible.
Este artículo, publicado originalmente en 2003 por el equipo de MySQL Hispano y actualizado por David Carrero en 2026, explica cómo funcionan las transacciones en MySQL 8, qué son las propiedades ACID, cómo manejar niveles de aislamiento y deadlocks, y cómo usarlas desde PHP 8 con PDO.
Las propiedades ACID
Una transacción correctamente implementada debe cumplir cuatro propiedades, conocidas por el acrónimo ACID:
Propiedad | Significado | Ejemplo práctico |
Atomicidad | Todo o nada: o todas las operaciones se completan o ninguna tiene efecto. | La transferencia bancaria: se resta y se suma, o no pasa nada. |
Consistencia | La base de datos pasa de un estado válido a otro estado válido. Las restricciones de integridad se respetan siempre. | Un pedido no puede tener un id_cliente que no exista en la tabla clientes. |
Aislamiento | Las transacciones concurrentes no se interfieren entre sí. Una transacción no ve los cambios sin confirmar de otra. | Dos usuarios editando el mismo registro simultáneamente no se pisan. |
Durabilidad | Una vez hecho el COMMIT, los cambios son permanentes aunque el servidor se caiga inmediatamente después. | El pedido confirmado no desaparece si hay un corte de luz. |
InnoDB, el motor por defecto de MySQL desde la versión 5.5, implementa las cuatro propiedades ACID.
InnoDB: el motor transaccional por defecto
El artículo original de 2003 tenía que especificar TYPE = InnoDB porque MyISAM era el motor por defecto y InnoDB acababa de incluirse en la instalación estándar de MySQL 4.0. Hoy la situación es la contraria: InnoDB es el motor por defecto desde MySQL 5.5 y MyISAM no tiene soporte transaccional.
Una advertencia sobre sintaxis: TYPE = InnoDB quedó obsoleto en MySQL 5.5 y desapareció en MySQL 5.7. La sintaxis correcta desde entonces es ENGINE = InnoDB:
-- Sintaxis antigua (MySQL 4.0 - 5.1, ya no funciona en 5.7+) CREATE TABLE ventas (...) TYPE = InnoDB; -- Sintaxis correcta desde MySQL 5.5 CREATE TABLE ventas ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, producto VARCHAR(100) NOT NULL, cantidad SMALLINT UNSIGNED NOT NULL, importe DECIMAL(10, 2) NOT NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
START TRANSACTION, COMMIT y ROLLBACK
Los tres comandos básicos de las transacciones no han cambiado desde 2003:
START TRANSACTION; UPDATE cuentas SET saldo = saldo - 500.00 WHERE id_cuenta = 1; UPDATE cuentas SET saldo = saldo + 500.00 WHERE id_cuenta = 2; COMMIT; -- Confirma. Los cambios son permanentes.
Si algo va mal antes del COMMIT:
START TRANSACTION; UPDATE cuentas SET saldo = saldo - 500.00 WHERE id_cuenta = 1; -- Error detectado en la lógica de negocio ROLLBACK; -- Deshace todo. El saldo queda intacto.
Nota: BEGIN es un sinónimo de START TRANSACTION en MySQL, pero START TRANSACTION es el estándar SQL y permite modificadores como START TRANSACTION READ ONLY.
AUTOCOMMIT: el modo silencioso
Por defecto MySQL opera en modo AUTOCOMMIT = ON: cada sentencia SQL es una transacción implícita que se confirma al terminar. Si quieres control manual debes o bien usar START TRANSACTION (que suspende AUTOCOMMIT para esa transacción) o bien desactivarlo:
SET autocommit = 0; -- Desactiva AUTOCOMMIT para la sesión actual
Con AUTOCOMMIT desactivado, todas las sentencias quedan pendientes hasta que hagas COMMIT o ROLLBACK explícitamente. Úsalo con cuidado en conexiones de larga duración.
SAVEPOINT: puntos de retorno parciales
En transacciones largas a veces no quieres deshacer todo, solo la última parte. Los SAVEPOINTs permiten marcar un punto de restauración dentro de una transacción activa:
START TRANSACTION; INSERT INTO pedidos (id_cliente, fecha) VALUES (42, NOW()); SAVEPOINT pedido_creado; INSERT INTO lineas_pedido (id_pedido, id_producto, cantidad) VALUES (LAST_INSERT_ID(), 7, 2); -- Supongamos que el producto 7 no tiene stock ROLLBACK TO SAVEPOINT pedido_creado; -- Deshace solo la línea de pedido -- La cabecera del pedido sigue ahí COMMIT;
Lecturas consistentes e InnoDB MVCC
InnoDB implementa MVCC (Multi-Version Concurrency Control): cuando una transacción lee datos, MySQL no le muestra los cambios de otras transacciones que aún no hayan hecho COMMIT. Esta es la «lectura consistente» que el artículo original demostraba con dos conexiones simultáneas al servidor.
El comportamiento exacto depende del nivel de aislamiento configurado.
Niveles de aislamiento
MySQL soporta los cuatro niveles definidos por el estándar SQL:
Nivel | Lecturas sucias | Lecturas no repetibles | Lecturas fantasma |
READ UNCOMMITTED | Posibles | Posibles | Posibles |
READ COMMITTED | No | Posibles | Posibles |
REPEATABLE READ (por defecto) | No | No | En InnoDB: no (gap locks) |
SERIALIZABLE | No | No | No |
El nivel por defecto de MySQL/InnoDB es REPEATABLE READ, que es un buen equilibrio entre consistencia y rendimiento para la mayoría de aplicaciones web. Para cambiarlo:
-- Solo para la sesión actual SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- O globalmente (requiere reinicio o SET PERSIST) SET PERSIST transaction_isolation = 'READ-COMMITTED';
Deadlocks: cuando dos transacciones se bloquean mutuamente
Un deadlock ocurre cuando la transacción A espera un bloqueo que tiene la transacción B, y B espera un bloqueo que tiene A. MySQL detecta automáticamente este estado y mata a una de las dos transacciones (la que tiene menos registros bloqueados), devolviendo el error 1213.
Para minimizar deadlocks:
- Accede siempre a las tablas en el mismo orden en todas tus transacciones.
- Mantén las transacciones lo más cortas posible.
- Usa
SELECT ... FOR UPDATEsolo cuando de verdad vayas a modificar esas filas. - Añade índices en las columnas usadas en las cláusulas WHERE de los UPDATE/DELETE para reducir el alcance de los bloqueos.
-- Ver el último deadlock detectado por InnoDB SHOW ENGINE INNODB STATUSG
Transacciones en PHP 8 con PDO
En PHP 8 la forma correcta de manejar transacciones es con PDO. El método beginTransaction() desactiva AUTOCOMMIT para esa conexión:
<?php
$dsn = 'mysql:host=localhost;dbname=banco;charset=utf8mb4';
$pdo = new PDO($dsn, 'usuario', 'contraseña', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'UPDATE cuentas SET saldo = saldo - ? WHERE id_cuenta = ?'
);
$stmt->execute([500.00, 1]);
$stmt = $pdo->prepare(
'UPDATE cuentas SET saldo = saldo + ? WHERE id_cuenta = ?'
);
$stmt->execute([500.00, 2]);
$pdo->commit();
echo "Transferencia realizada.n";
} catch (PDOException $e) {
$pdo->rollBack();
echo "Error: " . $e->getMessage() . "n";
}
El bloque try/catch garantiza que cualquier excepción de PDO (incluidos los errores de constraints o deadlocks) provoque el ROLLBACK automático. Nunca hagas COMMIT dentro de una transacción sin que antes el código de negocio haya validado que todo ha ido bien.
Verificar soporte InnoDB en MySQL 8
En MySQL 8 InnoDB es el único motor transaccional general disponible. Para confirmarlo:
SHOW ENGINESG
Verás InnoDB listado como «DEFAULT» con «Transactions: YES». La variable have_innodb del artículo original ya no existe en MySQL 8: InnoDB es parte inseparable del servidor.
Imagen: Pexels / Towfiqu barbhuiya
