Use FOR UPDATE SKIP LOCKED em uma consulta que também altera a linha selecionada de ready para processing. Isso permite que workers concorrentes reivindiquem diferentes linhas sintéticas de artigos, enquanto o estado salvo impede outra reivindicação após o fim do bloqueio.
Este guia é para um desenvolvedor que usa o Terminal em um Mac com uma conexão PostgreSQL funcional. Você criará três artigos sintéticos, verificará reivindicações concorrentes em duas sessões do banco de dados e executará um exemplo completo com node-postgres.
Por que dois workers podem selecionar o mesmo artigo?
Um SELECT simples não reserva seu resultado. Com o nível de isolamento padrão Read Committed do PostgreSQL, dois workers podem ler a mesma linha ready confirmada antes que qualquer um deles a altere. Colocar essa leitura simples e uma atualização posterior em uma transação não torna a seleção exclusiva por si só. O PostgreSQL documenta o comportamento dos snapshots em Transaction Isolation.
A reivindicação precisa de duas proteções: um bloqueio de linha durante a escolha do artigo e uma alteração de estado antes de liberar esse bloqueio. O PostgreSQL libera os bloqueios de linha quando a transação termina. Portanto, uma leitura de bloqueio isolada, seguida do processamento, deixaria a linha elegível novamente assim que a transação dessa leitura fosse concluída. Consulte Explicit Locking.
Use o mesmo protocolo de reivindicação em todos os workers. Um caminho da aplicação que lê um artigo ready e inicia o processamento sem reivindicá-lo com sucesso ainda pode duplicar o trabalho.
Criar uma pequena fila sintética de artigos
Os pré-requisitos são um banco de dados PostgreSQL em execução, o comando psql disponível no Terminal e uma configuração de conexão existente. Para a seção JavaScript, tenha também uma versão atualmente suportada do Node.js e o npm instalados. Este artigo parte de uma conexão funcional com o banco de dados; a instalação varia conforme a configuração existente do seu Mac.
O recurso SKIP LOCKED necessário foi introduzido no PostgreSQL 9.5. Essa é a versão mínima para a sintaxe, não uma recomendação para executar uma versão sem suporte. Use uma versão suportada do PostgreSQL; a documentação atual consultada identifica o PostgreSQL 18 como a versão atual. Consulte as notas da versão 9.5 e a documentação atual de SELECT.
Abra o Terminal e execute psql usando suas configurações habituais de conexão. Isso abre um prompt interativo para executar SQL e visualizar resultados, conforme descrito na documentação do psql. Execute a configuração a seguir uma vez em um banco de dados temporário no qual article_claim_demo ainda não exista. Ela não substitui deliberadamente uma tabela existente.
SHOW server_version;
SHOW transaction_isolation;
CREATE TABLE article_claim_demo (
id integer PRIMARY KEY,
title text NOT NULL,
state text NOT NULL DEFAULT 'ready'
CHECK (state IN ('ready', 'processing', 'done')),
rendered_title text
);
INSERT INTO article_claim_demo (id, title) VALUES
(1, 'Paper shapes'),
(2, 'Desk arrangements'),
(3, 'Color combinations');
Os exemplos pressupõem o nível de isolamento Read Committed e esta tabela simples, sem gatilhos adicionais. Mantenha as três linhas de exemplo inalteradas até concluir o teste com duas sessões.
Reivindicar um artigo com uma única instrução SQL
Execute esta instrução sempre que um worker precisar de um artigo. A consulta de candidatos bloqueia uma linha disponível; a atualização registra a reivindicação e retorna os campos de entrada. A documentação de UPDATE do PostgreSQL descreve esse padrão de CTE e atualização e confirma que RETURNING informa as linhas realmente atualizadas.
WITH candidate AS (
SELECT id
FROM article_claim_demo
WHERE state = 'ready'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE article_claim_demo AS a
SET state = 'processing'
FROM candidate AS c
WHERE a.id = c.id
AND a.state = 'ready'
RETURNING a.id, a.title;
O bloqueio deve ficar dentro da CTE de candidatos. SKIP LOCKED permite que outro worker ignore um candidato que não consiga bloquear imediatamente. LIMIT 1 limita cada reivindicação, e a junção pela chave primária identifica um único alvo de atualização. O PostgreSQL documenta explicitamente o bloqueio dentro de uma consulta WITH e o uso de linhas ignoradas em filas em SELECT: The Locking Clause.
Sem um BEGIN explícito, o PostgreSQL executa a instrução em sua própria transação. Uma instrução bem-sucedida confirma tanto a atualização associada à seleção quanto a alteração de estado antes de o processamento começar. Após a confirmação, o bloqueio da linha desaparece, mas state = 'ready' exclui esse artigo. Consulte Transactions.
Processe apenas um artigo retornado pela instrução de reivindicação. Um resultado vazio significa que esta tentativa não obteve um artigo; isso não indica que todo o trabalho foi concluído. Outros workers podem estar mantendo linhas elegíveis bloqueadas no momento. Além disso, SKIP LOCKED evita esperas por bloqueios de linha, mas ainda pode haver espera por bloqueios no nível da tabela. Esses limites estão documentados em SELECT.
Verificar o comportamento da reivindicação em duas sessões do Terminal
Use duas janelas do Terminal conectadas por meio de psql ao mesmo banco de dados. Comece com as três linhas ready originais. A sequência controlada a seguir mantém cada transação aberta para que você possa observar diretamente a sobreposição.
- Na sessão A, execute
BEGIN ISOLATION LEVEL READ COMMITTED;e, em seguida, cole a instrução completa de reivindicação acima. Deixe a transação aberta. Com os dados de exemplo intactos, o ID retornado deve ser1. - Na sessão B, execute
BEGIN ISOLATION LEVEL READ COMMITTED;e, em seguida, cole a mesma instrução de reivindicação. Ela deve retornar o ID2enquanto A ainda mantém o ID1bloqueado. - Execute
ROLLBACK;na sessão B e, em seguida,ROLLBACK;na sessão A. - Em qualquer uma das sessões, execute a consulta de inspeção abaixo. Como ambas as reivindicações foram revertidas, os três artigos devem estar novamente
ready.
SELECT id, title, state, rendered_title
FROM article_claim_demo
ORDER BY id;
Estes são resultados esperados, deduzidos do comportamento documentado de bloqueio e reversão, e não de uma execução de teste relatada no banco de dados. A reversão cancela cada atualização de estado e libera seus bloqueios. Se você usar COMMIT em vez disso, esses artigos permanecerão processing e não serão selecionados por reivindicações posteriores. Consulte Explicit Locking e Transactions.
Este teste destaca duas observações úteis: a sessão B avança além de um artigo que está bloqueado no momento, e uma alteração de estado confirmada mantém um artigo fora de reivindicações futuras. Teste ambas as propriedades ao adaptar um worker existente.
Executar três workers com node-postgres
Depois de reverter as duas transações de teste, crie um novo diretório de projeto local no Terminal. Instale o pacote com o comando de instalação documentado do node-postgres:
mkdir article-claim-demo
cd article-claim-demo
npm init -y
npm install pg
Salve o programa completo a seguir como workers.mjs. Ele usa o mesmo ambiente de conexão com o banco de dados que psql; o node-postgres documenta suas variáveis de conexão compartilhadas em Connecting. Cada worker tenta fazer uma reivindicação, converte o título sintético para maiúsculas e salva o resultado.
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ max: 3 });
const claimSQL = `
WITH candidate AS (
SELECT id
FROM article_claim_demo
WHERE state = 'ready'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE article_claim_demo AS a
SET state = 'processing'
FROM candidate AS c
WHERE a.id = c.id AND a.state = 'ready'
RETURNING a.id, a.title;
`;
async function worker(label) {
const claimed = await pool.query(claimSQL);
if (claimed.rows.length === 0) {
console.log(`${label}: no row available now`);
return;
}
const article = claimed.rows[0];
const renderedTitle = article.title.toUpperCase();
const completed = await pool.query(
`UPDATE article_claim_demo
SET rendered_title = $2, state = 'done'
WHERE id = $1 AND state = 'processing'
RETURNING id`,
[article.id, renderedTitle]
);
if (completed.rowCount !== 1) {
throw new Error(`${label}: completion did not update one row`);
}
console.log(`${label}: completed article ${article.id}`);
}
try {
const results = await Promise.allSettled([
worker('A'),
worker('B'),
worker('C')
]);
for (const result of results) {
if (result.status === 'rejected') {
console.error(result.reason);
process.exitCode = 1;
}
}
} finally {
await pool.end();
}
Execute node workers.mjs e, em seguida, execute novamente a consulta de inspeção em psql. Com três artigos prontos intactos e sem outra atividade, espere que os três estados mudem para done, com valores em maiúsculas em rendered_title. Os rótulos dos workers e a ordem das mensagens no console não precisam corresponder à ordem dos artigos.
Usar pool.query é apropriado aqui porque cada reivindicação é uma instrução completa e cada conclusão é uma instrução separada. O pool gerencia a obtenção e a devolução de conexões; pool.end() fecha o pool após o término das tentativas. Consulte Pooling.
A consulta de conclusão passa seus valores separadamente com $1 e $2, seguindo Queries. O programa verifica se o array rows retornado está vazio e verifica rowCount para saber o número de linhas de conclusão atualizadas, em conformidade com pg.Result.
O JavaScript passou por uma verificação de sintaxe durante a preparação. Não havia executáveis do PostgreSQL disponíveis no ambiente de pesquisa, então o SQL e o comportamento do banco de dados sob concorrência foram analisados com base na documentação, mas não foram executados aqui.
Escolher o limite correto da transação e lidar com trabalho inacabado
O exemplo confirma uma reivindicação curta antes de transformar o título. Isso mantém curto o tempo de duração do bloqueio, mas cria um limite de recuperação deliberado: se um worker parar depois de reivindicar e antes de concluir, o artigo permanece processing. Executar o programa novamente não o reivindicará.
Para esta demonstração em computador, pare todos os workers e inspecione os artigos processing antes de torná-los elegíveis novamente de forma deliberada. Não os redefina automaticamente enquanto um worker anterior ainda puder concluir: o exemplo não tem um campo de geração de reivindicação que permita distinguir uma tentativa anterior de uma posterior. Impedir reivindicações duplicadas, por si só, não oferece recuperação automática nem processamento exatamente uma vez.
Use este guia de decisão ao adaptar o exemplo:
| Sua tarefa | Abordagem da transação | Consequência |
|---|---|---|
| Reivindicar um artigo e, em seguida, realizar um trabalho mais demorado na aplicação | Confirmar primeiro a reivindicação de uma única instrução | Manter um estado processing visível e projetar a recuperação separadamente |
| Realizar uma pequena transformação inteiramente no PostgreSQL | Reivindicar e concluir dentro de uma única transação | As alterações no banco de dados podem ser confirmadas ou revertidas em conjunto |
| Executar várias instruções SQL como uma única transação no Node.js | Obter um cliente e usá-lo durante toda a operação | Manter a mesma conexão em BEGIN, nas instruções e em COMMIT ou ROLLBACK |
Para a última abordagem, o node-postgres exige que todas as instruções da transação usem o mesmo cliente obtido do pool. Chamadas separadas a pool.query podem usar conexões diferentes e, por isso, não podem compor essa transação. Libere o cliente após concluir ou reverter a transação. Consulte Transactions e Pooling.
Deixe explícitas as premissas de isolamento demonstradas. Os níveis Repeatable Read e Serializable podem gerar erros quando uma linha a ser bloqueada foi alterada desde o início da transação; aplicações que usam esses níveis precisam lidar com novas tentativas de transação. O PostgreSQL descreve esse limite em Explicit Locking.
Para esta tarefa sintética com artigos, a sequência essencial é concreta: selecionar e bloquear uma linha elegível, alterar seu estado na mesma instrução e processar somente o artigo retornado após o sucesso da reivindicação. Verifique a sobreposição em duas sessões e, em seguida, decida como sua aplicação lidará com um artigo cujo worker nunca registra a conclusão.
Fontes
- SELECT — PostgreSQL Global Development Group.
- UPDATE — PostgreSQL Global Development Group.
- 13.3. Explicit Locking — PostgreSQL Global Development Group.
- 13.2. Transaction Isolation — PostgreSQL Global Development Group.
- 3.4. Transactions — PostgreSQL Global Development Group.
- Release 9.5 — PostgreSQL Global Development Group.
- psql — PostgreSQL Global Development Group.
- node-postgres: Introduction and Install — node-postgres.
- Connecting — node-postgres.
- Pooling — node-postgres.
- Transactions — node-postgres.
- Queries — node-postgres.
- pg.Result — node-postgres.