Bloqueio no Postgres para combinação UPDATE / INSERT

11

Eu tenho duas mesas. Um é uma tabela de log; outro contém, essencialmente, códigos de cupom que podem ser usados ​​apenas uma vez.

O usuário precisa resgatar um cupom, que inserirá uma linha na tabela de log e marcará o cupom como usado (atualizando a usedcoluna para true).

Naturalmente, há uma condição óbvia de corrida / problema de segurança aqui.

Eu fiz coisas semelhantes no passado no mundo do mySQL. Nesse mundo, eu bloqueava as duas tabelas globalmente, protegia a lógica com o conhecimento de que isso só poderia acontecer uma vez por vez e depois desbloqueia as tabelas quando terminar.

Existe uma maneira melhor no Postgres de fazer isso? Em particular, estou preocupado que o bloqueio seja global, mas não precisa ser - eu realmente só preciso garantir que ninguém mais esteja tentando inserir esse código específico, portanto, talvez algum bloqueio no nível da linha funcione?

Rob Miller
fonte

Respostas:

15

Já ouvi falar de problemas de concorrência como esse no MySQL antes. Não é assim no Postgres.

Bloqueios internos no nível da linha no nível de READ COMMITTEDisolamento da transação padrão são suficientes.

Sugiro uma única declaração com um CTE modificador de dados (algo que o MySQL também não possui) porque é conveniente passar valores de uma tabela para outra diretamente (se você precisar disso). Se você não precisar de nada da coupontabela, poderá usar uma transação com instruções UPDATEe separadas INSERTtambém.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

É raro que mais de uma transação tente resgatar o mesmo cupom. Eles têm um número único, não têm? Mais de uma transação tentando no mesmo momento no tempo deve ser muito mais rara ainda. (Talvez um bug no aplicativo ou alguém tentando enganar o sistema?)

Seja como for, o UPDATEúnico consegue exatamente uma transação, não importa o quê. Um UPDATEadquire um bloqueio no nível da linha em cada linha de destino antes de atualizar. Se uma transação simultânea tentar UPDATEa mesma linha, ela verá o bloqueio na linha e aguardará a conclusão da transação de bloqueio ( ROLLBACKou COMMIT), sendo a primeira na fila de bloqueio:

  • Se confirmado, verifique novamente a condição. Se ainda estiver NOT used, trave a linha e continue. Caso contrário, o UPDATEagora não encontra nenhuma linha qualificativa e não faz nada , não retornando nenhuma linha, portanto a INSERTtambém não faz nada.

  • Se revertida, trave a linha e continue.

Não há potencial para uma condição de corrida .

Não há potencial para um conflito, a menos que você coloque mais gravações na mesma transação ou bloqueie mais linhas do que apenas uma.

O INSERTé livre de cuidados. Se, por algum erro, o coupon_idjá estiver na logtabela (e você tiver uma restrição UNIQUE ou PK ativada log.coupon_id), toda a transação será revertida após uma violação exclusiva. Indica um estado ilegal no seu banco de dados. Se a instrução acima for a única maneira de gravar na logtabela, isso nunca deve ocorrer.

Erwin Brandstetter
fonte
De fato, é raro que mais de uma transação tente resgatar o mesmo código, mas suas suspeitas estão certas de que isso ocorrerá exclusivamente quando alguém estiver tentando jogar no sistema. Muito obrigado por isso - os CTEs foram um grande atrativo para mim ao mudar para o Postgres, mas eu não sabia que o bloqueio implícito seria bom o suficiente para isso.
Rob Miller