Bloquea en PostgreSQL: 3. Bloquea otros objetos

Ya hemos hablado sobre algunos bloqueos a nivel de objeto (en particular, sobre bloqueos en las relaciones), así como sobre bloqueos a nivel de fila , su relación con los bloqueos de objetos y la cola de espera, que no siempre es honesto.

Hoy tenemos una mezcolanza. Comencemos con los puntos muertos (en realidad, iba a hablar sobre ellos la última vez, pero ese artículo resultó indecentemente largo), luego repasaremos los bloqueos de objetos restantes y hablaremos sobre los bloqueos de predicados en conclusión.

Puntos muertos


Cuando se usan bloqueos, es posible una situación de punto muerto (o punto muerto ). Ocurre cuando una transacción intenta capturar un recurso ya capturado por otra transacción, mientras que otra transacción intenta capturar un recurso capturado por la primera. Esto se ilustra en la figura de la izquierda a continuación: las flechas sólidas muestran los recursos capturados, las flechas discontinuas muestran los intentos de capturar un recurso ya ocupado.

Es conveniente visualizar un punto muerto mediante la construcción de un gráfico de expectativas. Para hacer esto, eliminamos recursos específicos y dejamos solo transacciones, notando qué transacción está esperando. Si el gráfico tiene un contorno (desde la parte superior se puede llegar con las flechas), este es un punto muerto.



Por supuesto, el punto muerto es posible no solo para dos transacciones, sino también para cualquier número mayor.

Si se produce un punto muerto, las transacciones involucradas en él no pueden hacer nada al respecto; esperarán indefinidamente. Por lo tanto, todos los DBMS y PostgreSQL también rastrean automáticamente los puntos muertos.

Sin embargo, la verificación requiere ciertos esfuerzos, que no quiero hacer cada vez que se solicita un nuevo bloqueo (después de todo, los puntos muertos son bastante raros). Por lo tanto, cuando el proceso intenta capturar el bloqueo y no puede, ingresa a la cola y se queda dormido, pero inicia el temporizador por el valor especificado en el parámetro deadlock_timeout (por defecto, 1 segundo). Si el recurso se libera antes, entonces bueno, lo guardamos en la verificación. Pero si después de deadlock_timeout la espera continúa, entonces el proceso de espera se despertará e iniciará una verificación.

Si la verificación (que consiste en construir un gráfico de expectativas y buscar contornos en ella) no reveló puntos muertos, entonces el proceso continúa durmiendo, ahora ya hasta el final.

Anteriormente en los comentarios, me reprocharon con razón por no decir nada sobre el parámetro lock_timeout , que actúa sobre cualquier operador y evita una espera indefinidamente larga: si no se pudo obtener el bloqueo en el tiempo especificado, la declaración finaliza con el error lock_not_available. No debe confundirse con el parámetro Statement_timeout , que limita el tiempo total de ejecución de la instrucción, sin importar si espera un bloqueo o simplemente hace el trabajo.

Si se detecta un punto muerto, entonces una de las transacciones (en la mayoría de los casos, la que inició la verificación) se termina por la fuerza. En este caso, los bloqueos capturados por él se liberan y las transacciones restantes pueden continuar funcionando.

Los puntos muertos generalmente significan que la aplicación no está diseñada correctamente. Hay dos formas de detectar tales situaciones: en primer lugar, aparecerán mensajes en el registro del servidor y, en segundo lugar, aumentará el valor de pg_stat_database.deadlocks.

Ejemplo de punto muerto


Una causa común de puntos muertos es el orden diferente en el que las filas de las tablas están bloqueadas.
Un simple ejemplo. La primera transacción tiene la intención de transferir 100 rublos de la primera cuenta a la segunda. Para hacer esto, primero reduce la primera cuenta:

=> BEGIN; => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1; 
 UPDATE 1 

Al mismo tiempo, la segunda transacción tiene la intención de transferir 10 rublos de la segunda cuenta a la primera. Ella comienza reduciendo el segundo conteo:

 | => BEGIN; | => UPDATE accounts SET amount = amount - 10.00 WHERE acc_no = 2; 
 | UPDATE 1 

Ahora la primera transacción está tratando de aumentar la segunda cuenta, pero descubre que la fila está bloqueada.

 => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2; 

Luego, la segunda transacción intenta aumentar la primera cuenta, pero también está bloqueada.

 | => UPDATE accounts SET amount = amount + 10.00 WHERE acc_no = 1; 

Hay una expectativa cíclica que nunca terminará por sí sola. Después de un segundo, la primera transacción, al no tener acceso al recurso, inicia una comprobación de punto muerto y corta el servidor.

 ERROR: deadlock detected DETAIL: Process 16477 waits for ShareLock on transaction 530695; blocked by process 16513. Process 16513 waits for ShareLock on transaction 530694; blocked by process 16477. HINT: See server log for query details. CONTEXT: while updating tuple (0,2) in relation "accounts" 

Ahora la segunda transacción puede continuar.

 | UPDATE 1 
 | => ROLLBACK; 

 => ROLLBACK; 

La forma correcta de realizar tales operaciones es bloquear los recursos en el mismo orden. Por ejemplo, en este caso, puede bloquear cuentas en orden ascendente de sus números.

Punto muerto para dos comandos ACTUALIZAR


A veces puedes obtener un punto muerto donde, al parecer, no debería ser. Por ejemplo, es conveniente y familiar percibir los comandos SQL como atómicos, pero tome ACTUALIZAR: este comando bloquea las filas a medida que se actualizan. Esto no está sucediendo de inmediato. Por lo tanto, si un comando actualiza las filas en un orden y el otro en otro, pueden quedar bloqueadas.

Es poco probable que se produzca una situación de este tipo, pero sin embargo puede encontrarse. Para la reproducción, crearemos un índice en la columna de cantidad, construido en orden descendente de cantidad:

 => CREATE INDEX ON accounts(amount DESC); 

Para tener tiempo de ver lo que está sucediendo, escribiremos una función que aumente el valor transmitido, pero lentamente, lentamente, por un segundo:

 => CREATE FUNCTION inc_slow(n numeric) RETURNS numeric AS $$ SELECT pg_sleep(1); SELECT n + 100.00; $$ LANGUAGE SQL; 

También necesitamos la extensión pgrowlocks.

 => CREATE EXTENSION pgrowlocks; 

El primer comando ACTUALIZAR actualizará toda la tabla. El plan de ejecución es obvio: un escaneo secuencial:

 | => EXPLAIN (costs off) | UPDATE accounts SET amount = inc_slow(amount); 
 | QUERY PLAN | ---------------------------- | Update on accounts | -> Seq Scan on accounts | (2 rows) 

Dado que las versiones de las filas en la página de nuestra tabla están en el orden creciente de la suma (exactamente como las agregamos), se actualizarán en el mismo orden. Comenzamos la actualización para trabajar.

 | => UPDATE accounts SET amount = inc_slow(amount); 

Mientras tanto, en otra sesión, prohibiremos el uso del escaneo secuencial:

 || => SET enable_seqscan = off; 

En este caso, el planificador decide usar la exploración de índice para la siguiente instrucción UPDATE:

 || => EXPLAIN (costs off) || UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 
 || QUERY PLAN || -------------------------------------------------------- || Update on accounts || -> Index Scan using accounts_amount_idx on accounts || Index Cond: (amount > 100.00) || (3 rows) 

La segunda y tercera filas se encuentran bajo la condición y, dado que el índice se construye en orden descendente, las filas se actualizarán en orden inverso.

Lanzamos la próxima actualización.

 || => UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 

Un vistazo rápido a la página tabular muestra que el primer operador ya ha logrado actualizar la primera fila (0,1) y la segunda, la última (0,3):

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

Otro segundo pase. El primer operador actualizó la segunda línea, y al segundo le gustaría hacer esto, pero no puede.

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,2) locker | 530699 <-    multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 3 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

Ahora a la primera instrucción le gustaría actualizar la última fila de la tabla, pero ya está bloqueada por la segunda. Aquí está el punto muerto.

Una de las transacciones se cancela:

 || ERROR: deadlock detected || DETAIL: Process 16549 waits for ShareLock on transaction 530699; blocked by process 16513. || Process 16513 waits for ShareLock on transaction 530700; blocked by process 16549. || HINT: See server log for query details. || CONTEXT: while updating tuple (0,2) in relation "accounts" 

Y el otro completa la ejecución:

 | UPDATE 3 

Los detalles interesantes sobre la detección y prevención de puntos muertos se pueden encontrar en el administrador de bloqueos README .

Eso se trata de puntos muertos, y procedemos a los bloqueos de objetos restantes.



Cerraduras sin relación


Cuando desea bloquear un recurso que no es una relación en la comprensión de PostgreSQL, se utilizan bloqueos de objetos. Tal recurso puede ser casi cualquier cosa: espacios de tablas, suscripciones, esquemas, roles, tipos de datos enumerados ... En términos generales, todo lo que se puede encontrar en el catálogo del sistema.

Veamos un ejemplo simple. Comenzamos la transacción y creamos una tabla en ella:

 => BEGIN; => CREATE TABLE example(n integer); 

Ahora veamos qué tipo de bloqueos de objetos aparecieron en pg_locks:

 => SELECT database, (SELECT datname FROM pg_database WHERE oid = l.database) AS dbname, classid, (SELECT relname FROM pg_class WHERE oid = l.classid) AS classname, objid, mode, granted FROM pg_locks l WHERE l.locktype = 'object' AND l.pid = pg_backend_pid(); 
  database | dbname | classid | classname | objid | mode | granted ----------+--------+---------+--------------+-------+-----------------+--------- 0 | | 1260 | pg_authid | 16384 | AccessShareLock | t 16386 | test | 2615 | pg_namespace | 2200 | AccessShareLock | t (2 rows) 

Para comprender qué se bloquea exactamente aquí, debe mirar tres campos: base de datos, classid y objid. Comencemos con la primera línea.

La base de datos es el OID de la base de datos a la que pertenece el recurso bloqueado. En nuestro caso, hay cero en esta columna. Esto significa que estamos tratando con un objeto global que no pertenece a ninguna base en particular.

Classid contiene el OID de pg_class, que corresponde al nombre de la tabla de catálogo del sistema, que determina el tipo de recurso. En nuestro caso, pg_authid, es decir, el rol es el recurso (usuario).

Objid contiene el OID de la tabla de catálogo del sistema que classid nos indicó.

 => SELECT rolname FROM pg_authid WHERE oid = 16384; 
  rolname --------- student (1 row) 

Por lo tanto, el rol del estudiante está bloqueado, desde el cual estamos trabajando.

Ahora tratemos con la segunda línea. Se indica la base de datos, y esta es la base de datos de prueba a la que estamos conectados.

Classid apunta a la tabla pg_namespace que contiene los esquemas.

 => SELECT nspname FROM pg_namespace WHERE oid = 2200; 
  nspname --------- public (1 row) 

Por lo tanto, el esquema público está bloqueado.

Entonces, vimos que al crear un objeto, el rol de propietario y el esquema en el que se crea el objeto se bloquean (en modo compartido). Lo cual es lógico: de lo contrario, alguien podría eliminar el rol o el esquema mientras la transacción aún no se haya completado.

 => ROLLBACK; 

Bloqueo de extensión de relación


Cuando aumenta el número de filas en una relación (es decir, en una tabla, índice, vista materializada), PostgreSQL puede usar el espacio libre en las páginas existentes para insertar, pero, obviamente, en algún momento debe agregar nuevas páginas. Físicamente, se agregan al final del archivo correspondiente. Esto se entiende como expandir la relación .

Para evitar que dos procesos se apresuren a agregar páginas al mismo tiempo, este proceso está protegido por un bloqueo especial de tipo extender. El mismo bloqueo se usa al limpiar índices para que otros procesos no puedan agregar páginas durante el escaneo.

Por supuesto, este bloqueo se libera sin esperar el final de la transacción.

Anteriormente, las tablas se expandían solo una página a la vez. Esto causó problemas cuando varios procesos insertaron filas simultáneamente, por lo tanto, en PostgreSQL 9.6, se agregaron varias páginas a las tablas a la vez (en proporción al número de procesos que esperan el bloqueo, pero no más de 512).

Bloqueo de página


Se aplica un bloqueo de nivel de página en el único caso (a excepción de los bloqueos de predicados, que se analizan más adelante).

Los índices GIN le permiten acelerar la búsqueda en valores compuestos, por ejemplo, palabras en documentos de texto (o elementos en matrices). Para una primera aproximación, dichos índices pueden representarse como un árbol B normal, en el que no se almacenan los documentos en sí, sino palabras individuales de estos documentos. Por lo tanto, al agregar un nuevo documento, el índice debe reconstruirse con bastante fuerza, introduciendo en él cada palabra incluida en el documento.

Para mejorar el rendimiento, los índices GIN tienen una función de inserción retrasada que está habilitada por la opción de almacenamiento fastupdate. Primero se agregan rápidamente nuevas palabras a la lista pendiente desordenada, y después de un tiempo, todo lo que se ha acumulado se mueve a la estructura de índice principal. Los ahorros se deben al hecho de que es probable que diferentes documentos contengan palabras duplicadas.

Para evitar que varios procesos pasen de la lista de espera al índice principal al mismo tiempo, la metapágina del índice se bloquea en modo exclusivo durante la transferencia. Esto no interfiere con el uso del índice en modo normal.

Cerraduras de asesoramiento


A diferencia de otros bloqueos (como los bloqueos de relación), los bloqueos de aviso nunca se configuran automáticamente, son administrados por el desarrollador de la aplicación. Son convenientes de usar, por ejemplo, si una aplicación necesita una lógica de bloqueo para algún propósito que no se ajusta a la lógica estándar de los bloqueos ordinarios.

Supongamos que tenemos un recurso condicional que no corresponde a ningún objeto de la base de datos (que podríamos bloquear con comandos como SELECT FOR o LOCK TABLE). Necesitas encontrar un identificador numérico para ello. Si el recurso tiene un nombre único, entonces una opción simple es tomarle un código hash:

 => SELECT hashtext('1'); 
  hashtext ----------- 243773337 (1 row) 

Así es como capturamos la cerradura:

 => BEGIN; => SELECT pg_advisory_lock(hashtext('1')); 

Como de costumbre, la información de bloqueo está disponible en pg_locks:

 => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

Para que un bloqueo funcione realmente, otros procesos también deben obtener un bloqueo antes de acceder al recurso. Obviamente, la aplicación debe garantizar el cumplimiento de esta regla.

En el ejemplo anterior, el bloqueo es válido hasta el final de la sesión, y no la transacción, como de costumbre.

 => COMMIT; => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

Debe ser lanzado explícitamente:

 => SELECT pg_advisory_unlock(hashtext('1')); 

Hay un gran conjunto de funciones para trabajar con bloqueos de aviso para todas las ocasiones:

  • pg_advisory_lock_shared trata un bloqueo compartido,
  • pg_advisory_xact_lock (y pg_advisory_xact_lock_shared) obtiene un bloqueo hasta el final de la transacción,
  • pg_try_advisory_lock (así como pg_try_advisory_xact_lock y pg_try_advisory_xact_lock_shared) no espera recibir un bloqueo, pero devuelve un valor falso si el bloqueo no se pudo obtener de inmediato.

El conjunto de funciones de prueba proporciona otra forma de no esperar un bloqueo, además de las enumeradas en un artículo anterior .

Cerraduras de predicado


El término bloqueo de predicados apareció hace mucho tiempo, en los primeros intentos de implementar un aislamiento completo basado en bloqueos en los primeros DBMS (el nivel es serializable, aunque el estándar SQL no existía en ese momento). El problema que se encontró luego fue que incluso el bloqueo de todas las líneas leídas y modificadas no proporciona un aislamiento completo: pueden aparecer nuevas líneas en la tabla que caen bajo las mismas condiciones de selección, lo que conduce a fantasmas (consulte el artículo sobre aislamiento ) .

La idea de los bloqueos de predicados era bloquear predicados, no filas. Si, al ejecutar una consulta con la condición a > 10, el predicado a > 10 está bloqueado, esto no agregará nuevas filas a la tabla que se encuentren bajo la condición y evitará fantasmas. El problema es que, en el caso general, esta es una tarea computacionalmente difícil; en la práctica, se puede resolver solo para predicados que tienen una forma muy simple.

En PostgreSQL, la capa serializable se implementa de manera diferente, además del aislamiento basado en instantáneas existente. El término bloqueo de predicados permanece, pero su significado ha cambiado radicalmente. De hecho, tales "bloqueos" no bloquean nada, pero se utilizan para rastrear las dependencias de datos entre transacciones.

Está demostrado que el aislamiento basado en imágenes permite una anomalía de grabación inconsistente y una anomalía de solo una transacción de lectura , pero no son posibles otras anomalías. Para comprender que estamos lidiando con una de las dos anomalías enumeradas, podemos analizar las dependencias entre transacciones y encontrar ciertos patrones en ellas.

Estamos interesados ​​en dos tipos de dependencias:

  • una transacción lee una fila, que luego es cambiada por otra transacción (dependencia RW),
  • una transacción modifica la línea que luego lee otra transacción (dependencia WR).

Las dependencias WR se pueden rastrear utilizando bloqueos convencionales existentes, pero las dependencias RW solo tienen que rastrearse adicionalmente.

Repito una vez más: a pesar del nombre, los bloqueos de predicados no bloquean nada. En cambio, cuando se confirma una transacción, se realiza una verificación y, si se detecta una secuencia de dependencias "incorrecta" que puede indicar una anomalía, la transacción se rompe.

Veamos cómo se produce la instalación de bloqueos de predicados. Para hacer esto, cree una tabla con un número suficientemente grande de filas y un índice en ella.

 => CREATE TABLE pred(n integer); => INSERT INTO pred(n) SELECT gn FROM generate_series(1,10000) g(n); => CREATE INDEX ON pred(n) WITH (fillfactor = 10); => ANALYZE pred; 

Si la consulta se ejecuta mediante exploración secuencial de toda la tabla, el bloqueo del predicado se establece en toda la tabla (incluso si no todas las filas caen bajo las condiciones de filtrado).

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 12763 | (1 row) 

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n > 100; 
 | QUERY PLAN | ---------------------------------------------------------------- | Seq Scan on pred (actual time=0.047..12.709 rows=9900 loops=1) | Filter: (n > 100) | Rows Removed by Filter: 100 | Planning Time: 0.190 ms | Execution Time: 15.244 ms | (5 rows) 

Todos los bloqueos de predicados siempre se capturan en un modo especial SIReadLock (Lectura de aislamiento serializable):

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+----------+------+------- relation | pred | | (1 row) 

 | => ROLLBACK; 

Pero si la consulta se ejecuta mediante exploración de índice, la situación cambia para mejor. Si hablamos del árbol B, entonces es suficiente establecer el bloqueo en las filas de la tabla de lectura y en las páginas escaneadas del índice, por lo que bloqueamos no solo valores específicos, sino también todo el rango leído.

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.122..0.131 rows=2 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1001)) | Heap Fetches: 2 | Planning Time: 0.096 ms | Execution Time: 0.153 ms | (5 rows) 

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- tuple | pred | 3 | 236 tuple | pred | 3 | 235 page | pred_n_idx | 22 | (3 rows) 

Puede notar varias dificultades.

En primer lugar, se crea un bloqueo por separado para cada versión de la línea que se lee, pero potencialmente puede haber muchas de esas versiones. El número total de bloqueos de predicados en el sistema está limitado por el producto de los valores de los parámetros max_pred_locks_per_transaction × max_connections (los valores predeterminados son 64 y 100, respectivamente). La memoria para tales bloqueos se asigna al inicio del servidor; intentar superar este número dará lugar a errores.

Por lo tanto, para bloqueos de predicados (¡y solo para ellos!), Se utiliza un aumento de nivel . Antes de PostgreSQL 10, había restricciones que estaban cableadas en el código y, a partir de él, puede controlar los parámetros elevando el nivel. Si el número de bloqueos de versión de fila por fila es mayor que max_pred_locks_per_page , dichos bloqueos se reemplazan con un bloqueo de nivel de página. Aquí hay un ejemplo:

 => SHOW max_pred_locks_per_page; 
  max_pred_locks_per_page ------------------------- 2 (1 row) 

 | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1002; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.019..0.039 rows=3 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1002)) | Heap Fetches: 3 | Planning Time: 0.069 ms | Execution Time: 0.057 ms | (5 rows) 

En lugar de tres bloqueos de tupla, vemos un tipo de página:

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 22 | (2 rows) 

Del mismo modo, si el número de bloqueos de página asociados con una sola relación excede max_pred_locks_per_relation , dichos bloqueos se reemplazan con un bloqueo de nivel de relación.

No hay otros niveles: los bloqueos de predicados solo se capturan para las relaciones, páginas o versiones de fila, y siempre con el modo SIReadLock.

Por supuesto, un aumento en el nivel de bloqueos conduce inevitablemente al hecho de que un mayor número de transacciones dará como resultado falsamente un error de serialización y, como resultado, el rendimiento del sistema disminuirá. Aquí debe buscar un equilibrio entre el consumo de memoria y el rendimiento.

La segunda dificultad es que en varias operaciones con el índice (por ejemplo, debido a la división de las páginas de índice al insertar nuevas líneas), el número de páginas de hoja que cubren el rango de lectura puede cambiar. Pero la implementación de esto tiene en cuenta:

 => INSERT INTO pred SELECT 1001 FROM generate_series(1,1000); => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 211 | page | pred_n_idx | 212 | page | pred_n_idx | 22 | (4 rows) 

 | => ROLLBACK; 

Por cierto, los bloqueos de predicados no siempre se eliminan inmediatamente después de completar la transacción, ya que son necesarios para rastrear las dependencias entre varias transacciones. Pero en cualquier caso, se gestionan automáticamente.

No todos los tipos de índice en PostgreSQL admiten bloqueos de predicados. Anteriormente, solo los árboles B podían presumir de esto, pero en PostgreSQL 11 la situación mejoró: se agregaron índices hash, GiST y GIN a la lista. Si se utiliza el acceso al índice, y el índice no funciona con bloqueos de predicados, entonces todo el índice está bloqueado en el bloqueo. Por supuesto, esto también aumenta el número de saltos falsos de transacciones.

En conclusión, observo que es con el uso de bloqueos de predicados que existe una restricción que, para garantizar un aislamiento completo, todas las transacciones deben funcionar en el nivel Serializable. Si una transacción usa un nivel diferente, simplemente no establecerá (y comprobará) bloqueos de predicados.

Por tradición, dejaré un enlace a README en bloqueos de predicados , desde el cual puede comenzar a estudiar el código fuente.

Continuará

Source: https://habr.com/ru/post/465263/


All Articles