13.3. Bloqueo explícito #

13.3.1. Bloqueos a nivel de tabla
13.3.2. Bloqueos a nivel de fila
13.3.3. Bloqueos a nivel de página
13.3.4. Callejones sin salida (Deadlocks)
13.3.5. Bloqueos de asesoramiento (Advisory Locks)

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.

13.3.1. Bloqueos a nivel de tabla #

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.

Tip

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 solicitadoModo de bloqueo existente
ACCESS SHAREROW SHAREROW EXCL.SHARE UPDATE EXCL.SHARESHARE ROW EXCL.EXCL.ACCESS EXCL.
ACCESS SHARE       X
ROW SHARE      XX
ROW EXCL.    XXXX
SHARE UPDATE EXCL.   XXXXX
SHARE  XX XXX
SHARE ROW EXCL.  XXXXXX
EXCL. XXXXXXX
ACCESS EXCL.XXXXXXXX

13.3.2. Bloqueos a nivel de fila #

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 solicitadoModo de bloqueo actual
FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR UPDATE
FOR KEY SHARE   X
FOR SHARE  XX
FOR NO KEY UPDATE XXX
FOR UPDATEXXXX

13.3.3. Bloqueos a nivel de página #

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.

13.3.4. Callejones sin salida (Deadlocks) #

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).

13.3.5. Bloqueos de asesoramiento (Advisory Locks) #

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.