Transacciones y ACID
Una transacción convierte varias operaciones en una sola que pasa entera o no pasa. Las cuatro letras de ACID prometen cosas distintas, y la de aislamiento es la que casi nunca está donde uno cree.
Para este tema conviene tener claro:Tablas, claves e integridad referencial
Una transferencia son dos operaciones: restar de una cuenta y sumar a la otra. Si el servidor se apaga en el medio, el dinero no puede desaparecer. Ese requisito, llevado al caso general, es lo que resuelve una transacción.
Atomicidad: todo o nada
Atomicidad: todo o nada. Las operaciones entre el inicio y el COMMIT se aplican juntas; si
algo falla, el ROLLBACK deja la base como si nada hubiera pasado.
Se implementa con un registro previo de escritura: antes de tocar los datos, el motor anota en un log qué va a cambiar. Si el sistema cae, al levantar rehace lo confirmado y deshace lo que quedó a medias. El log es también lo que hace posible restaurar a un punto en el tiempo.
Empieza una transferencia de 100 de la cuenta A a la B. Lo primero que se escribe es que arrancó.
Antes de seguir, predecí
Consistencia y durabilidad
Consistencia: una transacción lleva la base de un estado válido a otro, respetando claves,
foráneas y CHECK. Es la letra más discutida, porque en buena medida depende de que las
restricciones estén declaradas: el motor hace cumplir lo que se le dijo.
Durabilidad: una vez confirmada, la transacción sobrevive a un corte de energía. Se logra
escribiendo el log a disco antes de responder, y de ahí sale la tensión con el rendimiento: hacer
fsync en cada commit es lento, y la mayoría de los motores ofrecen aflojar esa garantía a cambio
de velocidad. Es una decisión, no un detalle de configuración.
Aislamiento, que es la letra difícil
Aislamiento: el resultado de ejecutar transacciones concurrentes debería ser el mismo que ejecutarlas una detrás de otra. Ésa es la definición fuerte, y es la que casi ningún sistema aplica por defecto, porque cuesta cara.
En su lugar hay niveles, y elegir uno es elegir qué anomalías se aceptan. La mayoría de las bases viene configurada en un nivel intermedio, y muchísimo código asume el nivel más fuerte sin saberlo.
Las tres anomalías clásicas
Las anomalías clásicas son tres. Lectura sucia: ver datos de una transacción que todavía no confirmó. Lectura no repetible: leer dos veces la misma fila dentro de una transacción y obtener valores distintos. Lectura fantasma: repetir una consulta por rango y encontrar filas nuevas.
A eso se agrega una cuarta que no está en la lista original y es la que más daño hace en la práctica: la actualización perdida. Dos transacciones leen el mismo saldo, cada una calcula el nuevo valor y escribe; la segunda pisa a la primera y un movimiento desaparece.
Los cuatro niveles estándar
Los niveles estándar son cuatro. READ UNCOMMITTED permite todo. READ COMMITTED evita las
lecturas sucias y es el default de PostgreSQL y Oracle. REPEATABLE READ además garantiza que lo
leído no cambie, y es el default de MySQL. SERIALIZABLE no admite ninguna anomalía.
Conviene saber el default del motor que se usa, porque el código se escribe implícitamente contra
uno. Y hay un detalle importante: en READ COMMITTED, leer un valor, calcular y escribirlo no
es seguro. Hay que releer con bloqueo —SELECT ... FOR UPDATE—, hacer la actualización en una sola
sentencia, o usar control optimista con un número de versión.
Dos reglas prácticas
Dos reglas prácticas. Las transacciones cortas: cuanto más tiempo abiertas, más bloqueos retienen y más conflictos generan. Nunca abarcar una llamada a un servicio externo dentro de una transacción abierta.
Y asumir que pueden fallar. Un interbloqueo hace que el motor cancele una de las dos transacciones, y eso no es un error excepcional: es el mecanismo funcionando. El código tiene que poder reintentar, y para eso la operación tiene que ser segura de repetir.
Una transferencia, escrita bien
BEGIN;
-- Bloquea las dos filas, siempre en el mismo orden para no trabar con otra transacción
SELECT balance FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- La regla de negocio, verificada dentro de la transacción
-- (con un CHECK (balance >= 0) el motor lo hace solo)
COMMIT;Tres decisiones en cinco líneas. La primera: FOR UPDATE bloquea las filas para que ninguna otra
transacción las lea con intención de modificarlas hasta el COMMIT. Sin eso, dos transferencias
simultáneas pueden leer el mismo saldo y las dos restar sobre el valor viejo.
La segunda: el ORDER BY id no es cosmético. Si una transacción toma la 1 y después la 2 mientras
otra toma la 2 y después la 1, quedan trabadas para siempre. Tomar los bloqueos siempre en el
mismo orden global es la forma más barata de hacer imposible un interbloqueo.
La tercera: escribir balance = balance - 100 en vez de leer en la aplicación y escribir el
resultado. La resta la hace el motor sobre el valor actual, así que no hay ventana entre la lectura
y la escritura.
Lo que ACID cuesta, y qué se afloja
| Letra | Qué garantiza | Con qué se paga |
|---|---|---|
| Atomicidad | todo o nada | un registro previo de escritura |
| Consistencia | las restricciones se cumplen | verificarlas en cada escritura |
| Aislamiento | como si corriera sola | bloqueos y espera, o versiones y memoria |
| Durabilidad | sobrevive a un corte | un fsync antes de responder |
Cierre
Atomicidad por el log, durabilidad por escribirlo antes de confirmar, consistencia por las restricciones declaradas. El aislamiento es el que viene recortado por defecto: conviene saber en qué nivel corre la base y no dar por sentado que leer, calcular y escribir es seguro.
Autoevaluación
¿Lo entendiste?
Práctica