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:
Una transacción lee datos escritos por una transacción concurrente no comprometida.
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).
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.
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.
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.
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.
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].
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.
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.