Fechaduras consultivas fantásticas e onde elas moram

O PostgreSQL possui um mecanismo muito conveniente para bloqueios consultivos , eles também são bloqueios consultivos . Nós da Tensor os usamos em muitos lugares do sistema, mas poucas pessoas entendem em detalhes como exatamente elas funcionam e quais problemas podem ser obtidos se mal tratados .



Leia mais sobre bloqueios

Vantagens dos bloqueios de recomendação


A diferença fundamental entre esse mecanismo e os bloqueios "comuns" de tabela / página / nível de registro é que existem vários recursos principais.

Bloqueio de ID personalizado


Bloqueios “normais” no PG estão sempre vinculados a um objeto de banco de dados específico (tabela, registro, página de dados) e ao processo que serve a conexão. Os bloqueios consultivos também são um processo, mas, em vez de um objeto real, um identificador abstrato pode ser definido como (bigint) ou como (número inteiro, número inteiro) .

A propósito, anexar cada bloqueio a um processo significa que, nomeando-o pg_terminate_backend(pid)ou completando corretamente a conexão no lado do cliente, você pode se livrar de todos os bloqueios impostos por ele.

Verificação CAS para captura de bloqueio


O CAS é um Compare-and-Set , ou seja, a verificação da capacidade de captura e a captura da trava ocorrem como uma operação atômica e, obviamente, ninguém pode ficar entre elas.

Ou seja, se você primeiro solicitar uma verificação para pg_locks , examinar o resultado e decidir se deve ou não bloquear, ninguém garante que, entre essas operações, ninguém conseguirá levar o objeto que você precisa. Mas se você usar pg_try_advisory_lock , receberá imediatamente esse bloqueio ou a função simplesmente retornará FALSE.

Não captura sem exceções e expectativas


Existem bloqueios “normais” no modelo “Se você já pediu um bloqueio, aguarde. Se você não quiser esperar ( NOWAIT, statement_timeout, lock_timeout) - aqui é uma exceção " . Essa abordagem interfere bastante na transação, pois é necessário implementar um bloco BEGIN-EXCEPTION-ENDpara processar ou reverter ( ROLLBACK) a transação.

A única maneira de evitar esse comportamento é usar o design SELECT ... SKIP LOCKEDque acompanha a versão 9.5. Infelizmente, com esse método, as opções "não tinham o que bloquear" e "eram, mas já estão bloqueadas" tornam-se indistinguíveis.

Os bloqueios recomendados causados ​​pelas funções try simplesmente retornam TRUE/FALSE.
Não confunda pg_advisory_locke - a primeira função aguardará até receber um bloqueio, e a segunda simplesmente retornará imediatamente se for impossível capturá-lo "agora".pg_try_advisory_lockFALSE

Bloqueios dentro e após a transação


Como mencionei acima, os bloqueios de objetos são "anexados" ao processo e existem apenas como parte da transação atual. Mesmo apenas para impor - não terá sucesso:

LOCK TABLE tbl;
-- ERROR:  LOCK TABLE can only be used in transaction blocks

Assim, no final da transação, todos os bloqueios impostos a ela são removidos. Por outro lado, os bloqueios consultivos foram originalmente projetados com a capacidade de reter bloqueios e fora do escopo de uma transação :

SELECT pg_advisory_lock(1);
SELECT * FROM pg_locks WHERE pid = pg_backend_pid() AND locktype = 'advisory';

-[ RECORD 1 ]------+--------------
locktype           | advisory
database           | 263911484
relation           |
page               |
tuple              |
virtualxid         |
transactionid      |
classid            | 0 <--  int4 #1    int8
objid              | 1 <--  int4 #2    int8
objsubid           | 1
virtualtransaction | 416/475768
pid                | 29264
mode               | ExclusiveLock
granted            | t
fastpath           | f

Mas já a partir da versão 9.1, surgiram versões xact de funções consultivas que permitem implementar o comportamento de bloqueios "comuns" que são liberados automaticamente quando a transação que os impôs é concluída.

Exemplos de uso em VLSI


Na verdade, como qualquer outro bloqueio, os avisos servem para garantir a exclusividade do processamento de um recurso. Em nosso país, esses recursos geralmente são a tabela inteira ou um registro específico da tabela, que por algum motivo não deseja ser "bloqueado".

Monoprocessamento do trabalhador


Se a necessidade de processar alguns dados no banco de dados for acionada por um evento externo, mas o processamento de vários processos for redundante ou pode levar a uma condição de corrida , é razoável possibilitar que apenas um processo trabalhe nesses dados por vez .

Para fazer isso, tentaremos bloquear com o identificador da tabela como o primeiro parâmetro e o ID de processamento do aplicativo específico como o segundo:

SELECT pg_try_advisory_lock(
  'processed_table'::regclass::oid
, -1 --   worker'
);

Se retornarmos FALSE, outra pessoa já está com esse bloqueio e, especificamente, esse processo não precisa fazer nada, mas é melhor terminar silenciosamente. É assim que, por exemplo, funciona o processo de cálculo dinâmico do custo das mercadorias em um depósito , isoladamente para cada esquema de cliente individual.

Processamento de fila simultâneo


Agora a tarefa é "o oposto" - queremos que as tarefas em alguma tabela de filas sejam processadas o mais rápido possível, multithread, tolerante a falhas e até a partir de diferentes lógicas de negócios (bem, não temos o poder de uma) - por exemplo, como nosso operador faz sobre a transferência de relatórios eletrônicos para agências governamentais ou para o serviço OFD .

Como os BLs de "processamento" são servidores diferentes, o mutex não pode mais ser desligado. Não é seguro destacar um coordenador de processos de distribuição de tarefas especial, "die" he - e tudo vai subir. E, portanto, é mais eficiente distribuir tarefas diretamente no nível do banco de dados, e esse método existe - nos tempos antigos, o modelo era honestamente espionado por Dmitry Koterov e modificado de forma criativa.

Nesse caso, impomos um bloqueio na ID da tabela e na PK de um registro específico:

SELECT
  *
FROM
  queue_table
WHERE
  pg_try_advisory_lock('queue_table'::regclass::oid, pk_id)
ORDER BY
  pk_id
LIMIT 1;

Ou seja, o processo receberá da tabela o primeiro registro que ainda não foi bloqueado pelos irmãos concorrentes.

No entanto, se PK não consistir em (número inteiro) , mas em (número inteiro, número inteiro) (como no mesmo cálculo de custo, por exemplo), você pode impor um bloqueio diretamente nesse par - é improvável que ocorra uma interseção com o "concorrente".

Importante! Não se esqueça de manter periodicamente adequadamente sua tabela de filas !

Processamento exclusivo de documentos


É usado em qualquer lugar nas soluções de gerenciamento de documentos . De fato, em um sistema da Web distribuído, o mesmo documento pode ser aberto para visualização por diferentes usuários ao mesmo tempo, mas só pode ser processado (alterar seu estado etc.) a qualquer momento por apenas um.

Questões tradicionais


Onde sem eles! Quase tudo se resume a uma coisa : eles não desbloquearam o que trancaram .

Sobreposição múltipla de um bloqueio consultivo


RTFM , como se costuma dizer:
Se várias solicitações de bloqueio forem recebidas de uma só vez, elas serão acumuladas. Portanto, se um recurso foi bloqueado três vezes, ele deve ser desbloqueado três vezes para estar disponível em outras sessões.

Coloque muitos bloqueios de uma só vez




Milhares deles! Leia o manual novamente :
Os bloqueios consultivo e regular são armazenados na área de memória compartilhada, cujo tamanho é determinado pelos parâmetros de configuração max_locks_per_transactione max_connections. É importante que essa memória seja suficiente, pois, caso contrário, o servidor não poderá emitir nenhum bloqueio . Portanto, o número de bloqueios recomendados que um servidor pode emitir geralmente é limitado a dezenas ou centenas de milhares, dependendo da configuração do servidor.

Em geral, se surgir uma situação em que você deseja impor vários milhares de bloqueios de aviso (mesmo que você os remova corretamente mais tarde) - pense muito bem onde será executado quando o servidor for ativado.

Vazamentos ao filtrar registros


Aqui, pegamos a solicitação anterior e adicionamos uma condição inofensiva, como ID da verificação de paridade - AND pk_id % 2 = 0. Ambas as condições para cada entrada serão verificadas ! Como resultado, foi pg_try_advisory_lockconcluído, o bloqueio foi sobreposto e o registro foi filtrado por paridade.



Ou uma opção do manual:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- !

Isso é tudo - o bloqueio permanece , mas não estamos cientes disso. É tratado com os pedidos corretos, no pior dos casos - pg_advisory_unlock_all.

Oh, confuso!


Clássicos do gênero ...

Confunda e , e me pergunto por que funciona por um longo tempo. Mas porque a versão sem tentativa está aguardando . Confunda e , e me pergunto para onde foi a fechadura - e "terminou" com a transação. E a transação consistiu em uma solicitação, porque em nenhum lugar foi declarada "explicitamente", sim.pg_try_advisory_lockpg_advisory_lock

pg_try_advisory_lockpg_try_advisory_xact_lock

Trabalhe através do pgbouncer


Essa é uma fonte separada de sofrimento para muitos quando, por uma questão de desempenho, trabalhar com o banco de dados passa pelo pgbouncer no modo de transação .

Isso significa que duas de suas transações vizinhas em execução na mesma conexão com o banco de dados (que realmente passa pelo pgbouncer) podem ser executadas em diferentes conexões "físicas" no lado base. E eles têm seus próprios bloqueios ... Cada



um tem algumas opções:

  • ou trabalhar através de uma conexão direta com o banco de dados
  • ou invente um algoritmo para que todos os bloqueios consultivos estejam apenas dentro da transação (xact)

É tudo por agora.

All Articles