13.2. Aislamiento de transacciones #

13.2.1. Nivel de aislamiento Read Committed
13.2.2. Nivel de aislamiento Repeatable Read
13.2.3. Nivel de aislamiento Serializable

El estándar SQL define cuatro niveles de aislamiento de transacciones. El más estricto es Serializable, el cual está definido por el estándar en un párrafo que dice que cualquier ejecución concurrente de un conjunto de transacciones serializables garantiza producir el mismo efecto que ejecutarlas una a la vez en algún orden. Los otros tres niveles se definen en términos de fenómenos resultantes de la interacción entre transacciones concurrentes, los cuales no deben ocurrir en cada nivel. El estándar señala que, debido a la definición de Serializable, ninguno de estos fenómenos es posible en ese nivel. (Esto no es sorprendente; si el efecto de las transacciones debe ser consistente con haber sido ejecutadas una a la vez, ¿cómo se podría ver algún fenómeno causado por interacciones?)

Los fenómenos que están prohibidos en varios niveles son:

lectura sucia (dirty read)

Una transacción lee datos escritos por una transacción concurrente no comprometida.

lectura no repetible (nonrepeatable read)

Una transacción vuelve a leer datos que leyó previamente y descubre que los datos han sido modificados por otra transacción (que se comprometió desde la lectura inicial).

lectura fantasma (phantom read)

Una transacción vuelve a ejecutar una consulta que devuelve un conjunto de filas que satisfacen una condición de búsqueda y descubre que el conjunto de filas que satisfacen la condición ha cambiado debido a otra transacción recientemente comprometida.

anomalía de serialización (serialization anomaly)

El resultado de comprometer con éxito un grupo de transacciones es inconsistente con todos los órdenes posibles de ejecutar esas transacciones una a la vez.

El estándar SQL y los niveles de aislamiento de transacciones implementados en PostgreSQL se describen en la Table 13.1.

Table 13.1. Niveles de aislamiento de transacciones

Nivel de aislamiento Lectura sucia Lectura no repetible Lectura fantasma Anomalía de serialización
Read uncommitted Permitido, pero no en PG Posible Posible Posible
Read committed No posible Posible Posible Posible
Repeatable read No posible No posible Permitido, pero no en PG Posible
Serializable No posible No posible No posible No posible

En PostgreSQL, puedes solicitar cualquiera de los cuatro niveles estándar de aislamiento de transacciones, pero internamente solo se implementan tres niveles de aislamiento distintos, es decir, el modo Read Uncommitted de PostgreSQL se comporta como Read Committed. Esto se debe a que es la única forma sensata de asignar los niveles de aislamiento estándar a la arquitectura de control de concurrencia multiversión de PostgreSQL.

La tabla también muestra que la implementación de Repeatable Read de PostgreSQL no permite lecturas fantasma. Esto es aceptable bajo el estándar SQL porque este especifica qué anomalías no deben ocurrir en ciertos niveles de aislamiento; las garantías más altas son aceptables. El comportamiento de los niveles de aislamiento disponibles se detalla en las siguientes subsecciones.

Para establecer el nivel de aislamiento de transacciones de una transacción, utiliza el comando SET TRANSACTION.

Important

Algunos tipos de datos y funciones de PostgreSQL tienen reglas especiales con respecto al comportamiento transaccional. En particular, los cambios realizados en una secuencia (y por lo tanto el contador de una columna declarada utilizando serial) son inmediatamente visibles para todas las demás transacciones y no se revierten si la transacción que realizó los cambios se cancela. Consulta la Section 9.17 y la Section 8.1.4.

13.2.1. Nivel de aislamiento Read Committed #

Read Committed es el nivel de aislamiento por defecto en PostgreSQL. Cuando una transacción utiliza este nivel de aislamiento, una consulta SELECT (sin una cláusula FOR UPDATE/SHARE) ve únicamente los datos comprometidos antes de que comenzara la consulta; nunca ve datos sin comprometer ni cambios comprometidos por transacciones concurrentes durante la ejecución de la consulta. En efecto, una consulta SELECT ve una instantánea de la base de datos en el instante en que comienza a ejecutarse la consulta. Sin embargo, SELECT sí ve los efectos de las actualizaciones anteriores ejecutadas dentro de su propia transacción, aunque aún no se hayan comprometido. También ten en cuenta que dos comandos SELECT sucesivos pueden ver datos diferentes, incluso dentro de una sola transacción, si otras transacciones comprometen cambios después de que comienza el primer SELECT y antes de que comience el segundo SELECT.

Los comandos UPDATE, DELETE, SELECT FOR UPDATE y SELECT FOR SHARE se comportan igual que SELECT en términos de búsqueda de filas objetivo: solo encontrarán filas objetivo que se hayan comprometido en el momento de inicio del comando. Sin embargo, tal fila objetivo podría haber sido ya actualizada (o eliminada o bloqueada) por otra transacción concurrente en el momento en que se encuentra. En este caso, el que pretende actualizar esperará a que la primera transacción de actualización se comprometa o se revierta (si todavía está en curso). Si la primera transacción se revierte, entonces sus efectos se niegan y el segundo actualizador puede proceder con la actualización de la fila originalmente encontrada. Si el primer actualizador se compromete, el segundo actualizador ignorará la fila si el primer actualizador la eliminó; de lo contrario, intentará aplicar su operation a la versión actualizada de la fila. La condición de búsqueda del comando (la cláusula WHERE) se vuelve a evaluar para ver si la versión actualizada de la fila sigue coincidiendo con la condición de búsqueda. Si es así, el segundo actualizador procede con su operación utilizando la versión actualizada de la fila. En el caso de SELECT FOR UPDATE y SELECT FOR SHARE, esto significa que es la versión actualizada de la fila la que se bloquea y se devuelve al cliente.

INSERT con una cláusula ON CONFLICT DO UPDATE se comporta de manera similar. En el modo Read Committed, cada fila propuesta para inserción se insertará o se actualizará. A menos que haya errores no relacionados, uno de esos dos resultados está garantizado. Si un conflicto se origina en otra transacción cuyos efectos aún no son visibles para el INSERT, la cláusula UPDATE afectará a esa fila, aunque posiblemente ninguna versión de esa fila sea visible de manera convencional para el comando.

El comando INSERT con una cláusula ON CONFLICT DO NOTHING puede no proceder con la inserción de una fila debido al resultado de otra transacción cuyos efectos no son visibles para la instantánea del INSERT. Una vez más, este es solo el caso en el modo Read Committed.

MERGE permite al usuario especificar varias combinaciones de subcomandos INSERT, UPDATE y DELETE. Un comando MERGE con subcomandos tanto INSERT como UPDATE se parece a INSERT con una cláusula ON CONFLICT DO UPDATE pero no garantiza que ocurra INSERT o UPDATE. Si MERGE intenta un UPDATE o DELETE y la fila se actualiza concurrentemente, pero la condición de unión (join) sigue pasando para el objetivo actual y la tupla de origen actual, entonces MERGE se comportará igual que los comandos UPDATE o DELETE y realizará su acción sobre la versión actualizada de la fila. Sin embargo, dado que MERGE puede especificar varias acciones y estas pueden ser condicionales, las condiciones de cada acción se vuelven a evaluar en la versión actualizada de la fila, empezando por la primera acción, incluso si la acción que había coincidido originalmente aparece más adelante en la lista de acciones. Por otro lado, si la fila se actualiza concurrentemente de modo que la condición de unión falla, entonces MERGE evaluará a continuación las acciones del comando NOT MATCHED BY SOURCE y NOT MATCHED [BY TARGET], y ejecutará la primera de cada tipo que tenga éxito. Si la fila se elimina concurrentemente, entonces MERGE evaluará las acciones NOT MATCHED [BY TARGET] del comando y ejecutará la primera que tenga éxito. Si MERGE intenta un INSERT y un índice único está presente y se inserta concurrentemente una fila duplicada, entonces se lanza un error de violación de unicidad; MERGE no intenta evitar tales errores reiniciando la evaluación de las condiciones MATCHED.

Debido a las reglas anteriores, es posible que un comando de actualización vea una instantánea inconsistente: puede ver los efectos de comandos de actualización concurrentes en las mismas filas que intenta actualizar, pero no ve los efectos de esos comandos en otras filas de la base de datos. Este comportamiento hace que el modo Read Committed no sea adecuado para comandos que involucren condiciones de búsqueda complejas; sin embargo, es perfecto para casos más sencillos. Por ejemplo, considera la transferencia de $100 de una cuenta a otra:

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

Si otra transacción intenta concurrentemente cambiar el saldo de la cuenta 7534, claramente queremos que la segunda sentencia comience con la versión actualizada de la fila de la cuenta. Dado que cada comando afecta únicamente a una fila predeterminada, permitirle ver la versión actualizada de la fila no crea ninguna inconsistencia problemática.

Un uso más complejo puede producir resultados indeseables en el modo Read Committed. Por ejemplo, considera un comando DELETE que opera sobre datos que están siendo añadidos y eliminados de sus criterios de restricción por otro comando, por ejemplo, asume que website es una tabla de dos filas con website.hits igual a 9 y 10:

BEGIN;
UPDATE website SET hits = hits + 1;
-- ejecutado desde otra sesión:  DELETE FROM website WHERE hits = 10;
COMMIT;

El comando DELETE no tendrá ningún efecto a pesar de que hay una fila website.hits = 10 antes y después del UPDATE. Esto ocurre porque el valor de la fila antes del update (9) se omite, y cuando el UPDATE se completa y el DELETE obtiene un bloqueo, el nuevo valor de la fila ya no es 10 sino 11, que ya no coincide con el criterio.

Debido a que el modo Read Committed inicia cada comando con una nueva instantánea que incluye todas las transacciones comprometidas hasta ese instante, los comandos subsiguientes en la misma transacción verán los efectos de la transacción concurrente comprometida en cualquier caso. El punto en cuestión arriba es si un único comando ve o no una vista absolutamente consistente de la base de datos.

El aislamiento parcial de transacciones proporcionado por el modo Read Committed es adecuado para muchas aplicaciones, y este modo es rápido y sencillo de usar; sin embargo, no es suficiente para todos los casos. Las aplicaciones que realizan consultas y actualizaciones complejas pueden requerir una vista más rigurosamente consistente de la base de datos que la que proporciona el modo Read Committed.

13.2.2. Nivel de aislamiento Repeatable Read #

El nivel de aislamiento Repeatable Read solo ve los datos comprometidos antes de que comenzara la transacción; nunca ve datos sin comprometer ni cambios comprometidos por transacciones concurrentes durante la ejecución de la transacción. (Sin embargo, cada consulta sí ve los efectos de las actualizaciones anteriores ejecutadas dentro de su propia transacción, aunque aún no se hayan comprometido). Esta es una garantía más fuerte que la requerida por el estándar SQL para este nivel de aislamiento, y evita todos los fenómenos descritos en la Table 13.1 excepto las anomalías de serialización. Como se mencionó anteriormente, esto está específicamente permitido por el estándar, el cual solo describe las protecciones mínimas que debe proporcionar cada nivel de aislamiento.

Este nivel se diferencia de Read Committed en que una consulta en una transacción de lectura repetible ve una instantánea del momento en que se inicia la primera sentencia que no sea de control de transacciones en la transacción, no de la fecha de inicio de la sentencia actual dentro de la transacción. Por lo tanto, los comandos SELECT sucesivos dentro de una sola transacción ven los mismos datos, es decir, no ven los cambios realizados por otras transacciones que se comprometieron después de que comenzó su propia transacción.

Las aplicaciones que utilicen este nivel deben estar preparadas para reintentar transacciones debido a fallas de serialización.

Los comandos UPDATE, DELETE, MERGE, SELECT FOR UPDATE y SELECT FOR SHARE se comportan igual que SELECT en términos de búsqueda de filas objetivo: solo encontrarán filas objetivo que se hayan comprometido en el momento de inicio de la transacción. Sin embargo, tal fila objetivo podría haber sido ya actualizada (o eliminada o bloqueada) por otra transacción concurrente en el momento en que se encuentra. En este caso, la transacción de lectura repetible esperará a que la primera transacción de actualización se comprometa o se revierta (si todavía está en curso). Si el primer actualizador se revierte, entonces sus efectos se niegan y la transacción de lectura repetible puede proceder a actualizar la fila originalmente encontrada. Pero si el primer actualizador se compromete (y realmente actualizó o eliminó la fila, no solo la bloqueó), entonces la transacción de lectura repetible se revertirá con el mensaje:

ERROR:  could not serialize access due to concurrent update

porque una transacción de lectura repetible no puede modificar ni bloquear filas cambiadas por otras transacciones después de que comenzó la transacción de lectura repetible.

Cuando una aplicación recibe este mensaje de error, debe abortar la transacción actual y reintentar toda la transacción desde el principio. La segunda vez, la transacción verá el cambio previamente comprometido como parte de su vista inicial de la base de datos, por lo que no hay conflicto lógico en el uso de la nueva versión de la fila como punto de partida para la actualización de la nueva transacción.

Ten en cuenta que solo las transacciones de actualización pueden necesitar ser reintentadas; las transacciones de solo lectura nunca tendrán conflictos de serialización.

El modo Repeatable Read proporciona una garantía rigurosa de que cada transacción ve una vista completamente estable de la base de datos. Sin embargo, esta vista no siempre será consistente con alguna ejecución serial (una a la vez) de transacciones concurrentes del mismo nivel. Por ejemplo, incluso una transacción de solo lectura en este nivel puede ver un registro de control actualizado para mostrar que un lote se ha completado pero no ver uno de los registros de detalle que es lógicamente parte del lote porque leyó una revisión anterior del registro de control. Es probable que los intentos de hacer cumplir las reglas de negocio mediante transacciones que se ejecutan en este nivel de aislamiento no funcionen correctamente sin un uso cuidadoso de bloqueos explícitos para bloquear las transacciones en conflicto.

El nivel de aislamiento Repeatable Read se implementa utilizando una técnica conocida en la literatura académica sobre bases de datos y en algunos otros productos de bases de datos como Aislamiento de instantáneas (Snapshot Isolation). Se pueden observar diferencias en el comportamiento y el rendimiento cuando se compara con sistemas que utilizan una técnica tradicional de bloqueo que reduce la concurrencia. Algunos otros sistemas pueden incluso ofrecer Repeatable Read y Snapshot Isolation como niveles de aislamiento distintos con comportamientos diferentes. Los fenómenos permitidos que distinguen las dos técnicas no fueron formalizados por los investigadores de bases de datos hasta después de que se desarrollara el estándar SQL, y están fuera del alcance de este manual. Para un tratamiento completo, consulta la [berenson95].

Note

Antes de la versión 9.1 de PostgreSQL, una solicitud para el nivel de aislamiento de transacciones Serializable proporcionaba exactamente el mismo comportamiento descrito aquí. Para mantener el comportamiento Serializable heredado, ahora se debe solicitar Repeatable Read.

13.2.3. Nivel de aislamiento Serializable #

El nivel de aislamiento Serializable proporciona el aislamiento de transacciones más estricto. Este nivel emula la ejecución serial de transacciones para todas las transacciones comprometidas; como si las transacciones se hubieran ejecutado una tras otra, serialmente, en lugar de concurrentemente. Sin embargo, al igual que en el nivel Repeatable Read, las aplicaciones que utilicen este nivel deben estar preparadas para reintentar transacciones debido a fallas de serialización. De hecho, este nivel de aislamiento funciona exactamente igual que Repeatable Read excepto que también monitorea las condiciones que podrían hacer que la ejecución de un conjunto concurrente de transacciones serializables se comporte de una manera inconsistente con todas las ejecuciones seriales posibles (una a la vez) de esas transacciones. Este monitoreo no introduce ningún bloqueo adicional al presente en la lectura repetible, pero hay cierta sobrecarga en el monitoreo, y la detección de las condiciones que podrían causar una anomalía de serialización desencadenará un fallo de serialización.

Como ejemplo, considera una tabla mytab, que inicialmente contiene:

  class | value
 -------+-------
      1 |    10
      1 |    20
      2 |   100
      2 |   200

Supón que la transacción serializable A calcula:

SELECT SUM(value) FROM mytab WHERE class = 1;

y luego inserta el resultado (30) como el value en una nueva fila con class = 2. Concurrentemente, la transacción serializable B calcula:

SELECT SUM(value) FROM mytab WHERE class = 2;

y obtiene el resultado 300, el cual inserta en una nueva fila con class = 1. Luego, ambas transacciones intentan comprometerse. Si cualquiera de las transacciones se estuviera ejecutando en el nivel de aislamiento Repeatable Read, a ambas se les permitiría comprometerse; pero dado que no hay un orden de ejecución serial consistente con el resultado, el uso de transacciones Serializables permitirá que una transacción se comprometa y revertirá la otra con este mensaje:

ERROR:  could not serialize access due to read/write dependencies among transactions

Esto se debe a que si A se hubiera ejecutado antes que B, B habría calculado la suma 330, no 300, y de manera similar, el otro orden habría dado como resultado una suma diferente calculada por A.

Cuando se depende de transacciones Serializables para evitar anomalías, es importante que cualquier dato leído de una tabla de usuario permanente no se considere válido hasta que la transacción que lo leyó se haya comprometido con éxito. Esto es cierto incluso para las transacciones de solo lectura, excepto que los datos leídos dentro de una transacción de solo lectura aplazable (deferrable) se saben válidos tan pronto como se leen, porque dicha transacción espera hasta que pueda adquirir una instantánea garantizada libre de tales problemas antes de comenzar a leer cualquier dato. En todos los demás casos, las aplicaciones no deben depender de los resultados leídos durante una transacción que posteriormente se canceló; en su lugar, deben reintentar la transacción hasta que tenga éxito.

Para garantizar una verdadera serializabilidad, PostgreSQL utiliza bloqueo de predicado (predicate locking), lo que significa que mantiene bloqueos que le permiten determinar cuándo una escritura habría tenido un impacto en el resultado de una lectura anterior de una transacción concurrente, si se hubiera ejecutado primero. En PostgreSQL estos bloqueos no causan ningún bloqueo y, por lo tanto, no pueden participar en la causa de un callejón sin salida (deadlock). Se utilizan para identificar e indicar dependencias entre transacciones Serializables concurrentes que, en ciertas combinaciones, pueden conducir a anomalías de serialización. Por el contrario, una transacción Read Committed o Repeatable Read que quiera asegurar la consistencia de los datos puede necesitar adquirir un bloqueo sobre una tabla completa, lo que podría bloquear a otros usuarios que intenten utilizar esa tabla, o puede utilizar SELECT FOR UPDATE o SELECT FOR SHARE que no solo pueden bloquear otras transacciones sino que causan acceso a disco.

Los bloqueos de predicado en PostgreSQL, al igual que en la mayoría de los otros sistemas de bases de datos, se basan en los datos a los que realmente ha accedido una transacción. Estos se mostrarán en la vista del sistema pg_locks con un mode de SIReadLock. Los bloqueos particulares adquiridos durante la ejecución de una consulta dependerán del plan utilizado por la consulta, y múltiples bloqueos de grano fino (por ejemplo, bloqueos de tuplas) pueden combinarse en menos bloqueos de grano grueso (por ejemplo, bloqueos de páginas) durante el curso de la transacción para evitar el agotamiento de la memoria utilizada para rastrear los bloqueos. Una transacción READ ONLY puede ser capaz de liberar sus bloqueos SIRead antes de completarse, si detecta que ya no pueden ocurrir conflictos que puedan conducir a una anomalía de serialización. De hecho, las transacciones READ ONLY a menudo podrán establecer ese hecho en el arranque y evitar la adquisición de bloqueos de predicado. Si solicitas explícitamente una transacción SERIALIZABLE READ ONLY DEFERRABLE, se bloqueará hasta que pueda establecer este hecho. (Este es el único caso en el que las transacciones Serializables bloquean pero las transacciones Repeatable Read no). Por otro lado, los bloqueos SIRead a menudo deben mantenerse más allá del compromiso de la transacción, hasta que las transacciones de lectura y escritura superpuestas se completen.

El uso constante de transacciones Serializables puede simplificar el desarrollo. La garantía de que cualquier conjunto de transacciones Serializables concurrentes comprometidas con éxito tendrá el mismo efecto que si se ejecutaran una a la vez significa que si puedes demostrar que una sola transacción, tal como está escrita, hará lo correcto cuando se ejecute por sí misma, puedes tener la confianza de que hará lo correcto en cualquier combinación de transacciones Serializables, incluso sin ninguna información sobre lo que esas otras transacciones podrían hacer, o bien no se compromete con éxito. Es importante que un entorno que utilice esta técnica disponga de una forma generalizada de gestionar los fallos de serialización (que siempre devuelven un valor SQLSTATE de '40001'), porque será muy difícil predecir exactamente qué transacciones podrían contribuir a las dependencias de lectura/escritura y necesitar ser revertidas para evitar anomalías de serialización. El monitoreo de las dependencias de lectura/escritura tiene un coste, al igual que el reinicio de las transacciones que se terminan con un fallo de serialización, pero en comparación con el coste y el bloqueo implicados en el uso de bloqueos explícitos y SELECT FOR UPDATE o SELECT FOR SHARE, las transacciones Serializables son la mejor opción de rendimiento para algunos entornos.

Aunque el nivel de aislamiento de transacciones Serializable de PostgreSQL solo permite que las transacciones concurrentes se comprometan si puede demostrar que existe un orden serial de ejecución que produciría el mismo efecto, no siempre evita que se lancen errores que no ocurrirían en una ejecución serial verdadera. En particular, es posible ver violaciones de restricciones únicas causadas por conflictos con transacciones Serializables superpuestas incluso después de comprobar explícitamente que la clave no está presente antes de intentar insertarla. Esto puede evitarse asegurándose de que todas las transacciones Serializables que inserten claves potencialmente en conflicto comprueben explícitamente si pueden hacerlo primero. Por ejemplo, imagina una aplicación que pide al usuario una nueva clave y luego comprueba que no existe ya intentando seleccionarla primero, o genera una nueva clave seleccionando la clave máxima existente y sumando uno. Si algunas transacciones Serializables insertan nuevas claves directamente sin seguir este protocolo, se podrían reportar violaciones de restricciones únicas incluso en casos en los que no podrían ocurrir en una ejecución serial de las transacciones concurrentes.

Para un rendimiento óptimo cuando se confía en transacciones Serializables para el control de concurrencia, se deben considerar estos puntos:

  • Declara las transacciones como READ ONLY cuando sea posible.

  • Controla el número de conexiones activas, utilizando un pool de conexiones si es necesario. Esta es siempre una consideración importante para el rendimiento, pero puede ser especialmente importante en un sistema ocupado que utiliza transacciones Serializables.

  • No pongas más en una sola transacción de lo necesario para fines de integridad.

  • No dejes conexiones colgando en estado idle in transaction (inactivo en transacción) más tiempo del necesario. El parámetro de configuración idle_in_transaction_session_timeout puede utilizarse para desconectar automáticamente las sesiones persistentes.

  • Elimina los bloqueos explícitos, SELECT FOR UPDATE y SELECT FOR SHARE donde ya no sean necesarios debido a las protecciones proporcionadas automáticamente por las transacciones Serializables.

  • Cuando el sistema se ve obligado a combinar múltiples bloqueos de predicado a nivel de página en un solo bloqueo de predicado a nivel de relación porque la tabla de bloqueos de predicado se queda sin memoria, puede producirse un aumento en la tasa de fallos de serialización. Puedes evitar esto aumentando max_pred_locks_per_transaction, max_pred_locks_per_relation y/o max_pred_locks_per_page.

  • Un escaneo secuencial siempre requerirá un bloqueo de predicado a nivel de relación. Esto puede resultar en un aumento de la tasa de fallos de serialización. Puede ser útil fomentar el uso de escaneos de índices reduciendo random_page_cost y/o aumentando cpu_tuple_cost. Asegúrate de sopesar cualquier disminución en las reversiones y reinicios de transacciones frente a cualquier cambio general en el tiempo de ejecución de las consultas.

El nivel de aislamiento Serializable se implementa utilizando una técnica conocida en la literatura académica sobre bases de datos como Serializable Snapshot Isolation, la cual se basa en el aislamiento de instantáneas añadiendo comprobaciones de anomalías de serialización. Se pueden observar algunas diferencias en el comportamiento y el rendimiento en comparación con otros sistemas que utilizan una técnica de bloqueo tradicional. Consulta la [ports12] para obtener información detallada.