Bloqueios advisory do PostgreSQL para fluxos concorrentes seguros
Aprenda a usar bloqueios advisory do PostgreSQL para evitar processamento duplicado em aprovações, faturamento e agendadores com padrões práticos, trechos SQL e verificações simples.

O problema real: dois processos fazem o mesmo trabalho
Processamento duplo acontece quando o mesmo item é tratado duas vezes porque dois atores diferentes acham que são responsáveis. Em apps reais, aparece como um cliente sendo cobrado duas vezes, uma aprovação aplicada duas vezes ou um e‑mail de “fatura pronta” enviado duas vezes. Tudo pode parecer ok em testes e quebrar sob tráfego real.
Isso geralmente acontece quando o tempo fica apertado e mais de uma coisa pode agir:
Dois workers pegam o mesmo job ao mesmo tempo. Uma tentativa de retry é disparada porque uma chamada de rede estava lenta, mas a primeira tentativa ainda está em execução. Um usuário clica duas vezes em Aprovar porque a UI travou por um segundo. Dois agendadores se sobrepõem após um deploy ou por causa de drift do relógio. Mesmo um toque pode virar duas requisições se um app móvel reenviar após timeout.
A parte dolorosa é que cada ator está se comportando “razoavelmente” por si só. O bug é a lacuna entre eles: nenhum sabe que o outro já está processando o mesmo registro.
O objetivo é simples: para um dado item (um pedido, uma solicitação de aprovação, uma fatura), apenas um ator deve ser autorizado a fazer o trabalho crítico por vez. Os demais devem esperar brevemente ou recuar e tentar novamente.
Bloqueios advisory do PostgreSQL podem ajudar. Eles dão uma forma leve de dizer “estou trabalhando no item X” usando o banco de dados que você já confia para consistência.
Ajuste as expectativas: um bloqueio não é um sistema de fila completo. Não vai agendar jobs para você, garantir ordenação ou armazenar mensagens. É um portão de segurança ao redor da parte do fluxo que nunca deve rodar duas vezes.
O que são (e não são) bloqueios advisory do PostgreSQL
Bloqueios advisory do PostgreSQL são uma forma de garantir que apenas um worker execute uma parte do trabalho por vez. Você escolhe uma chave de bloqueio (como “fatura 123”), pede ao banco para bloqueá‑la, faz o trabalho e depois libera.
A palavra “advisory” é importante. O Postgres não entende o significado da sua chave e não protege nada automaticamente. Ele só registra um fato: essa chave está bloqueada ou não. Seu código precisa concordar sobre o formato da chave e adquirir o bloqueio antes de executar a parte arriscada.
Também vale comparar bloqueios advisory com bloqueios de linha. Bloqueios de linha (como SELECT ... FOR UPDATE) protegem linhas reais da tabela. São excelentes quando o trabalho mapeia bem para uma única linha. Bloqueios advisory protegem uma chave que você escolhe, útil quando o workflow toca muitas tabelas, chama serviços externos ou começa antes de a linha sequer existir.
Bloqueios advisory são úteis quando você precisa de:
- Ações uma‑por‑vez por entidade (uma aprovação por solicitação, uma cobrança por fatura)
- Coordenação entre vários servidores de app sem adicionar um serviço de bloqueio separado
- Proteção em torno de uma etapa do fluxo maior do que um único update de linha
Eles não substituem outras ferramentas de segurança. Não tornam operações idempotentes, não aplicam regras de negócio e não evitam duplicatas se um caminho de código esquecer de adquirir o bloqueio.
São chamados de “leve” porque você pode usá‑los sem mudar schema nem infraestrutura extra. Em muitos casos, dá para corrigir processamento duplo com uma única chamada de bloqueio ao redor de uma seção crítica, mantendo o resto do desenho igual.
Tipos de bloqueio que você realmente vai usar
Quando se fala “bloqueios advisory do PostgreSQL”, geralmente refere‑se a um pequeno conjunto de funções. Escolher a certa muda o comportamento em erros, timeouts e retries.
Sessão vs transação
Um bloqueio no nível de sessão (pg_advisory_lock) dura enquanto a conexão com o banco durar. Isso pode ser conveniente para workers de longa execução, mas também significa que um bloqueio pode ficar preso se seu app travar de uma forma que deixe uma conexão do pool pendurada.
Um bloqueio no nível de transação (pg_advisory_xact_lock) está ligado à transação atual. Ao fazer commit ou rollback, o PostgreSQL libera automaticamente. Para a maioria dos fluxos request‑response (aprovações, cliques de cobrança, ações de admin), esse é o padrão mais seguro porque é difícil esquecer de liberar.
Bloqueante vs try-lock
Chamadas bloqueantes esperam até o bloqueio ficar disponível. Simples, mas pode fazer uma requisição web parecer travada se outra sessão estiver segurando o bloqueio.
Chamadas try-lock retornam imediatamente:
pg_try_advisory_lock(nível de sessão)pg_try_advisory_xact_lock(nível de transação)
Try-lock costuma ser melhor para ações de UI. Se o bloqueio estiver ocupado, você pode devolver uma mensagem clara como “Já em processamento” e pedir ao usuário para tentar novamente.
Compartilhado vs exclusivo
Bloqueios exclusivos são “um por vez”. Bloqueios compartilhados permitem múltiplos detentores, mas bloqueiam um exclusivo. A maioria dos problemas de processamento duplo usa bloqueios exclusivos. Compartilhados são úteis quando muitos leitores podem prosseguir, mas um escritor raro precisa rodar sozinho.
Como os bloqueios são liberados
A liberação depende do tipo:
- Bloqueios de sessão: liberados ao desconectar, ou explicitamente com
pg_advisory_unlock - Bloqueios de transação: liberados automaticamente quando a transação termina
Escolhendo a chave de bloqueio certa
Um bloqueio advisory só funciona se todo worker tentar bloquear exatamente a mesma chave para a mesma parte do trabalho. Se um caminho de código bloquear “fatura 123” e outro bloquear “cliente 45”, você ainda pode ter duplicatas.
Comece nomeando a “coisa” que você quer proteger. Faça concreto: uma fatura, uma solicitação de aprovação, uma execução agendada ou o ciclo mensal de cobrança de um cliente. Essa escolha decide quanta concorrência você permite.
Escolha o escopo que corresponda ao risco
A maioria das equipes acaba com uma destas opções:
- Por registro: mais seguro para aprovações e faturas (bloquear por invoice_id ou request_id)
- Por cliente/conta: útil quando ações devem ser serializadas por cliente (faturamento, alterações de crédito)
- Por etapa do workflow: quando etapas diferentes podem rodar em paralelo, mas cada etapa precisa ser uma‑por‑vez
Trate o escopo como uma decisão de produto, não um detalhe de banco. “Por registro” evita clicks duplos cobrando duas vezes. “Por cliente” evita que dois jobs background gerem extratos sobrepostos.
Escolha uma estratégia de chave estável
Geralmente há duas opções: dois inteiros de 32 bits (usados como namespace + id) ou um inteiro de 64 bits (bigint), por vezes criado ao hash de uma string ID.
Chaves em dois inteiros são fáceis de padronizar: escolha um número de namespace fixo por workflow (por exemplo, aprovações vs faturamento) e use o ID do registro como segundo valor.
Hashing é útil quando seu identificador é um UUID, mas você precisa aceitar um pequeno risco de colisão e ser consistente em todos os lugares.
Seja qual for a escolha, documente o formato e centralize-o. “Quase a mesma chave” em dois lugares é uma forma comum de reintroduzir duplicatas.
Passo a passo: um padrão seguro para processamento um‑a‑um
Um bom workflow com advisory lock é simples: bloquear, verificar, agir, registrar, commitar. O bloqueio não é a regra de negócio por si só. É um guarda‑rail que torna a regra confiável quando dois workers atingem o mesmo registro ao mesmo tempo.
Um padrão prático:
- Abra uma transação quando o resultado precisa ser atômico.
- Adquira o bloqueio para a unidade de trabalho específica. Prefira um bloqueio de transação (
pg_advisory_xact_lock) para que ele seja liberado automaticamente. - Re‑verifique o estado no banco. Não presuma que você foi o primeiro. Confirme que o registro ainda é elegível.
- Faça o trabalho e escreva um marcador durável de “feito” no banco (atualização de status, entrada de razão, linha de auditoria).
- Faça commit e deixe o bloqueio ser liberado. Se você usou um bloqueio de sessão, faça unlock antes de devolver a conexão ao pool.
Exemplo: dois servidores recebem “Aprovar fatura #123” no mesmo segundo. Ambos começam, mas só um obtém o bloqueio para 123. O vencedor verifica que a fatura #123 ainda está pending, marca como approved, grava o registro de auditoria/pagamento e faz commit. O segundo servidor ou falha rápido (try-lock) ou espera, então re‑verifica e sai sem criar duplicação. De qualquer forma, você evita processamento duplo mantendo a UI responsiva.
Para depuração, registre o suficiente para rastrear cada tentativa: request id, approval id e chave de bloqueio computada, actor id, resultado (lock_busy, already_approved, approved_ok) e tempos.
Onde os advisory locks se encaixam: aprovações, faturamento, agendadores
Bloqueios advisory funcionam melhor quando a regra é direta: para uma coisa específica, apenas um processo pode fazer o trabalho “vencedor” por vez. Você mantém seu banco e código de app existentes, mas adiciona um pequeno portão que torna condições de corrida muito mais difíceis de acontecer.
Aprovações
Aprovações são armadilhas clássicas de concorrência. Dois revisores (ou a mesma pessoa clicando duas vezes) podem apertar Aprovar em milissegundos. Com um bloqueio chaveado pelo request id, apenas uma transação faz a mudança de estado. Os demais aprendem rapidamente o resultado e podem mostrar uma mensagem clara como “já aprovado” ou “já rejeitado”.
Isso é comum em portais de cliente e painéis admin onde muitas pessoas observam a mesma fila.
Faturamento
Faturamento geralmente precisa de uma regra mais estrita: uma tentativa de pagamento por fatura, mesmo quando ocorrem retries. Um timeout de rede pode fazer o usuário clicar em Pagar novamente, ou um retry background pode rodar enquanto a primeira tentativa ainda está em voo.
Um bloqueio chaveado pelo invoice id garante que apenas um caminho converse com o provedor de pagamento por vez. A segunda tentativa pode retornar “pagamento em andamento” ou ler o status mais recente. Isso evita trabalho duplicado e reduz o risco de cobranças em dobro.
Agendadores e workers em background
Em setups multi‑instância, agendadores podem rodar a mesma janela em paralelo por engano. Um bloqueio chaveado pelo nome do job mais a janela de tempo (por exemplo, daily-settlement:2026-01-29) garante que apenas uma instância rode.
A mesma abordagem funciona para workers que puxam itens de uma tabela: bloqueie pelo ID do item para que só um worker o processe.
Chaves comuns incluem um único approval request ID, um único invoice ID, um nome de job mais janela de tempo, um customer ID para “uma exportação por vez” ou uma chave de idempotência única para retries.
Um exemplo realista: impedir dupla aprovação em um portal
Imagine uma solicitação de aprovação em um portal: uma ordem de compra está aguardando e dois gerentes clicam Aprovar no mesmo segundo. Sem proteção, ambas as requisições podem ler “pending” e ambas escrever “approved”, criando entradas de auditoria duplicadas, notificações duplicadas ou trabalho downstream disparado duas vezes.
Bloqueios advisory do PostgreSQL dão um modo direto de tornar essa ação uma‑por‑vez por aprovação.
O fluxo
Quando a API recebe uma ação de approve, primeiro ela toma um bloqueio baseado no id da aprovação (assim diferentes aprovações ainda podem ser processadas em paralelo).
Um padrão comum é: bloquear por approval_id, ler o status atual, atualizar o status e inserir um registro de auditoria, tudo em uma transação.
BEGIN;
-- One-at-a-time per approval_id
SELECT pg_try_advisory_xact_lock($1) AS got_lock; -- $1 = approval_id
-- If got_lock = false, return "someone else is approving, try again".
SELECT status FROM approvals WHERE id = $1 FOR UPDATE;
-- If status != 'pending', return "already processed".
UPDATE approvals
SET status = 'approved', approved_by = $2, approved_at = now()
WHERE id = $1;
INSERT INTO approval_audit(approval_id, actor_id, action, created_at)
VALUES ($1, $2, 'approved', now());
COMMIT;
O que o segundo clique experiencia
A segunda requisição ou não consegue o bloqueio (então retorna rápido “Já em processamento”) ou obtém o bloqueio depois que o primeiro termina, vê que o status já está aprovado e sai sem alterar nada. Em qualquer caso, você evita processamento duplo mantendo a UI responsiva.
Para depuração, registre request id, approval id e chave de bloqueio computada, actor id, resultado (lock_busy, already_approved, approved_ok) e tempos.
Lidando com espera, timeouts e retries sem travar o app
Esperar por um bloqueio parece inofensivo até transformar‑se em botão girando, worker preso ou backlog que nunca limpa. Quando você não consegue o bloqueio, falhe rápido onde um humano espera e espere apenas onde aguardar é seguro.
Para ações de usuário: try-lock e responda claramente
Se alguém clica Aprovar ou Cobrar, não bloqueie a requisição por segundos. Use try-lock para que a app responda na hora.
Uma abordagem prática: tente o lock e, se falhar, retorne uma resposta clara “ocupado, tente novamente” (ou atualize o estado do item). Isso reduz timeouts e desencoraja cliques repetidos.
Mantenha a seção bloqueada curta: valide estado, aplique a mudança, faça commit.
Para jobs em background: bloquear é aceitável, mas limite
Para schedulers e workers, bloquear pode ser ok porque nenhum humano está aguardando. Mas ainda é preciso limites, caso contrário um job lento pode travar toda a frota.
Use timeouts para que um worker desista e siga em frente:
SET lock_timeout = '2s';
SET statement_timeout = '30s';
SELECT pg_advisory_lock(123456);
Também defina um tempo máximo esperado para o job. Se faturamento normalmente termina em menos de 10 segundos, trate 2 minutos como incidente. Monitore tempo de início, id do job e quanto tempo os bloqueios ficam segurados. Se seu runner suporta cancelamento, cancele tarefas que ultrapassem o limite para que a sessão termine e o bloqueio seja liberado.
Planeje retries de propósito. Quando não se adquire o bloqueio, decida o próximo passo: reagende com backoff (e um pouco de aleatoriedade), pule trabalho best‑effort para esse ciclo, ou marque o item como contendido se falhas repetidas precisarem de atenção.
Erros comuns que causam bloqueios presos ou duplicatas
O surpreso mais comum é bloqueios de sessão que nunca são liberados. Pools de conexão mantêm conexões abertas, então uma sessão pode sobreviver além de uma requisição. Se você adquirir um lock de sessão e esquecer de desbloquear, ele pode ficar segurado até a conexão ser reciclada. Outros workers vão esperar (ou falhar) e pode ser difícil descobrir por quê.
Outra fonte de duplicatas é bloquear mas não re‑verificar o estado. Um bloqueio apenas garante que um worker execute a seção crítica por vez. Não garante que o registro ainda é elegível. Sempre re-cheque dentro da mesma transação (por exemplo, confirme pending antes de mover para approved).
Chaves de bloqueio também pegam times desprevenidos. Se um serviço bloquear por order_id e outro bloquear por uma chave diferente computada para o mesmo recurso do mundo real, você terá dois bloqueios. Ambos os caminhos podem rodar ao mesmo tempo, criando uma falsa sensação de segurança.
Manter bloqueios por muito tempo geralmente é autoinfligido. Se você faz chamadas lentas de rede enquanto segura o bloqueio (provedor de pagamento, email/SMS, webhooks), uma pequena barreira vira gargalo. Mantenha a seção bloqueada focada em trabalho rápido no banco: valide estado, grave o novo estado, registre o que deve acontecer a seguir. Então dispare efeitos colaterais após o commit.
Por fim, bloqueios advisory não substituem idempotência ou constraints do banco. Trate‑os como um semáforo, não uma prova. Use restrições únicas onde couber e chaves de idempotência para chamadas externas.
Checklist rápido antes do deploy
Trate bloqueios advisory como um pequeno contrato: todo mundo no time deve saber o que o bloqueio protege e o que é permitido enquanto ele está segurado.
Uma checklist curta que pega a maioria dos problemas:
- Uma chave de bloqueio clara por recurso, documentada e reutilizada em todos os lugares
- Adquira o bloqueio antes de qualquer coisa irreversível (pagamentos, emails, chamadas externas)
- Re‑verifique o estado depois do bloqueio e antes de gravar mudanças
- Mantenha a seção bloqueada curta e mensurável (log de espera pelo bloqueio e tempo de execução)
- Decida o que
lock busysignifica para cada caminho (mensagem na UI, retry com backoff, pular)
Próximos passos: aplique o padrão e mantenha‑o sustentável
Escolha um lugar onde duplicatas mais doem e comece por ali. Bons alvos iniciais são ações que custam dinheiro ou mudam estado permanentemente, como “cobrar fatura” ou “aprovar solicitação”. Envolva apenas essa seção crítica com um bloqueio advisory e depois expanda para passos próximos quando confiar no comportamento.
Adicione observabilidade básica cedo. Registre quando um worker não conseguir o bloqueio e quanto tempo trabalhos bloqueados demoram. Se esperas por bloqueios dispararem, geralmente significa que a seção crítica está grande demais ou uma query lenta está escondida dentro dela.
Bloqueios funcionam melhor sobre segurança de dados, não em vez dela. Mantenha campos de status claros (pending, processing, done, failed) e proteja‑os com constraints quando possível. Se um retry ocorrer no pior momento, uma constraint única ou uma chave de idempotência pode ser a segunda linha de defesa.
Se você está construindo fluxos no AppMaster (appmaster.io), pode aplicar o mesmo padrão mantendo a mudança de estado crítica dentro de uma transação e adicionando um pequeno passo SQL para tomar um advisory lock em nível de transação antes da etapa de “finalizar”.
Bloqueios advisory são uma boa solução até você realmente precisar de features de fila (prioridades, jobs com delay, dead‑letter), ter contenção pesada e precisar de paralelismo mais inteligente, precisar coordenar entre bancos sem um Postgres compartilhado ou exigir regras de isolamento mais rigorosas. O objetivo é confiabilidade chata: mantenha o padrão pequeno, consistente, visível nos logs e respaldado por constraints.
FAQ
Use um bloqueio advisory quando precisar que “apenas um ator por vez” execute uma unidade específica de trabalho, como aprovar uma solicitação, cobrar uma fatura ou executar uma janela agendada. É especialmente útil quando múltiplas instâncias do app podem tocar o mesmo item e você não quer adicionar um serviço de bloqueio separado.
Bloqueios de linha protegem linhas reais que você seleciona e são ótimos quando toda a operação mapeia claramente para um único UPDATE de linha. Bloqueios advisory protegem uma chave que você escolhe, então funcionam mesmo quando o fluxo toca várias tabelas, chama serviços externos ou começa antes da linha final existir.
Padrão para pg_advisory_xact_lock (nível de transação) em ações request/response porque ele é liberado automaticamente quando você faz commit ou rollback. Use pg_advisory_lock (nível de sessão) somente quando realmente precisar que o bloqueio viva além da transação e você tiver certeza de que sempre fará pg_advisory_unlock antes de devolver a conexão ao pool.
Para ações orientadas ao usuário, prefira try-lock (pg_try_advisory_xact_lock) para que a requisição falhe rápido e retorne uma resposta clara “já em processamento”. Para workers de background, um bloqueio bloqueante pode ser aceitável, mas limite com lock_timeout para que uma tarefa travada não paralise tudo.
Bloqueie a menor unidade que não pode rodar duas vezes, geralmente “uma fatura” ou “uma solicitação de aprovação”. Se você bloquear muito amplamente (por exemplo, por cliente) pode reduzir a taxa de transferência; se bloquear muito estreitamente pode continuar a ter duplicatas.
Escolha um formato de chave estável e use-o em todos os lugares que possam executar a mesma ação crítica. Uma abordagem comum é dois inteiros: um namespace fixo para o fluxo e o ID da entidade, assim diferentes fluxos não se bloqueiam acidentalmente enquanto ainda coordenam corretamente.
Não. Um bloqueio apenas evita execução concorrente; não prova que a operação é segura de repetir. Você ainda precisa re-checar o estado dentro da transação (por exemplo, verificar se o item continua pending) e confiar em constraints únicas ou chaves de idempotência onde fizer sentido.
Mantenha a seção bloqueada curta e focada no banco: adquira o bloqueio, re-verifique elegibilidade, grave o novo estado e faça commit. Execute efeitos lentos (pagamentos, emails, webhooks) depois do commit ou via um registro estilo outbox para não segurar o bloqueio durante atrasos de rede.
A causa mais comum é um bloqueio em nível de sessão mantido por uma conexão de pool que nunca foi desbloqueada devido a um bug no caminho de código. Prefira bloqueios em nível de transação e, se usar sessão, garanta que pg_advisory_unlock seja executado antes de devolver a conexão ao pool.
Registre o ID da entidade e a chave de bloqueio computada, se o bloqueio foi adquirido, quanto tempo levou para adquirir e quanto tempo a transação rodou. Também registre o resultado como lock_busy, already_processed ou processed_ok para distinguir contenção de duplicatas reais.


