PostgreSQL proporciona varios modos de bloqueo para controlar el acceso
concurrente a los datos en las tablas. Estos modos se pueden utilizar para el bloqueo controlado por la
aplicación en situaciones en las que MVCC no proporciona el comportamiento deseado.
Además, la mayoría de los comandos de PostgreSQL adquieren automáticamente
bloqueos de los modos adecuados para garantizar que las tablas referenciadas no sean eliminadas o modificadas
de formas incompatibles mientras se ejecuta el comando. (Por ejemplo, TRUNCATE no puede
ejecutarse de forma segura simultáneamente con otras operaciones en la misma tabla, por lo que obtiene un
bloqueo ACCESS EXCLUSIVE en la tabla para imponer eso).
Para examinar una lista de los bloqueos pendientes actuales en un servidor de base de datos, utiliza
la vista del sistema pg_locks. Para obtener
más información sobre cómo supervisar el estado del subsistema gestor de bloqueos, consulta la
Chapter 27.
La lista siguiente muestra los modos de bloqueo disponibles y los contextos en los que
PostgreSQL los utiliza automáticamente. También puedes adquirir cualquiera
de estos bloqueos explícitamente con el comando LOCK.
Recuerda que todos estos modos de bloqueo son bloqueos a nivel de tabla, incluso si el nombre contiene
la palabra “row” (fila); los nombres de los modos de bloqueo son históricos.
Hasta cierto punto, los nombres reflejan el uso típico de cada modo de bloqueo — pero la semántica
es siempre la misma. La única diferencia real entre un modo de bloqueo y otro es el conjunto de modos de
bloqueo con los que entra en conflicto cada uno (consulta la la Table 13.2).
Dos transacciones no pueden mantener bloqueos de modos en conflicto sobre la misma tabla al mismo tiempo.
(Sin embargo, una transacción nunca entra en conflicto consigo misma. Por ejemplo, puede adquirir un
bloqueo ACCESS EXCLUSIVE y posteriormente adquirir un bloqueo ACCESS SHARE
sobre la misma tabla). Muchos transactions pueden mantener simultáneamente modos de bloqueo que no entren en
conflicto. Fíjate en particular en que algunos modos de bloqueo entran en conflicto consigo mismos
(por ejemplo, un bloqueo ACCESS EXCLUSIVE no puede ser mantenido por más de una transacción
a la vez) mientras que otros no (por ejemplo, un bloqueo ACCESS SHARE puede ser mantenido
por varias transacciones).
Modos de bloqueo a nivel de tabla
ACCESS SHARE (AccessShareLock)
Entra en conflicto únicamente con el modo de bloqueo ACCESS EXCLUSIVE.
El comando SELECT adquiere un bloqueo de este modo en las tablas referenciadas.
En general, cualquier consulta que solo lea una tabla y no la modifique adquirirá
este modo de bloqueo.
ROW SHARE (RowShareLock)
Entra en conflicto con los modos de bloqueo EXCLUSIVE y
ACCESS EXCLUSIVE.
El comando SELECT adquiere un bloqueo de este modo en todas las tablas en las que
se especifica una de las opciones FOR UPDATE, FOR NO KEY UPDATE,
FOR SHARE o FOR KEY SHARE (además de bloqueos
ACCESS SHARE en cualquier otra tabla que esté referenciada sin ninguna opción
explícita de bloqueo FOR ...).
ROW EXCLUSIVE (RowExclusiveLock)
Entra en conflicto con los modos de bloqueo SHARE, SHARE ROW
EXCLUSIVE, EXCLUSIVE y ACCESS EXCLUSIVE.
Los comandos UPDATE, DELETE, INSERT y
MERGE adquieren este modo de bloqueo en la tabla objetivo (además de bloqueos
ACCESS SHARE en cualquier otra tabla referenciada). En general, este modo de bloqueo
será adquirido por cualquier comando que modifique datos en una tabla.
SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)
Entra en conflicto con los modos de bloqueo SHARE UPDATE EXCLUSIVE,
SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE
y ACCESS EXCLUSIVE.
Este modo protege una tabla contra cambios concurrentes de esquema y ejecuciones de VACUUM.
Adquirido por VACUUM (sin FULL), ANALYZE,
CREATE INDEX CONCURRENTLY, CREATE STATISTICS,
COMMENT ON, REINDEX CONCURRENTLY y ciertas variantes de
ALTER INDEX y
ALTER TABLE
(para más detalles consulta la documentación de estos comandos).
SHARE (ShareLock)
Entra en conflicto con los modos de bloqueo ROW EXCLUSIVE,
SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE,
EXCLUSIVE y ACCESS EXCLUSIVE.
Este modo protege una tabla contra cambios de datos concurrentes.
Adquirido por CREATE INDEX (sin CONCURRENTLY).
SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)
Entra en conflicto con los modos de bloqueo ROW EXCLUSIVE,
SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW
EXCLUSIVE, EXCLUSIVE y ACCESS EXCLUSIVE.
Este modo protege una tabla contra cambios de datos concurrentes y es auto-exclusivo, de modo que
solo una sesión puede mantenerlo a la vez.
Adquirido por CREATE TRIGGER y algunas formas de
ALTER TABLE.
EXCLUSIVE (ExclusiveLock)
Entra en conflicto con los modos de bloqueo ROW SHARE, ROW
EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE,
SHARE ROW EXCLUSIVE, EXCLUSIVE y
ACCESS EXCLUSIVE.
Este modo solo permite bloqueos ACCESS SHARE concurrentes, es decir, solo las
lecturas de la tabla pueden proceder en paralelo con una transacción que mantenga este modo de bloqueo.
Adquirido por REFRESH MATERIALIZED VIEW CONCURRENTLY.
ACCESS EXCLUSIVE (AccessExclusiveLock)
Entra en conflicto con los bloqueos de todos los modos (ACCESS SHARE,
ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE,
SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE y
ACCESS EXCLUSIVE).
Este modo garantiza que el mantenedor es la única transacción que está accediendo a la tabla de cualquier forma.
Adquirido por los comandos DROP TABLE, TRUNCATE,
REINDEX, CLUSTER, VACUUM FULL y
REFRESH MATERIALIZED VIEW (sin CONCURRENTLY).
Muchas formas de ALTER INDEX y ALTER TABLE también adquieren
un bloqueo en este nivel. Este es también el modo de bloqueo por defecto para las sentencias
LOCK TABLE que no especifican explícitamente un modo.
Solo un bloqueo ACCESS EXCLUSIVE bloquea una sentencia SELECT
(sin FOR UPDATE/SHARE).
Una vez adquirido, un bloqueo se mantiene normalmente hasta el final de la transacción. Pero si un bloqueo
se adquiere después de establecer un punto de salvaguarda (savepoint), el bloqueo se libera inmediatamente
si se revierte a dicho punto. Esto es coherente con el principio de que ROLLBACK cancela
todos los efectos de los comandos desde el punto de salvaguarda. Lo mismo ocurre con los bloqueos adquiridos
dentro de un bloque de excepción de PL/pgSQL: un escape por error del bloque
libera los bloqueos adquiridos dentro del mismo.
Table 13.2. Compatibilidad de modos de bloqueo
| Modo de bloqueo solicitado | Modo de bloqueo existente | |||||||
|---|---|---|---|---|---|---|---|---|
ACCESS SHARE | ROW SHARE | ROW EXCL. | SHARE UPDATE EXCL. | SHARE | SHARE ROW EXCL. | EXCL. | ACCESS EXCL. | |
ACCESS SHARE | X | |||||||
ROW SHARE | X | X | ||||||
ROW EXCL. | X | X | X | X | ||||
SHARE UPDATE EXCL. | X | X | X | X | X | |||
SHARE | X | X | X | X | X | |||
SHARE ROW EXCL. | X | X | X | X | X | X | ||
EXCL. | X | X | X | X | X | X | X | |
ACCESS EXCL. | X | X | X | X | X | X | X | X |
Además de los bloqueos a nivel de tabla, existen bloqueos a nivel de fila, los cuales se enumeran a continuación con los contextos en los que los utiliza automáticamente PostgreSQL. Consulta la la Table 13.3 para ver una tabla completa de los conflictos de bloqueos a nivel de fila. Ten en cuenta que una transacción puede mantener bloqueos en conflicto sobre la misma fila, incluso en subtransacciones diferentes; pero aparte de eso, dos transacciones nunca pueden mantener bloqueos en conflicto sobre la misma fila. Los bloqueos a nivel de fila no afectan a las consultas de datos; solo bloquean a los escritores y bloqueadores sobre la misma fila. Los bloqueos a nivel de fila se liberan al finalizar la transacción o durante la reversión de un punto de salvaguarda, al igual que los bloqueos a nivel de tabla.
Modos de bloqueo a nivel de fila
FOR UPDATE
FOR UPDATE hace que las filas recuperadas por la sentencia SELECT
queden bloqueadas como si fuera para una actualización. Esto evita que sean bloqueadas, modificadas o
eliminadas por otras transacciones hasta que finalice la transacción actual. Es decir, otras transacciones
que intenten un UPDATE, DELETE, SELECT FOR UPDATE,
SELECT FOR NO KEY UPDATE, SELECT FOR SHARE o
SELECT FOR KEY SHARE de estas filas se bloquearán hasta que finalice la transacción
actual; a la inversa, SELECT FOR UPDATE esperará por una transacción concurrente
que haya ejecutado cualquiera de esos comandos sobre la misma fila, y luego bloqueará y devolverá la fila
actualizada (o ninguna fila, si fue eliminada). Sin embargo, dentro de una transacción
REPEATABLE READ o SERIALIZABLE, se lanzará un error si una fila
que va a ser bloqueada ha cambiado desde que comenzó la transacción. Para más discusión consulta la
Section 13.4.
El modo de bloqueo FOR UPDATE también es adquirido por cualquier
DELETE en una fila, y también por un UPDATE que modifique los
valores de ciertas columnas. Actualmente, el conjunto de columnas considerado para el caso de
UPDATE son aquellas que tienen un índice único sobre ellas que puede ser utilizado
en una clave foránea (por lo que no se consideran los índices parciales ni los basados en expresiones),
pero esto puede cambiar en el futuro.
FOR NO KEY UPDATE
Se comporta de manera similar a FOR UPDATE, excepto que el bloqueo adquirido es más
débil: este bloqueo no bloqueará los comandos SELECT FOR KEY SHARE que intenten
adquirir un bloqueo sobre las mismas filas. Este modo de bloqueo también es adquirido por cualquier
UPDATE que no adquiera un bloqueo FOR UPDATE.
FOR SHARE
Se comporta de manera similar a FOR NO KEY UPDATE, excepto que adquiere un bloqueo
compartido en lugar de un bloqueo exclusivo en cada fila recuperada. Un bloqueo compartido bloquea a
otras transacciones de realizar un UPDATE, DELETE,
SELECT FOR UPDATE o SELECT FOR NO KEY UPDATE sobre estas filas,
pero no les impide realizar SELECT FOR SHARE o SELECT FOR KEY SHARE.
FOR KEY SHARE
Se comporta de manera similar a FOR SHARE, excepto que el bloqueo es más débil:
SELECT FOR UPDATE queda bloqueado, pero no SELECT FOR NO KEY UPDATE.
Un bloqueo de clave compartida bloquea a otras transacciones de realizar un DELETE
o cualquier UPDATE que cambie los valores de la clave, pero no otros
UPDATE, y tampoco impide SELECT FOR NO KEY UPDATE,
SELECT FOR SHARE o SELECT FOR KEY SHARE.
PostgreSQL no recuerda ninguna información sobre las filas modificadas en memoria,
por lo que no hay límite en el número de filas bloqueadas a la vez. Sin embargo, el bloqueo de una fila
puede causar una escritura en disco, por ejemplo, SELECT FOR UPDATE modifica las filas
seleccionadas para marcarlas como bloqueadas, por lo que provocará escrituras en disco.
Table 13.3. Conflictos de bloqueos a nivel de fila
| Modo de bloqueo solicitado | Modo de bloqueo actual | |||
|---|---|---|---|---|
| FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
| FOR KEY SHARE | X | |||
| FOR SHARE | X | X | ||
| FOR NO KEY UPDATE | X | X | X | |
| FOR UPDATE | X | X | X | X |
Además de los bloqueos a nivel de tabla y de fila, se utilizan bloqueos compartidos/exclusivos a nivel de página para controlar el acceso de lectura/escritura a las páginas de las tablas en el pool de búferes compartidos. Estos bloqueos se liberan inmediatamente después de recuperar o actualizar una fila. Normalmente, los desarrolladores de aplicaciones no necesitan preocuparse por los bloqueos a nivel de página, pero se mencionan aquí para completar la información.
El uso de bloqueos explícitos puede aumentar la probabilidad de callejones sin salida (deadlocks), en los que dos (o más) transacciones mantienen bloqueos que la otra desea. Por ejemplo, si la transacción 1 adquiere un bloqueo exclusivo sobre la tabla A e intenta adquirir un bloqueo exclusivo sobre la tabla B, mientras que la transacción 2 ya ha bloqueado exclusivamente la tabla B y ahora desea un bloqueo exclusivo sobre la tabla A, ninguna de las dos puede proceder. PostgreSQL detecta automáticamente situaciones de callejón sin salida y las resuelve cancelando una de las transacciones implicadas, permitiendo que la otra (u otras) se complete. (Es difícil predecir exactamente qué transacción se cancelará y no se debe confiar en ello).
Ten en cuenta que los callejones sin salida también pueden producirse como resultado de bloqueos a nivel de fila (y, por tanto, pueden producirse incluso si no se utilizan bloqueos explícitos). Considera el caso en el que dos transacciones concurrentes modifican una tabla. La primera transacción ejecuta:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
Esto adquiere un bloqueo a nivel de fila sobre la fila con el número de cuenta especificado. Luego, la segunda transacción ejecuta:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
La primera sentencia UPDATE adquiere con éxito un bloqueo a nivel de fila sobre la fila
especificada, por lo que tiene éxito al actualizar esa fila. Sin embargo, la segunda sentencia
UPDATE descubre que la fila que intenta actualizar ya ha sido bloqueada, por lo que
espera a que finalice la transacción que adquirió el bloqueo. La transacción dos está esperando ahora a que
la transacción uno finalice antes de continuar la ejecución. Ahora, la transacción uno ejecuta:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
La transacción uno intenta adquirir un bloqueo a nivel de fila sobre la fila especificada, pero no puede: la transacción dos ya mantiene dicho bloqueo. Así que espera a que finalice la transacción dos. Por tanto, la transacción uno está bloqueada esperando a la transacción dos, y la transacción dos está bloqueada esperando a la transacción uno: una condición de callejón sin salida. PostgreSQL detectará esta situación y cancelará una de las transacciones.
La mejor defensa contra los callejones sin salida es generalmente evitarlos asegurándose de que todas las aplicaciones que utilizan una base de datos adquieren los bloqueos sobre múltiples objetos en un orden consistente. En el ejemplo anterior, si ambas transacciones hubieran actualizado las filas en el mismo orden, no se habría producido ningún callejón sin salida. También se debe asegurar que el primer bloqueo adquirido sobre un objeto en una transacción sea el modo más restrictivo que se necesitará para ese objeto. Si no es factible verificar esto con antelación, los callejones sin salida se pueden gestionar sobre la marcha reintentando las transacciones que se cancelan debido a callejones sin salida.
Mientras no se detecte ninguna situación de callejón sin salida, una transacción que busque un bloqueo a nivel de tabla o de fila esperará indefinidamente a que se liberen los bloqueos en conflicto. Esto significa que es una mala idea que las aplicaciones mantengan transacciones abiertas durante largos periodos de tiempo (por ejemplo, mientras esperan la entrada del usuario).
PostgreSQL proporciona un medio para crear bloqueos que tienen significados definidos por la aplicación. Se llaman bloqueos de asesoramiento (advisory locks), porque el sistema no impone su uso — depende de la aplicación utilizarlos correctamente. Los bloqueos de asesoramiento pueden ser útiles para estrategias de bloqueo que resulten incómodas para el modelo MVCC. Por ejemplo, un uso común de los bloqueos de asesoramiento es emular las estrategias de bloqueo pesimista típicas de los llamados sistemas de gestión de datos de “archivos planos”. Aunque se podría utilizar una bandera almacenada en una tabla para el mismo propósito, los bloqueos de asesoramiento son más rápidos, evitan el crecimiento de la tabla (bloat) y el servidor los limpia automáticamente al final de la sesión.
Hay dos maneras de adquirir un bloqueo de asesoramiento en PostgreSQL: a nivel de sesión o a nivel de transacción. Una vez adquirido a nivel de sesión, un bloqueo de asesoramiento se mantiene hasta que se libera explícitamente o finaliza la sesión. A diferencia de las solicitudes de bloqueo estándar, las solicitudes de bloqueo de asesoramiento a nivel de sesión no respetan la semántica de las transacciones: un bloqueo adquirido durante una transacción que posteriormente se revierte se seguirá manteniendo después de la reversión, e igualmente un desbloqueo es efectivo incluso si la transacción que realiza la llamada falla más tarde. Un proceso propietario puede adquirir un bloqueo varias veces; para cada solicitud de bloqueo completada debe haber una solicitud de desbloqueo correspondiente antes de que el bloqueo se libere realmente. Por otro lado, las solicitudes de bloqueo a nivel de transacción se comportan más como las solicitudes de bloqueo habituales: se liberan automáticamente al final de la transacción y no existe una operación de desbloqueo explícita. Este comportamiento suele ser más cómodo que el de nivel de sesión para un uso a corto plazo de un bloqueo de asesoramiento. Las solicitudes de bloqueo a nivel de sesión y de transacción para el mismo identificador de bloqueo de asesoramiento se bloquearán entre sí de la forma esperada. Si una sesión ya mantiene un bloqueo de asesoramiento determinado, las solicitudes adicionales de la misma tendrán éxito siempre, incluso si otras sesiones están esperando el bloqueo; esta afirmación es cierta independientemente de si el mantenimiento del bloqueo existente y la nueva solicitud se realizan a nivel de sesión o de transacción.
Como todos los bloqueos en PostgreSQL, una lista completa de los bloqueos de
asesoramiento mantenidos actualmente por cualquier sesión se puede encontrar en la vista del sistema
pg_locks.
Tanto los bloqueos de asesoramiento como los bloqueos habituales se almacenan en un pool de memoria compartida cuyo tamaño está definido por las variables de configuración max_locks_per_transaction y max_connections. Se debe tener cuidado de no agotar esta memoria, o el servidor no podrá conceder ningún bloqueo en absoluto. Esto impone un límite superior al número de bloqueos de asesoramiento que puede conceder el servidor, normalmente entre decenas y cientos de miles, dependiendo de cómo esté configurado el servidor.
En ciertos casos en los que se utilizan métodos de bloqueo de asesoramiento, especialmente en consultas que
involucran cláusulas de ordenación explícita y LIMIT, se debe tener cuidado para controlar
los bloqueos adquiridos debido al orden en que se evalúan las expresiones SQL. Por ejemplo:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- correcto SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- ¡peligro! SELECT pg_advisory_lock(q.id) FROM ( SELECT id FROM foo WHERE id > 12345 LIMIT 100 ) q; -- correcto
En las consultas anteriores, la segunda forma es peligrosa porque no se garantiza que el
LIMIT se aplique antes de que se ejecute la función de bloqueo. Esto podría provocar
que se adquirieran algunos bloqueos que la aplicación no esperaba y, por tanto, no se liberaran (hasta que
finalice la sesión).
Desde el punto de vista de la aplicación, tales bloqueos quedarían colgados, aunque seguirían siendo visibles
en pg_locks.
Las funciones proporcionadas para manipular los bloqueos de asesoramiento se describen en la Section 9.28.10.