Estou usando um banco de dados Postgres para implementar um agendamento de trabalhos para um grande número de computadores/processos. Para encurtar a história, cada trabalho tem seu id, todo o agendamento é implementado com três guias: todos os trabalhos, trabalhos em execução no momento e trabalhos já concluídos.
A principal funcionalidade do agendamento é (1) solicitar um trabalho e (2) informar o banco de dados sobre um trabalho concluído. A solicitação de um trabalho leva qualquer id da lista de trabalhos, que não esteja na tabela em execução e nem na tabela concluída:
insert into piper.jobs_running
select x.fid from (
SELECT fid FROM piper.jobs
except
select fid from piper.jobs_running
except
select fid from piper.jobs_completed
) as x limit 1
returning(fid)
A conclusão do trabalho o remove da lista em execução e o insere na lista concluída. Como não é específico da simultaneidade, omito os comandos SQL (leva de dezenas de minutos a algumas horas para concluir um trabalho).
Foi uma surpresa desagradável para mim que dois processos executando exatamente a mesma consulta acima (praticamente solicitando o trabalho ao mesmo tempo) possam obter o mesmo id de trabalho (fid). A única explicação possível que estou apresentando é que o Postgres não é compatível com o ACID. Comentários?
Informações adicionais: Eu configurei as transações para serem serializáveis (em postgresql.conf set default_transaction_isolation = 'serializable'
). Agora o SGBD falha nas transações caso o isolamento não seja cumprido. é possível forçar o Postgres a reiniciá-los automaticamente?
Problemas específicos com a consulta
O padrão do PostgreSQL é o
READ COMMITTED
isolamento e parece que você não está usando nada diferente. EmREAD COMMITTED
cada instrução obtém seu próprio instantâneo. Ele não pode ver alterações não confirmadas de outras transações; é como se simplesmente não tivessem acontecido.Agora, imagine que você execute isso simultaneamente em três sessões, em uma configuração com três entradas em
jobs
, zero emjobs_running
e zero emjobs_completed
:Cada execução selecionará os três trabalhos em
jobs
. Como seus instantâneos foram todos tirados antes de qualquer um deles fazer uma alteração ou até mesmo criar a linha não confirmada, *todos eles encontrarão zero linhas emjobs_running
ejobs_completed
.Então todos eles reivindicam um emprego. Provavelmente o mesmo trabalho , porque mesmo que não haja
ORDER BY
, a ordem de digitalização será a mesma.Bloqueio
O bloqueio de linha cruza as fronteiras transnacionais e permite que você se comunique entre as transações para impor a ordem. Então você pode pensar que isso resolveria seu problema, mas não vai.
Se você
FOR UPDATE
bloquear arow
entrada, de modo que a linha seja bloqueada exclusivamente, o bloqueio será retido até que a transação seja confirmada ou revertida. Portanto, você pensaria que a próxima transação obteria uma linha diferente ou, se tentasse obter a mesma linha, aguardaria a liberação do bloqueio, verificaria se agora havia uma entrada emjobs_running
e pularia a linha. Você estaria errado.O que acontecerá é que você bloqueará todas as linhas. Uma transação obterá com sucesso os bloqueios em todas as linhas. As outras transações, que farão o mesmo índice ou varredura sequencial, geralmente tentarão bloquear as linhas na mesma ordem, travarão na primeira tentativa de bloqueio e aguardarão a reversão ou confirmação da primeira transação. Se você não tiver sorte, eles podem começar a bloquear diferentes conjuntos de linhas e travar uns contra os outros, causando a interrupção do impasse, mas geralmente você não está obtendo nenhuma simultaneidade útil.
Pior ainda, a primeira transação escolhe uma das linhas que bloqueou, insere uma linha
jobs_running
e confirma, liberando os bloqueios. Outra transação é capaz de continuar e bloqueia todas as linhas .... mas não obtém um novo instantâneo do estado do banco de dados (o instantâneo é obtido no início da instrução), então * não pode ver que você inseriu uma linha emjobs_running
. Assim, ele pega o mesmo trabalho, insere uma linha para esse trabalho emjobs_running
e confirma.Novas verificações de condição
O PostgreSQL tem um recurso peculiar não incluído na maioria dos bancos de dados onde, se uma transação for bloqueada em um bloqueio, ele verifica novamente se a linha selecionada ainda corresponde à condição de bloqueio quando obtém o bloqueio após a confirmação da primeira transação.
É por isso que o exemplo em https://stackoverflow.com/questions/11532550/atomic-update-select-in-postgres funciona - ele se baseia em novas verificações de qualificador em
WHERE
cláusulas após recuperar um bloqueio.O uso de locking força tudo a rodar serialmente, então na prática você pode muito bem ter uma única conexão fazendo o trabalho.
Isolamento, ACID e realidade
O isolamento de transações no PostgreSQL não é o ideal perfeito onde as transações são executadas simultaneamente, mas seus efeitos são os mesmos como se fossem executados em série.
O único banco de dados do mundo real que forneceria isolamento perfeito seria aquele que bloqueia exclusivamente todas as tabelas quando uma transação de gravação a acessa pela primeira vez; portanto, na prática, as transações só podem ser de acesso simultâneo se forem para diferentes partes do banco de dados. Ninguém deseja usar esse banco de dados em situações em que a simultaneidade é desejável ou útil.
Todas as implementações do mundo real são concessões.
READ COMMITTED
predefiniçãoO padrão do PostgreSQL é o
READ COMMITTED
isolamento, um nível de isolamento bem definido que permite leituras não repetíveis e fantasmas , conforme discutido no manual do PostgreSQL sobre isolamento de transação .SERIALIZABLE
isolamentoVocê pode solicitar um
SERIALIZABLE
isolamento mais rígido por transação ou como padrão global por usuário, por banco de dados ou (não recomendado). Isso fornece garantias muito mais fortes, embora ainda não perfeitas, ao custo de forçar a reversão das transações se elas interagirem de maneiras que não poderiam acontecer se tivessem sido executadas em série.Como suas consultas simultâneas sempre tentarão obter o primeiro trabalho, não importa quantos trabalhos existam, todos, exceto um, serão interrompidos com falhas de serialização. Portanto, na prática, você não obtém nenhuma simultaneidade útil e pode muito bem ter uma única conexão distribuindo tarefas aos trabalhadores.
(Observe que antes do PostgreSQL 9.1
SERIALIZABLE
oferecia garantias muito mais fracas e não detectava muitos casos em que as transações eram interdependentes.)Reexecução automática
SERIALIZABLE
O PostgreSQL não executa automaticamente transações
SERIALIZABLE
que abortam devido a falhas de serialização. Há casos em que isso seria bastante útil, mas outros casos em que isso seria completamente errado e perigoso - especialmente quando estão envolvidos ciclos de leitura/modificação/gravação por meio do aplicativo. No momento, não há suporte para a reexecução automática de transações em falhas de serialização. Espera-se que o aplicativo tente novamente.Não escreva um sistema de filas DIY
Parece que o que você está tentando fazer é escrever um sistema de filas. Considere não fazer isso. Escrever um sistema de filas robusto, confiável e correto é difícil, e existem alguns bons já disponíveis que você pode adotar. Você tem que lidar com coisas como segurança contra falhas, o que acontece quando alguém pega uma tarefa e não consegue completá-la, condições de corrida em torno da conclusão assim que você desiste de o manipulador de tarefas concluí-la, etc. Existem muitos problemas sutis de simultaneidade. Não tente DIY.
9.5 e
SKIP LOCKED
O PostgreSQL 9.5, ainda em desenvolvimento, adiciona um recurso que torna o enfileiramento um pouco mais fácil.
Ele permite que você diga que if when you
SELECT ... FOR UPDATE
, se uma linha estiver bloqueada, você deve ignorá-la e continuar para encontrar a próxima linha não bloqueada. Isso é muito útil quando combinado com umLIMIT
, pois permite dizer "encontre-me a primeira linha que outra pessoa ainda não está tentando reivindicar". Portanto, torna-se muito simples escrever filas que capturam trabalhos de forma segura e simultânea.Até que esse recurso esteja disponível, sugiro fortemente que você se atenha ao gerenciamento de fila de conexão única ou use um sistema de fila de tarefas bem testado.
A resposta de craig-ringer responde totalmente à pergunta. Por uma questão de integridade, adiciono o seguinte código trivial para resolver o problema usando bloqueios: