Já falamos sobre alguns
bloqueios no nível do objeto (em particular, sobre bloqueios nas relações), bem como sobre
bloqueios no nível da linha , seu relacionamento com bloqueios de objetos e a fila de espera, o que nem sempre é honesto.
Hoje temos uma mistura. Vamos começar com os
impasses (na verdade, eu ia falar sobre eles da última vez, mas esse artigo ficou indecentemente longo), depois examinaremos os
bloqueios de objetos restantes e falaremos sobre
bloqueios de predicados em conclusão.
Deadlocks
Ao usar bloqueios, é possível uma situação de
conflito (ou
conflito ). Ocorre quando uma transação tenta capturar um recurso já capturado por outra transação, enquanto outra transação tenta capturar um recurso capturado pela primeira. Isso é ilustrado na figura à esquerda abaixo: setas sólidas mostram recursos capturados, setas tracejadas mostram tentativas de capturar um recurso já ocupado.
É conveniente visualizar um impasse construindo um gráfico de expectativas. Para isso, removemos recursos específicos e deixamos apenas transações, observando qual transação está aguardando. Se o gráfico tiver um contorno (a partir do topo, você pode acessá-lo pelas setas) - isso é um impasse.

Obviamente, o impasse é possível não apenas para duas transações, mas também para qualquer número maior.
Se ocorrer um impasse, as transações envolvidas não poderão fazer nada a respeito - elas esperarão indefinidamente. Portanto, todos os DBMSs e PostgreSQL também rastreiam automaticamente os deadlocks.
No entanto, a verificação exige alguns esforços, que eu não quero fazer sempre que um novo bloqueio é solicitado (afinal, os bloqueios são bastante raros). Portanto, quando o processo tenta capturar o bloqueio e não pode, ele entra na fila e adormece, mas inicia o timer com o valor especificado no parâmetro
deadlock_timeout (por padrão - 1 segundo). Se o recurso for liberado mais cedo, tudo bem, salvamos na verificação. Mas se após o
deadlock_timeout a espera continuar, o processo de espera será despertado e iniciará uma verificação.
Se a verificação (que consiste na construção de um gráfico de expectativas e na busca de contornos) não revelou impasses, o processo continua adormecido - agora já com um final amargo.
Nos comentários anteriores, fui criticado com razão por não dizer nada sobre o parâmetro lock_timeout , que atua em qualquer operador e evita uma espera indefinidamente longa: se o bloqueio não puder ser obtido no tempo especificado, a instrução termina com o erro lock_not_available. Ele não deve ser confundido com o parâmetro statement_timeout , que limita o tempo total de execução da instrução, independentemente de esperar um bloqueio ou apenas executar o trabalho.
Se um impasse for detectado, uma das transações (na maioria dos casos, a que iniciou a verificação) será forçada a terminar. Nesse caso, os bloqueios capturados por ele são liberados e as transações restantes podem continuar funcionando.
Os conflitos geralmente significam que o aplicativo não foi projetado corretamente. Há duas maneiras de detectar tais situações: primeiro, as mensagens aparecerão no log do servidor e, segundo, o valor de pg_stat_database.deadlocks aumentará.
Exemplo de impasse
Uma causa comum de deadlocks é a ordem diferente na qual as linhas nas tabelas são bloqueadas.
Um exemplo simples. A primeira transação pretende transferir 100 rublos da primeira conta para a segunda. Para fazer isso, ela primeiro reduz a primeira contagem:
=> BEGIN; => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1;
UPDATE 1
Ao mesmo tempo, a segunda transação pretende transferir 10 rublos da segunda conta para a primeira. Ela começa reduzindo a segunda contagem:
| => BEGIN; | => UPDATE accounts SET amount = amount - 10.00 WHERE acc_no = 2;
| UPDATE 1
Agora a primeira transação está tentando aumentar a segunda conta, mas descobre que a linha está bloqueada.
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2;
Em seguida, a segunda transação tenta aumentar a primeira conta, mas também é bloqueada.
| => UPDATE accounts SET amount = amount + 10.00 WHERE acc_no = 1;
Há uma expectativa cíclica que nunca termina por si própria. Após um segundo, a primeira transação, sem acesso ao recurso, inicia uma verificação de conflito e interrompe o 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"
Agora a segunda transação pode continuar.
| UPDATE 1
| => ROLLBACK;
=> ROLLBACK;
A maneira correta de executar essas operações é bloquear recursos na mesma ordem. Por exemplo, nesse caso, você pode bloquear contas em ordem crescente de seus números.
Impasse para dois comandos UPDATE
Às vezes, você pode obter um impasse onde, ao que parece, não deveria estar. Por exemplo, é conveniente e familiar perceber os comandos SQL como atômicos, mas use UPDATE - esse comando bloqueia as linhas à medida que elas são atualizadas. Isso não está acontecendo de uma só vez. Portanto, se um comando atualizar as linhas em uma ordem e a outra em outra, elas poderão estar em um impasse.
É improvável que haja uma situação dessas, mas, mesmo assim, pode se encontrar. Para a reprodução, criaremos um índice na coluna da quantidade, construída em ordem decrescente da quantidade:
=> CREATE INDEX ON accounts(amount DESC);
Para ter tempo de ver o que está acontecendo, escreveremos uma função que aumenta o valor transmitido, mas lenta, lentamente, por um segundo:
=> CREATE FUNCTION inc_slow(n numeric) RETURNS numeric AS $$ SELECT pg_sleep(1); SELECT n + 100.00; $$ LANGUAGE SQL;
Também precisamos da extensão pgrowlocks.
=> CREATE EXTENSION pgrowlocks;
O primeiro comando UPDATE atualizará a tabela inteira. O plano de execução é óbvio - uma varredura seqüencial:
| => EXPLAIN (costs off) | UPDATE accounts SET amount = inc_slow(amount);
| QUERY PLAN | ---------------------------- | Update on accounts | -> Seq Scan on accounts | (2 rows)
Como as versões das linhas na página da nossa tabela estão na ordem crescente da soma (exatamente como as adicionamos), elas serão atualizadas na mesma ordem. Começamos a atualização para funcionar.
| => UPDATE accounts SET amount = inc_slow(amount);
Enquanto isso, em outra sessão, proibiremos o uso de varredura seqüencial:
|| => SET enable_seqscan = off;
Nesse caso, o planejador decide usar a verificação de índice para a seguinte instrução 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)
A segunda e terceira linhas se enquadram na condição e, como o índice é construído em ordem decrescente, as linhas serão atualizadas na ordem inversa.
Lançamos a próxima atualização.
|| => UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00;
Uma rápida olhada na página tabular mostra que o primeiro operador já conseguiu atualizar a primeira linha (0,1) e o segundo - o último (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}
Mais um segundo passa. O primeiro operador atualizou a segunda linha e o segundo gostaria de fazer isso, mas não pode.
=> 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}
Agora, a primeira instrução gostaria de atualizar a última linha da tabela, mas já está bloqueada pela segunda. Aqui está o impasse.
Uma das transações é abortada:
|| 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"
E o outro completa a execução:
| UPDATE 3
Detalhes interessantes sobre a detecção e prevenção de conflitos podem ser encontrados no gerenciador de bloqueios README .
Isso é tudo sobre deadlocks, e prosseguimos para os bloqueios de objetos restantes.

Fechaduras sem relação
Quando você deseja bloquear um recurso que não é uma
relação no entendimento do PostgreSQL, bloqueios de objetos são usados. Esse recurso pode ser quase qualquer coisa: espaços de tabela, assinaturas, esquemas, funções, tipos de dados enumerados ... Grosso modo, tudo o que pode ser encontrado no catálogo do sistema.
Vejamos um exemplo simples. Iniciamos a transação e criamos uma tabela nela:
=> BEGIN; => CREATE TABLE example(n integer);
Agora vamos ver que tipo de objeto bloqueios apareceu em 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 entender exatamente o que está bloqueado aqui, é necessário examinar três campos: banco de dados, classid e objid. Vamos começar com a primeira linha.
Banco de dados é o OID do banco de dados ao qual o recurso bloqueado pertence. No nosso caso, há zero nesta coluna. Isso significa que estamos lidando com um objeto global que não pertence a nenhuma base específica.
Classid contém o OID de pg_class, que corresponde ao nome da tabela de catálogos do sistema, que determina o tipo de recurso. No nosso caso, pg_authid, ou seja, a função é o recurso (usuário).
Objid contém o OID da tabela de catálogo do sistema que classid nos indicou.
=> SELECT rolname FROM pg_authid WHERE oid = 16384;
rolname --------- student (1 row)
Assim, o papel do aluno é bloqueado, do qual estamos trabalhando.
Agora vamos lidar com a segunda linha. O banco de dados é indicado e este é o banco de dados de teste ao qual estamos conectados.
Classid aponta para a tabela pg_namespace que contém os esquemas.
=> SELECT nspname FROM pg_namespace WHERE oid = 2200;
nspname --------- public (1 row)
Assim, o esquema público está bloqueado.
Portanto, vimos que, ao criar um objeto, a função de proprietário e o esquema no qual o objeto é criado são bloqueados (no modo compartilhado). O que é lógico: caso contrário, alguém poderá remover a função ou o esquema enquanto a transação ainda não estiver concluída.
=> ROLLBACK;
Bloqueio de extensão de relacionamento
Quando o número de linhas em uma relação (ou seja, em uma tabela, índice, visualização materializada) aumenta, o PostgreSQL pode usar o espaço livre nas páginas existentes para inserir, mas, obviamente, em algum momento você precisa adicionar novas páginas. Fisicamente, eles são adicionados ao final do arquivo correspondente. Isso é entendido como
expandir o relacionamento .
Para impedir que dois processos se apressem para adicionar páginas ao mesmo tempo, esse processo é protegido por um bloqueio especial do tipo extend. O mesmo bloqueio é usado ao limpar índices para que outros processos não possam adicionar páginas durante a digitalização.
Obviamente, esse bloqueio é liberado sem aguardar o final da transação.
Anteriormente, as tabelas eram expandidas apenas uma página por vez. Isso causou problemas quando vários processos inseriram linhas simultaneamente; portanto, no PostgreSQL 9.6, várias páginas foram adicionadas às tabelas de uma só vez (na proporção do número de processos aguardando o bloqueio, mas não mais que 512).
Bloqueio de página
Um bloqueio no nível da página é aplicado no único caso (exceto os bloqueios de predicado, que serão discutidos posteriormente).
Os índices GIN permitem acelerar a pesquisa em valores compostos, por exemplo, palavras em documentos de texto (ou elementos em matrizes). Para uma primeira aproximação, esses índices podem ser representados como uma árvore B comum, na qual não são armazenados os próprios documentos, mas palavras individuais desses documentos. Portanto, ao adicionar um novo documento, o índice precisa ser reconstruído com bastante força, introduzindo nele todas as palavras incluídas no documento.
Para melhorar o desempenho, os índices GIN têm um recurso de inserção atrasada que é ativado pela opção de armazenamento fastupdate. Novas palavras são adicionadas rapidamente à lista pendente não ordenada e, após algum tempo, tudo o que acumulou é movido para a estrutura principal do índice. A economia se deve ao fato de documentos diferentes provavelmente conterem palavras duplicadas.
Para excluir vários processos da passagem da lista de espera para o índice principal ao mesmo tempo, a meta página de índice é bloqueada no modo exclusivo durante a transferência. Isso não interfere no uso do índice no modo normal.
Bloqueios consultivos
Ao contrário de outros bloqueios (como bloqueios de relacionamento), os bloqueios consultivos nunca são definidos automaticamente, eles são gerenciados pelo desenvolvedor do aplicativo. Eles são convenientes de usar, por exemplo, se um aplicativo precisar de lógica de bloqueio para algum propósito que não se encaixe na lógica padrão de bloqueios comuns.
Suponha que tenhamos um recurso condicional que não corresponde a nenhum objeto de banco de dados (que poderíamos bloquear com comandos como SELECT FOR ou LOCK TABLE). Você precisa criar um identificador numérico para ele. Se o recurso tiver um nome exclusivo, uma opção simples é obter um código de hash:
=> SELECT hashtext('1');
hashtext ----------- 243773337 (1 row)
É assim que capturamos o bloqueio:
=> BEGIN; => SELECT pg_advisory_lock(hashtext('1'));
Como sempre, as informações de bloqueio estão disponíveis em 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 um bloqueio realmente funcione, outros processos também devem obter um bloqueio antes de acessar o recurso. Obviamente, o cumprimento desta regra deve ser garantido pelo aplicativo.
No exemplo acima, o bloqueio é válido até o final da sessão, e não a transação, como de costume.
=> 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)
Ele deve ser liberado explicitamente:
=> SELECT pg_advisory_unlock(hashtext('1'));
Há um grande conjunto de funções para trabalhar com bloqueios consultivos para todas as ocasiões:
- pg_advisory_lock_shared trata um bloqueio compartilhado,
- pg_advisory_xact_lock (e pg_advisory_xact_lock_shared) recebe um bloqueio até o final da transação,
- pg_try_advisory_lock (assim como pg_try_advisory_xact_lock e pg_try_advisory_xact_lock_shared) não espera receber um bloqueio, mas retorna um valor falso se o bloqueio não puder ser obtido imediatamente.
O conjunto de funções try fornece outra maneira de não esperar um bloqueio, além das listadas
em um artigo anterior .
Bloqueios de predicado
O termo
bloqueio de predicado apareceu há muito tempo, nas primeiras tentativas de implementar o isolamento completo com base em bloqueios nos DBMSs anteriores (o nível é serializável, embora o padrão SQL não existisse naquele momento). O problema que foi encontrado foi que mesmo o bloqueio de todas as linhas lidas e alteradas não fornece isolamento completo:
novas linhas podem aparecer na tabela que se enquadram nas mesmas condições de seleção, o que leva a
fantasmas (consulte o
artigo sobre isolamento ) .
A idéia de bloqueios de predicado era bloquear predicados, não linhas. Se, ao executar uma consulta com a condição
a > 10, o predicado
a > 10 for bloqueado, isso não adicionará novas linhas à tabela que se enquadram na condição e evitará fantasmas. O problema é que, no caso geral, essa é uma tarefa computacionalmente difícil; na prática, ele pode ser resolvido apenas para predicados que tenham uma forma muito simples.
No PostgreSQL, a camada Serializable é implementada de maneira diferente, além do isolamento existente baseado em snapshots. O termo
bloqueio de predicado permanece, mas seu significado mudou radicalmente. De fato, esses "bloqueios" não bloqueiam nada, mas são usados para rastrear dependências de dados entre transações.
Está provado que o isolamento baseado em imagens permite uma
anomalia de gravação inconsistente e uma
anomalia de apenas uma transação de leitura , mas nenhuma outra anomalia é possível. Para entender que estamos lidando com uma das duas anomalias listadas, podemos analisar as dependências entre transações e encontrar certos padrões nelas.
Estamos interessados em dois tipos de dependências:
- uma transação lê uma linha, que é alterada por outra transação (dependência de RW),
- uma transação modifica a linha que outra transação lê (dependência WR).
As dependências de WR podem ser rastreadas usando bloqueios convencionais existentes, mas as dependências de RW precisam apenas rastrear adicionalmente.
Repito mais uma vez: apesar do nome, os bloqueios de predicado não bloqueiam nada. Em vez disso, quando uma transação é confirmada, uma verificação é executada e, se uma sequência de dependência "ruim" for detectada e indicar uma anomalia, a transação será interrompida.
Vamos ver como ocorre a instalação dos bloqueios de predicado. Para fazer isso, crie uma tabela com um número suficientemente grande de linhas e um índice nela.
=> 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;
Se a consulta for executada por varredura seqüencial de toda a tabela, o bloqueio de predicado será definido em toda a tabela (mesmo que nem todas as linhas se enquadrem nas condições de filtragem).
| => 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)
Quaisquer bloqueios de predicado são sempre capturados em um modo especial SIReadLock (leitura serializada de isolamento):
=> SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763;
locktype | relation | page | tuple ----------+----------+------+------- relation | pred | | (1 row)
| => ROLLBACK;
Mas se a consulta for executada usando a verificação de índice, a situação mudará para melhor. Se falamos sobre a árvore B, basta definir o bloqueio nas linhas da tabela de leitura e nas páginas frondosas do índice - assim, bloqueamos não apenas valores específicos, mas também toda a faixa lida.
| => 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)
Você pode perceber várias dificuldades.
Primeiramente, um bloqueio separado é criado para cada versão da linha lida, mas é possível que exista muitas dessas versões. O número total de bloqueios de predicado no sistema é limitado pelo produto dos valores do parâmetro
max_pred_locks_per_transaction ×
max_connections (os valores padrão são 64 e 100, respectivamente). A memória para esses bloqueios é alocada na inicialização do servidor; tentar exceder esse número resultará em erros.
Portanto, para bloqueios de predicado (e apenas para eles!),
É usado um
aumento de nível . Antes do PostgreSQL 10, havia restrições conectadas ao código e, começando com ele, você pode controlar os parâmetros aumentando o nível. Se o número de bloqueios de versão de linha
por linha for maior que
max_pred_locks_per_page , esses bloqueios serão substituídos por um bloqueio no nível da página. Aqui está um exemplo:
=> 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)
Em vez de três bloqueios de tupla, vemos um 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)
Da mesma forma, se o número de bloqueios de página
associados a um único relacionamento exceder
max_pred_locks_per_relation , esses bloqueios serão substituídos por um bloqueio no nível de relacionamento.
Não há outros níveis: os bloqueios de predicado são capturados apenas para relacionamentos, páginas ou versões de linha e sempre com o modo SIReadLock.
Obviamente, um aumento no nível de bloqueios inevitavelmente leva ao fato de que um número maior de transações resultará falsamente em um erro de serialização e, como resultado, a taxa de transferência do sistema diminuirá. Aqui você precisa procurar um equilíbrio entre o consumo de memória e o desempenho.
A segunda dificuldade é que, em várias operações com o índice (por exemplo, devido à divisão das páginas de índice ao inserir novas linhas), o número de páginas de folha que cobrem o intervalo de leitura pode mudar. Mas a implementação disso leva em consideração:
=> 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;
A propósito, os bloqueios de predicado nem sempre são removidos imediatamente após a conclusão da transação, porque são necessários para rastrear as dependências entre
várias transações. Mas, em qualquer caso, eles são gerenciados automaticamente.
Nem todos os tipos de índice no PostgreSQL suportam bloqueios de predicado. Anteriormente, apenas as árvores B podiam se gabar disso, mas no PostgreSQL 11 a situação melhorava: índices de hash, GiST e GIN foram adicionados à lista. Se o acesso ao índice for usado e o índice não funcionar com bloqueios de predicado, o índice inteiro será bloqueado. Obviamente, isso também aumenta o número de quebras de transações falsas.
Concluindo, observo que é com o uso de bloqueios de predicado que há uma restrição que, para garantir o isolamento completo,
todas as transações devem funcionar no nível serializável. Se uma transação usa um nível diferente, simplesmente não define (e verifica) bloqueios de predicado.
Por tradição, deixarei um link para o README sobre bloqueios de predicado , a partir do qual você pode começar a estudar o código-fonte.
Para ser continuado .