Eu tenho um banco de dados Postgres que contém detalhes sobre clusters de servidores, como status do servidor ('ativo', 'em espera' etc). Servidores ativos a qualquer momento podem precisar fazer failover para um modo de espera, e não me importo com qual modo de espera é usado em particular.
Quero que uma consulta de banco de dados altere o status de um standby - APENAS UM - e retorne o IP do servidor que deve ser utilizado. A escolha pode ser arbitrária: como o status do servidor muda com a consulta, não importa qual standby está selecionado.
É possível limitar minha consulta a apenas uma atualização?
Aqui está o que eu tenho até agora:
UPDATE server_info SET status = 'active'
WHERE status = 'standby' [[LIMIT 1???]]
RETURNING server_ip;
Postgres não gosta disso. O que eu poderia fazer diferente?
Sem acesso de gravação simultâneo
Materialize uma seleção em um CTE (Common Table Expressions) e una-o na
FROM
cláusula doUPDATE
.Eu originalmente tinha uma subconsulta simples aqui, mas isso pode evitar
LIMIT
certos planos de consulta, como Feike apontou:Ou use uma subconsulta pouco correlacionada para o caso simples com
LIMIT
1
. Mais simples, mais rápido:Com acesso de gravação simultâneo
Assumindo o nível de isolamento padrão
READ COMMITTED
para tudo isso. Níveis de isolamento mais rigorosos (REPEATABLE READ
eSERIALIZABLE
) ainda podem resultar em erros de serialização. Ver:Sob carga de gravação simultânea, adicione
FOR UPDATE SKIP LOCKED
para bloquear a linha para evitar condições de corrida.SKIP LOCKED
foi adicionado no Postgres 9.5 , para versões mais antigas veja abaixo. O manual:Se não houver nenhuma linha qualificada e desbloqueada, nada acontece nesta consulta (nenhuma linha é atualizada) e você obtém um resultado vazio. Para operações não críticas, isso significa que você terminou.
No entanto, transações simultâneas podem ter linhas bloqueadas, mas não concluem a atualização (
ROLLBACK
ou outros motivos). Para ter certeza , execute uma verificação final:SELECT
também vê linhas bloqueadas. Wile que não retornatrue
, uma ou mais linhas ainda estão inacabadas e as transações ainda podem ser revertidas. (Ou novas linhas foram adicionadas enquanto isso.) Espere um pouco, então faça um loop nas duas etapas: (UPDATE
até que você não receba nenhuma linha de volta;SELECT
...) até que você obtenhatrue
.Relacionado:
Sem
SKIP LOCKED
no PostgreSQL 9.4 ou anteriorAs transações simultâneas que tentam bloquear a mesma linha são bloqueadas até que a primeira libere seu bloqueio.
Se a primeira foi revertida, a próxima transação pega o bloqueio e prossegue normalmente; outros na fila continuam esperando.
Se o primeiro foi confirmado, a
WHERE
condição é reavaliada e, se não forTRUE
mais (status
mudou), o CTE (surpreendentemente) não retornará nenhuma linha. Nada acontece. Esse é o comportamento desejado quando todas as transações desejam atualizar a mesma linha .Mas não quando cada transação deseja atualizar a próxima linha . E como queremos apenas atualizar uma linha arbitrária (ou aleatória ) , não faz sentido esperar.
Podemos desbloquear a situação com a ajuda de bloqueios consultivos :
Desta forma, a próxima linha ainda não bloqueada será atualizada. Cada transação recebe uma nova linha para trabalhar. Eu tive ajuda do Wiki Postgres tcheco para esse truque.
id
sendo qualquerbigint
coluna exclusiva (ou qualquer tipo com uma conversão implícita comoint4
ouint2
).Se bloqueios consultivos estiverem em uso para várias tabelas em seu banco de dados simultaneamente, desambiguar com
pg_try_advisory_xact_lock(tableoid::int, id)
-id
sendo exclusivointeger
aqui.Como
tableoid
é umabigint
quantidade, teoricamente pode transbordarinteger
. Se você é paranóico o suficiente, use(tableoid::bigint % 2147483648)::int
- deixando uma "colisão de hash" teórica para o verdadeiramente paranóico ...Além disso, o Postgres é livre para testar as
WHERE
condições em qualquer ordem. Ele pode testarpg_try_advisory_xact_lock()
e adquirir um bloqueio antesstatus = 'standby'
de , o que pode resultar em bloqueios consultivos adicionais em linhas não relacionadas, ondestatus = 'standby'
não é true. Pergunta relacionada no SO:Normalmente, você pode simplesmente ignorar isso. Para garantir que apenas as linhas qualificadas sejam bloqueadas, você pode aninhar o(s) predicado(s) em um CTE como acima ou uma subconsulta com o
OFFSET 0
hack (impede o inlining) . Exemplo:Ou (mais barato para varreduras sequenciais) aninhe as condições em uma
CASE
declaração como:No entanto , o
CASE
truque também impediria o Postgres de usar um índice nostatus
. Se esse índice estiver disponível, você não precisará de aninhamento extra para começar: apenas as linhas qualificadas serão bloqueadas em uma verificação de índice.Como você não pode ter certeza de que um índice é usado em todas as chamadas, você pode apenas:
O
CASE
é logicamente redundante, mas atende ao propósito discutido.Se o comando fizer parte de uma transação longa, considere os bloqueios em nível de sessão que podem ser (e devem ser) liberados manualmente. Assim, você pode desbloquear assim que terminar com a linha bloqueada:
pg_try_advisory_lock()
epg_advisory_unlock()
. O manual:Relacionado: