Read in English · Lire en français · Auf Deutsch lesen
Sección 7.3 de 7.8 · Avanzado · 28 min de lectura
Transacciones, ACID y niveles de aislamiento
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Los bugs de concurrencia pasan todas las pruebas en una máquina tranquila y luego aparecen en el pico de la hora de la cena. En esta lección aprenderás qué garantizan realmente las transacciones cuando se ejecutan muchas a la vez, y cómo proteger tus reglas.
Imagina a dos cajeros que comparten un único libro de cuentas en papel para la cuenta de un cliente. Cada uno consulta el saldo, calcula el nuevo en un bloc de notas y lo anota. Si lo hacen en el mismo momento, se pierde el cambio de uno de los cajeros. Una transacción viene con normas sobre cómo se turnan los cajeros: si pueden mirar las anotaciones a medio escribir del otro, y si uno debe esperar. Normas más estrictas significan menos errores, pero más espera. Y, como verás, los nombres de estas normas significan cosas ligeramente distintas en bases de datos distintas.
Una transacción agrupa lecturas y escrituras en una unidad, y probablemente hayas oído el acrónimo ACID. La atomicidad tiene que ver con los fallos: si algo sale mal a mitad de camino, se deshace cada cambio, así que puedes reintentar con seguridad. La consistencia significa que se cumplen tus reglas, como que los cargos igualen a los abonos. La base de datos hace cumplir las claves y las restricciones, pero la mayoría de las reglas de negocio solo las mantienen tus propias transacciones. El aislamiento significa que las transacciones concurrentes no interfieren entre sí. En su forma más estricta, serializable, el resultado es como si se hubieran ejecutado de una en una. Los niveles más débiles permiten cierta interferencia, a cambio de velocidad. Y la durabilidad significa que, una vez confirmado un commit, los datos sobreviven a una caída.
El bug de concurrencia más común en el código de negocio es la actualización perdida. Aquí, dos retiradas afectan a una cuenta que tiene cien libras. La primera transacción, que retira treinta, lee el saldo y ve cien. Instantes después, la segunda, que retira cincuenta, también lee cien. La primera escribe setenta: cien menos treinta. Después la segunda escribe cincuenta, cien menos cincuenta, y sobrescribe en silencio la primera escritura. El saldo debería ser veinte. Se ha perdido una retirada, aunque cada transacción era correcta por sí sola.
La mayoría de las bases de datos modernas dan a cada transacción una instantánea coherente mediante el control de concurrencia multiversión. En lugar de sobrescribir una fila, cada escritura crea una nueva versión, etiquetada con el número de la transacción que la escribió. Una instantánea tomada en la transacción ciento cinco ve la versión más reciente confirmada antes de ese momento: un saldo de setenta. Una instantánea posterior, en la ciento veinte, ve veinte. Así, los lectores nunca esperan a los escritores, y los escritores nunca esperan a los lectores. El costo es el mantenimiento: una transacción que se deja abierta durante horas mantiene vivas las versiones antiguas, y el almacenamiento se infla.
Leer de una instantánea se llama aislamiento de instantánea. Evita las actualizaciones perdidas, pero permite una anomalía más sutil llamada write skew, así que no es totalmente serializable. Los nombres de los niveles de aislamiento no son portables, y los valores por defecto difieren. PostgreSQL y Oracle usan por defecto read committed, MySQL repeatable read y CockroachDB serializable. Peor aún, repeatable read significa cosas distintas en PostgreSQL y en MySQL. El de MySQL todavía puede perder una actualización escrita como una lectura seguida de una escritura. Y el serializable de Oracle es en realidad aislamiento de instantánea. Un código que es correcto en una base de datos puede ser incorrecto en otra con un ajuste de nombre idéntico.
Veamos el write skew en acción. El sistema de turnos de un hospital tiene una regla: al menos un médico debe estar de guardia. Alice y Bob están de guardia, y los dos pulsan salir de guardia casi en el mismo momento. Cada solicitud cuenta los médicos de guardia. Si hay al menos dos, pone su propia fila como fuera de guardia. Con aislamiento de instantánea, ambas instantáneas muestran dos médicos de guardia, así que las dos comprobaciones se superan. La transacción de Alice actualiza la fila de Alice, y la de Bob actualiza la de Bob. Son filas distintas, así que no se detecta ningún conflicto. Ambas confirman, y nadie queda de guardia. Con aislamiento serializable, la base de datos detecta que cada transacción se basó en datos que la otra modificó después. Un commit falla, y el reintento vuelve a contar, encuentra un solo médico y se niega. En un nivel más débil, puedes bloquear las filas de las que depende la decisión con select for update. Ahora la solicitud de Bob espera a que termine la de Alice.
Todo código que lee un valor, decide en la aplicación y vuelve a escribir está en riesgo. Nombra la regla que protege, y deja que las restricciones o una única actualización condicional hagan el trabajo siempre que sea posible. El bloqueo pesimista bloquea las filas que lees hasta que confirmas. Se adapta a operaciones cortas y concurridas, como asignar un repartidor a un pedido, a costa de esperas y posibles interbloqueos. El control optimista no toma ningún bloqueo. Cada fila lleva un número de versión, y la escritura solo tiene éxito si la versión no ha cambiado. Se adapta a un gerente que edita una carta en un navegador durante cinco minutos. O usa aislamiento serializable y reintenta la transacción completa cuando falle, así que deja los correos electrónicos y las llamadas de pago fuera de ella. Los profesionales discrepan sobre cuál es el mejor valor por defecto; lo que importa es una elección deliberada y probada.
La sección completa incluye todas las anomalías, una comparación de bases de datos reales y una lista de comprobación. A continuación, en la Sección siete punto cuatro, veremos la replicación y el particionamiento. Nos vemos allí.
Lo que necesitas saber
- ACID: la atomicidad significa todo o nada ante un fallo; la consistencia significa que se cumplen las invariantes de la aplicación; el aislamiento limita la interferencia entre transacciones concurrentes; la durabilidad significa que los datos confirmados sobreviven a las caídas, normalmente gracias a un registro de escritura anticipada y a réplicas.
- El estándar SQL define read uncommitted, read committed, repeatable read y serializable según las lecturas sucias, las lecturas no repetibles y los fantasmas que prohíben; Berenson et al. (1995) demostraron que estas definiciones son ambiguas e incompletas.
- Las actualizaciones perdidas y el write skew surgen cuando las transacciones leen datos, deciden en la aplicación y vuelven a escribir; el aislamiento de instantánea evita las actualizaciones perdidas, pero permite el write skew.
- MVCC mantiene varias versiones de cada fila para que lectores y escritores no se bloqueen entre sí; las transacciones de larga duración inflan el almacenamiento.
- Los valores por defecto y los nombres difieren: PostgreSQL y Oracle usan por defecto read committed, MySQL InnoDB repeatable read y CockroachDB serializable; el serializable de Oracle es aislamiento de instantánea.
- Protege las rutas de lectura-modificación-escritura con restricciones, actualizaciones condicionales atómicas, SELECT ... FOR UPDATE (pesimista) o columnas de versión (optimista), o usa aislamiento serializable con reintentos de la transacción completa.
Lo que promete una transacción
Una transacción agrupa varias lecturas y escrituras en una unidad que o bien surte efecto por completo (se confirma) o bien no surte ningún efecto (se aborta y se revierte). El Capítulo 0 presentó la idea con una transferencia bancaria. Esta sección trata de la parte que la mayoría de los desarrolladores entiende mal: qué ocurre cuando se ejecutan muchas transacciones a la vez, qué garantías dan realmente las bases de datos y cómo elegir. Los ejemplos usan SQL, pero el razonamiento se aplica a cualquier almacén que ofrezca transacciones.
Las transacciones importan para la arquitectura porque deciden dónde se pueden hacer cumplir las reglas de negocio de forma barata. Dentro de una base de datos, una transacción puede mantener cuadrado un libro de cuentas o impedir que una tabla se reserve dos veces con unas pocas líneas de código. Entre servicios no existe esa herramienta, y por eso la propiedad de los datos y las sagas son tan importantes. Saber exactamente qué garantiza una sola base de datos te dice qué reglas deben quedar dentro de los límites de un único propietario.
ACID en detalle
El término ACID lo acuñaron Theo Härder y Andreas Reuter en 1983, a partir del trabajo anterior de Jim Gray. Cada letra es más restringida de lo que parece. La atomicidad tiene que ver con los fallos, no con la concurrencia: si algo sale mal a mitad de camino (una caída, la violación de una restricción, un interbloqueo, la aplicación que decide abortar), se deshace cada cambio hecho hasta ese momento, de modo que la aplicación puede reintentar con seguridad. Abortabilidad sería un nombre más preciso.
La consistencia significa que una transacción lleva la base de datos de un estado válido a otro, donde válido
significa que se cumplen tus invariantes: los cargos igualan a los abonos, el stock nunca es negativo, todo pedido tiene un cliente. La base de datos puede hacer cumplir algunas invariantes por sí misma (claves, claves foráneas, restricciones CHECK, índices únicos), pero la mayoría son reglas de negocio que solo mantienen las transacciones de la aplicación. Por tanto, la consistencia es una propiedad del uso que hace la aplicación de la atomicidad y el aislamiento, no algo que la base de datos proporcione por sí sola. Tampoco tiene relación con la C
de CAP, que significa linealizabilidad (Capítulo 6).
El aislamiento significa que las transacciones que se ejecutan de forma concurrente no interfieren entre sí. En su forma más estricta, la serializabilidad, el resultado de ejecutar transacciones de forma concurrente es el mismo que si se hubieran ejecutado de una en una en algún orden. Los niveles más débiles permiten cierta interferencia a cambio de rendimiento, y el resto de esta sección trata precisamente de qué interferencia. La durabilidad significa que, una vez confirmado un commit, los datos sobreviven a las caídas. Las bases de datos de un solo nodo lo consiguen con un registro de escritura anticipada (write-ahead log) que se vuelca al disco antes de que el commit retorne; las bases de datos replicadas añaden copias en otras máquinas. La durabilidad nunca es absoluta: un disco puede mentir sobre un volcado, y la replicación asíncrona puede perder las últimas escrituras confirmadas cuando falla el líder (consulta replicación).
Lo que sale mal sin aislamiento
Los bugs de concurrencia son difíciles de encontrar porque dependen del momento exacto: pasan todas las pruebas en una tranquila máquina de desarrollo y aparecen en el pico de la hora de la cena. La forma más clara de entender los niveles de aislamiento es a través de las anomalías concretas (también llamadas fenómenos) que permiten o evitan. La actualización perdida que se muestra a continuación es la más común en el código de negocio.
Una actualización perdida, paso a paso
Las anomalías, de la más leve a la más sutil
| Anomalía | Qué ocurre | Ejemplo de reparto de comida o de banca |
|---|---|---|
| Escritura sucia | Una transacción sobrescribe datos que otra transacción ha escrito pero aún no ha confirmado. | Dos transacciones que asignan un repartidor se intercalan, de modo que la fila del pedido nombra a un repartidor y la fila del repartidor a otro pedido. Todo nivel de aislamiento real lo evita. |
| Lectura sucia | Una transacción lee datos escritos por otra transacción que no se ha confirmado (y que puede revertirse). | Un informe cuenta un pago de 500 que después se revierte. |
| Lectura no repetible (difusa) | Una transacción lee la misma fila dos veces y obtiene valores distintos porque otra transacción confirmó un cambio entre ambas lecturas. | El proceso de pago lee un precio de plato de 9.50, una edición del restaurante confirma 11.00 y la segunda lectura del proceso de pago ve 11.00. |
| Sesgo de lectura (read skew) | Una transacción ve partes de la base de datos en momentos distintos, de modo que los valores relacionados no cuadran. | Un cliente con 500 repartidos en dos cuentas ve 450 en total porque una transferencia de 50 se confirmó entre la lectura de los dos saldos. |
| Lectura fantasma | Una transacción vuelve a ejecutar una consulta con una condición y encuentra filas añadidas o eliminadas por otra transacción confirmada. | Contar dos veces los repartos abiertos de un repartidor devuelve 2 y después 3. |
| Actualización perdida | Dos transacciones leen un valor, cada una calcula uno nuevo y lo escribe, y una escritura sobrescribe en silencio a la otra. | Dos retiradas de la misma cuenta (diagrama anterior). |
| Write skew (sesgo de escritura) | Dos transacciones leen un conjunto de datos que se solapa, toman decisiones basadas en él y actualizan filas distintas, de modo que juntas rompen una regla que cada una comprobó. | Dos médicos de guardia ven cada uno que el otro sigue de guardia y ambos salen de guardia (ejemplo práctico más abajo). |
Los niveles de aislamiento del estándar SQL
El estándar SQL (desde SQL-92 en adelante) define cuatro niveles de aislamiento según cuáles de tres fenómenos prohíben: lecturas sucias, lecturas no repetibles y fantasmas. El estándar establece serializable como valor por defecto, pero casi ningún producto convencional lo sigue; la mayoría usa por defecto read committed o repeatable read por rendimiento.
Los niveles del estándar SQL y los fenómenos que descartan
| Nivel de aislamiento | Lectura sucia | Lectura no repetible | Lectura fantasma |
|---|---|---|---|
| Read uncommitted | posible | posible | posible |
| Read committed | evitada | posible | posible |
| Repeatable read | evitada | evitada | posible |
| Serializable | evitada | evitada | evitada, y el resultado debe ser igual a algún orden en serie |
En 1995, Hal Berenson, Phil Bernstein, Jim Gray, Jim Melton, Elizabeth O'Neil y Patrick O'Neil publicaron A Critique of ANSI SQL Isolation Levels. Demostraron que las definiciones en inglés del estándar eran ambiguas, que prohibir los tres fenómenos no proporciona por sí solo serializabilidad y que los niveles del estándar no describían los niveles de aislamiento que usaban los productos reales. Añadieron fenómenos que al estándar se le escapaban, en particular la escritura sucia, la actualización perdida, el read skew y el write skew, y definieron el aislamiento de instantánea (snapshot isolation): cada transacción lee de una instantánea coherente de los datos confirmados en el momento de su inicio, y una transacción no puede confirmar una escritura en una fila que una transacción concurrente ya ha modificado y confirmado (gana el primero que confirma
). El aislamiento de instantánea evita las lecturas sucias, las lecturas no repetibles, el read skew y las actualizaciones perdidas, así como los fantasmas que afectan a las lecturas, pero permite el write skew, así que no es serializable. Encaja con dificultad entre los niveles del estándar, y por eso los productos lo etiquetan de forma distinta.
MVCC: cómo funcionan las instantáneas
La mayoría de las bases de datos modernas implementan las lecturas de instantánea con control de concurrencia multiversión (MVCC). En lugar de sobrescribir una fila en su sitio, una escritura crea una nueva versión etiquetada con el identificador de la transacción que escribe, y la versión antigua se conserva hasta que ninguna transacción en curso pueda necesitarla todavía. A cada lector se le da una instantánea, que es básicamente una lista de las transacciones que se habían confirmado cuando empezó, y solo ve las versiones que crearon esas transacciones. La gran ventaja es que los lectores nunca esperan a los escritores y los escritores nunca esperan a los lectores; solo entran en conflicto dos escritores sobre la misma fila.
Versiones de una fila con MVCC
El costo es el mantenimiento. Hay que limpiar las versiones antiguas: PostgreSQL las guarda en la tabla y las elimina con vacuum, mientras que Oracle y MySQL InnoDB guardan las imágenes previas en registros de deshacer (undo logs). Una transacción que se deja abierta durante horas retiene versiones antiguas, de modo que las tablas y los registros de deshacer se inflan y el rendimiento se degrada. Por eso las transacciones de larga duración son una mala señal operativa en cualquier base de datos MVCC, y un motivo para mantener las consultas analíticas fuera de la base de datos operativa. En read committed con MVCC, cada sentencia obtiene una instantánea nueva; en el aislamiento de instantánea, toda la transacción usa una sola.
Los valores por defecto reales, y por qué engañan los nombres
Los nombres de los niveles de aislamiento no son portables. La misma palabra puede significar garantías distintas en productos distintos, y los valores por defecto difieren. El proyecto Hermitage de Martin Kleppmann probó el comportamiento real de muchas bases de datos frente a las anomalías anteriores; la documentación de los fabricantes confirma los valores por defecto. Toma esta tabla como punto de partida y consulta la documentación de la versión que uses.
Mismos nombres, comportamiento distinto: qué hacen las bases de datos reales (resumido a partir de la documentación de los fabricantes y de las pruebas de Hermitage)
| Base de datos | Nivel por defecto | Qué son realmente los niveles | Notas |
|---|---|---|---|
| PostgreSQL | Read committed | Read uncommitted se comporta como read committed. Repeatable read es aislamiento de instantánea. Serializable es aislamiento de instantánea serializable (SSI). | Repeatable read aborta una transacción que intenta actualizar una fila modificada desde su instantánea (could not serialize access due to concurrent update), así que se evitan las actualizaciones perdidas; el write skew, no. |
| MySQL (InnoDB) | Repeatable read | Los SELECT simples leen una instantánea tomada en la primera lectura; las lecturas con bloqueo, UPDATE y DELETE actúan sobre las filas confirmadas más recientes con bloqueos de registro y de hueco (record locks y gap locks). Serializable convierte los SELECT simples en lecturas con bloqueo cuando autocommit está desactivado. | En repeatable read, una lectura-modificación-escritura escrita como SELECT y después UPDATE todavía puede perder una actualización o producir write skew, salvo que el SELECT use FOR UPDATE. |
| Oracle Database | Read committed | Ofrece read committed y serializable; su serializable es aislamiento de instantánea. | Evita las actualizaciones perdidas en serializable, pero permite el write skew. |
| Microsoft SQL Server | Read committed (con bloqueos) en SQL Server; Azure SQL Database activa read committed snapshot por defecto | Read committed con bloqueos, read committed snapshot, repeatable read (con bloqueos), snapshot (aislamiento de instantánea) y serializable (con bloqueos). | En SQL Server, el aislamiento de instantánea debe habilitarse por base de datos (ALLOW_SNAPSHOT_ISOLATION) antes de usarlo. |
| CockroachDB | Serializable | Serializable por defecto; read committed también está disponible. | Los clientes deben reintentar las transacciones que fallan con un error de serialización. |
Cómo lograr la serializabilidad
Las bases de datos que ofrecen verdadera serializabilidad usan una de tres técnicas. La ejecución realmente en serie ejecuta las transacciones de una en una en un único hilo, lo cual es rápido si cada transacción es corta y los datos están en memoria (VoltDB, y los scripts y transacciones de Redis, funcionan así). El bloqueo en dos fases (2PL), que usan los niveles serializables de SQL Server y de MySQL, hace que los lectores tomen bloqueos compartidos y los escritores bloqueos exclusivos, incluidos bloqueos sobre rangos para impedir los fantasmas, y los mantiene hasta el final de la transacción; es correcto, pero lectores y escritores se bloquean entre sí, el throughput cae cuando hay contención y los interbloqueos son frecuentes. El aislamiento de instantánea serializable (SSI), descrito por Michael Cahill, Uwe Röhm y Alan Fekete en 2008 y usado en PostgreSQL desde la versión 9.1 (CockroachDB usa una técnica optimista relacionada), ejecuta las transacciones sobre instantáneas sin bloquear, registra las dependencias de lectura-escritura entre ellas y aborta una transacción cuando aparece un patrón peligroso.
Sea cual sea la técnica, las transacciones serializables pueden fallar con un error de serialización (SQLSTATE 40001 en PostgreSQL y CockroachDB) o con un interbloqueo, y la aplicación debe reintentar la transacción completa. Este es el costo oculto del nivel más fuerte: cada ruta de código transaccional necesita un bucle de reintentos que sea seguro repetir, lo que significa que no puede haber efectos secundarios, como enviar correos electrónicos o llamar a proveedores de pago, dentro de la transacción.
Control de concurrencia pesimista y optimista
En la práctica, la mayoría de las aplicaciones se ejecutan en read committed o repeatable read y protegen explícitamente rutas concretas de lectura-modificación-escritura. Hay tres herramientas. La mejor, cuando es posible, es una actualización atómica que deja a la base de datos hacer la aritmética y la comprobación en una sola sentencia, como UPDATE accounts SET balance = balance - 30 WHERE id = 7 AND balance >= 30, y después comprobar que ha cambiado exactamente una fila. Con read committed en PostgreSQL, una actualización concurrente hace que la segunda sentencia espere y después vuelva a evaluar su condición contra la fila nueva, así que no se pierde ninguna actualización.
Cuando la decisión necesita una lógica que la base de datos no puede expresar, usa el bloqueo pesimista: bloquea las filas que lees con SELECT ... FOR UPDATE, de modo que las transacciones concurrentes que las quieran esperen hasta que confirmes. Es sencillo y se adapta a operaciones cortas y con mucha contención (asignar un repartidor a un pedido). Los costos son la espera, el riesgo de interbloqueos (bloquea las filas en un orden coherente, por ejemplo por id ascendente) y un menor throughput. Entre las variantes están NOWAIT, que falla de inmediato en lugar de esperar, y SKIP LOCKED, que se salta las filas ya bloqueadas y es la forma habitual de construir una cola de trabajos fiable sobre una tabla relacional.
BEGIN;
-- Lock the row: other FOR UPDATE readers and writers must wait
SELECT balance FROM accounts WHERE id = 7 FOR UPDATE;
-- application checks: balance (100) >= 30, so allow it
UPDATE accounts SET balance = 70 WHERE id = 7;
INSERT INTO ledger (account_id, amount, kind)
VALUES (7, -30, 'withdrawal');
COMMIT; -- lock released; the waiting transaction now sees 70El control de concurrencia optimista no toma ningún bloqueo mientras el usuario o el código está pensando. Cada fila lleva un número de versión (o una marca de tiempo o un hash). El escritor solo actualiza si la versión sigue siendo la que leyó, y la incrementa. Si no cambia ninguna fila, alguien se le adelantó, y la aplicación recarga y reintenta o se lo comunica al usuario. Esto se adapta a datos con poca contención y a un tiempo de reflexión
largo: el gerente de un restaurante que edita una carta en un navegador durante cinco minutos no debe mantener un bloqueo de base de datos todo ese tiempo. HTTP incorpora la misma idea, con los ETag y la cabecera If-Match.
-- Read (no lock); remember version = 12
SELECT id, name, price, version FROM menu_items WHERE id = 501;
-- Later, write only if the version is unchanged
UPDATE menu_items
SET price = 11.00, version = version + 1
WHERE id = 501 AND version = 12;
-- Rows affected = 1: success.
-- Rows affected = 0: a concurrent edit won; reload and retry
-- or show the user the other change.Ejemplo práctico
Ejemplo práctico: write skew con dos médicos de guardia
Un sistema de turnos de un hospital hace cumplir una regla: al menos un médico debe estar de guardia en cada turno. Alice y Bob están de guardia esta noche. Los dos se encuentran mal y pulsan Salir de guardia
casi en el mismo momento. Cada solicitud ejecuta esta lógica en una transacción: contar los médicos de guardia en el turno; si hay al menos dos, poner mi propia fila como fuera de guardia. La base de datos funciona con aislamiento de instantánea (repeatable read de PostgreSQL o serializable de Oracle).
| Inicio | Tabla doctors: Alice on_call = true, Bob on_call = true. Regla: count(on_call) >= 1. |
|---|---|
| Paso 1: T1 (Alice) empieza | T1 toma una instantánea. |
| Paso 2: T2 (Bob) empieza | T2 toma una instantánea. Ambas instantáneas muestran dos médicos de guardia. |
| Paso 3: T1 lee | SELECT count(*) FROM doctors WHERE shift = 1234 AND on_call devuelve 2. La comprobación se supera. |
| Paso 4: T2 lee | La misma consulta en la instantánea de T2 también devuelve 2. La comprobación se supera. |
| Paso 5: T1 escribe | UPDATE doctors SET on_call = false WHERE name = 'Alice' AND shift = 1234 |
| Paso 6: T2 escribe | UPDATE doctors SET on_call = false WHERE name = 'Bob' AND shift = 1234. Es una fila distinta, así que no se detecta ningún conflicto de escritura-escritura. |
| Paso 7: ambas confirman | El aislamiento de instantánea no ve ningún conflicto: cada transacción escribió una fila que la otra no escribió. Resultado: nadie está de guardia. Cada transacción era correcta por separado; juntas rompieron la regla. |
| Con serializable (SSI) | La base de datos detecta que cada transacción leyó datos que la otra modificó después (un ciclo peligroso de dependencias de lectura-escritura). Un commit falla con SQLSTATE 40001; el reintento vuelve a contar, encuentra un médico y se niega. |
| Solución en un nivel más débil | Bloquea las filas de las que depende la decisión: SELECT name FROM doctors WHERE shift = 1234 AND on_call FOR UPDATE, y después cuenta las filas devueltas. Ahora T2 espera a que se liberen los bloqueos de T1. En read committed, T2 vuelve a leer, encuentra solo a Bob de guardia y se niega; en repeatable read de PostgreSQL falla en su lugar con un error de serialización, y el reintento se niega. |
El write skew es una generalización de la actualización perdida: las transacciones leen los mismos datos pero escriben filas distintas, así que la regla de que gana el primero que confirma, propia del aislamiento de instantánea, nunca se activa.
El mismo patrón aparece en los sistemas de reservas (dos personas que reservan el último asiento o franjas de reunión que se solapan), en los nombres de usuario (dos registros que comprueban que un nombre está libre) y en la banca (dos retiradas de cuentas distintas que juntas superan un límite de descubierto conjunto).
Cuando las filas que demostrarían un conflicto aún no existen, como ocurre con dos reservas que se solapan, FOR UPDATE no tiene nada que bloquear. Las opciones son el aislamiento serializable, una restricción de unicidad o de exclusión (PostgreSQL puede prohibir de forma declarativa los rangos de tiempo que se solapan) o materializar el conflicto: crear filas, por ejemplo una por sala y franja horaria, con el único fin de poder bloquearlas.
Cómo elegir una estrategia de aislamiento
No existe un único nivel correcto; existe un nivel correcto para cada fragmento de lógica. Un enfoque práctico parte de las invariantes, no de los ajustes.
Lista de comprobación: cómo hacer segura una ruta de código transaccional
- Nombra la invarianteEscribe la regla que protege el código (el saldo nunca por debajo de cero, un repartidor por pedido, al menos un médico de guardia). Si no hay ninguna, probablemente el nivel por defecto basta.
- Prefiere las restricciones y las sentencias atómicasLos índices únicos, las claves foráneas, las restricciones CHECK y de exclusión, y las actualizaciones condicionales de una sola sentencia las hace cumplir la base de datos sea cual sea el nivel de aislamiento.
- Detecta las lecturas-modificaciones-escriturasTodo código que lee un valor, decide en la aplicación y vuelve a escribir es un riesgo de actualización perdida o de write skew. Protégelo con FOR UPDATE, una columna de versión o aislamiento serializable.
- Conoce la semántica real de tu base de datosComprueba qué significan realmente los nombres de los niveles de tu producto (el repeatable read de PostgreSQL y el de MySQL difieren; el serializable de Oracle es aislamiento de instantánea).
- Reintenta correctamenteSerializable y repeatable read pueden abortar con errores de serialización e interbloqueos. Reintenta la transacción completa con espera progresiva (back-off), y deja los efectos secundarios externos fuera de ella.
- Mantén cortas las transaccionesNada de tiempo de reflexión del usuario, llamadas de red ni informes grandes dentro de una transacción; las transacciones largas mantienen bloqueos e inflan el almacenamiento MVCC.
- Prueba con concurrenciaEjecuta las rutas críticas de forma concurrente en una prueba de integración contra el motor de base de datos real, no contra un sustituto en memoria con una semántica distinta.
Como regla general, read committed con bloqueo explícito o comprobaciones de versión es un valor por defecto sólido y rápido para la mayoría de las aplicaciones de negocio, siempre que el equipo sea disciplinado a la hora de encontrar las rutas de lectura-modificación-escritura. Serializable es el valor por defecto más seguro cuando las reglas son complejas, el equipo es grande o el costo de una invariante incumplida es alto (dinero, medicina, seguridad), y muchos equipos lo usan ahora en las rutas críticas, aceptando los reintentos. Los profesionales discrepan de verdad en este punto: unos sostienen que serializable por defecto evita toda una clase de bugs que la revisión de código nunca detectará; otros sostienen que sus reintentos y su contención cuestan demasiado y que el bloqueo explícito hace visible la intención. Ambas posturas son defendibles si la elección es deliberada y está probada.
Comprueba lo que has entendido
¿Por qué el aislamiento de instantánea no es serializable?
Porque permite el write skew: dos transacciones pueden leer datos que se solapan y escribir filas distintas, rompiendo juntas una regla que cada una comprobó, sin que se detecte ningún conflicto de escritura-escritura.
¿De qué depende la C de ACID?
Sobre todo de la aplicación: la base de datos hace cumplir las restricciones declaradas, pero las invariantes de negocio solo se mantienen si las transacciones de la aplicación las preservan.
¿Cuándo elegirías el control de concurrencia optimista en lugar del pesimista?
Cuando los conflictos son poco frecuentes o hay un tiempo de reflexión largo entre la lectura y la escritura, como cuando un usuario edita un formulario, de modo que mantener bloqueos sería un desperdicio o imposible.
¿Por qué el código que usa aislamiento serializable debe poder reintentarse con seguridad?
Las transacciones serializables pueden abortarse por fallos de serialización o interbloqueos, y la respuesta correcta es volver a ejecutar la transacción completa, así que no debe tener efectos secundarios externos.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Nuestro sistema de reservas a veces reserva dos veces la misma mesa aunque comprobamos primero la disponibilidad. ¿Qué nivel de aislamiento o qué enfoque de bloqueo deberíamos usar?”