Como implementar corretamente o bloqueio otimista no MySQL

13

Como se implementa corretamente o bloqueio otimista no MySQL?

Nossa equipe deduziu que devemos fazer o item 4 abaixo ou existe o risco de que outro thread possa atualizar a mesma versão do registro, mas gostaríamos de validar que essa é a melhor maneira de fazê-lo.

  1. Crie um campo de versão na tabela para o qual você deseja usar o bloqueio otimista, por exemplo, nome da coluna = "versão"
  2. Ao selecionar, inclua a coluna da versão e anote a versão
  3. Em uma atualização subsequente do registro, a instrução de atualização deve emitir "where version = X", em que X é a versão que recebemos no item 2 e defina o campo de versão durante essa instrução de atualização como X + 1
  4. Execute um SELECT FOR UPDATEno registro que vamos atualizar para serializar quem pode fazer alterações no registro que estamos tentando atualizar.

Para esclarecer, estamos tentando impedir que dois encadeamentos que selecionam o mesmo registro na mesma janela de tempo em que eles capturam a mesma versão do registro substituam uns aos outros, caso tentem atualizar o registro ao mesmo tempo. Acreditamos que, a menos que façamos o número 4, há uma chance de que, se os dois threads inserirem suas respectivas transações ao mesmo tempo (mas ainda não emitiram suas atualizações), quando atualizarem, o segundo segmento que usará o UPDATE ... onde version = X estará operando em dados antigos.

Estamos corretos ao pensar que devemos fazer esse bloqueio pessimista ao atualizar, mesmo usando campos de versão / bloqueio otimista?

Melhores Práticas
fonte
Qual é o problema? Se você aumentar o número da versão com seu UPDATE, o segundo UPDATE falhará porque o número da versão não é o mesmo de quando foi lido - e é isso que você deseja.
21412 AndreKR
Você está certo? Não está claro que, a menos que você defina o nível de isolamento da transação para uma configuração específica, você realmente verá os outros threads atualizados. Se você digitar a transação ao mesmo tempo, o segundo encadeamento poderá muito bem ver os dados OLD quando realizar a atualização. O MySQL não é tão robusto na área de ACID quanto o Oracle, portanto, procurando a melhor maneira de implementar o bloqueio otimista no MySQL que evitará atualizações / leituras sujas.
BestPractices
Mas a transação falhará de qualquer maneira durante a confirmação, certo?
AndreKR
As indicações são de que alguém desejaria fazer uma seleção para atualização para lidar com esta situação: dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
BestPractices
@BestPractices Você precisa de um SELECT ... FOR UPDATE ou bloqueio otimista por versão de linha, não por ambos. Veja os detalhes na resposta.
Craig Ringer

Respostas:

17

Seu desenvolvedor está enganado. Você precisa de um SELECT ... FOR UPDATE ou outro controle de versão, não de ambos.

Experimente e veja. Abertas três sessões MySQL (A), (B)e (C)para o mesmo banco de dados.

Em (C)questão:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;

Em ambos (A)e (B)emita um UPDATEque testa e define a versão da linha, alterando o winnertexto em cada um para que você possa ver qual sessão é qual:

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;

Agora (C), UNLOCK TABLES;para liberar a trava.

(A)e (B)vai correr pelo bloqueio da linha. Um deles vai ganhar e conseguir o bloqueio. O outro irá bloquear na fechadura. O vencedor que conseguiu o bloqueio irá mudar de linha. Supondo que (A)seja o vencedor, agora você pode ver a linha alterada (ainda não confirmada e não visível para outras transações) com a SELECT * FROM test WHERE id = 1.

Agora, COMMITna sessão do vencedor, digamos (A).

(B)obterá o bloqueio e prosseguirá com a atualização. No entanto, a versão não corresponde mais e, portanto, não altera as linhas, conforme relatado pelo resultado da contagem de linhas. Apenas um UPDATEteve algum efeito, e o aplicativo cliente pode ver claramente qual UPDATEteve êxito e qual falhou. Nenhum bloqueio adicional é necessário.

Veja os logs da sessão em pastebin aqui . Eu usei mysql --prompt="A> "etc para facilitar a diferença entre as sessões. Copiei e colei a saída intercalada na sequência de tempo, portanto, não é uma saída totalmente bruta e é possível que eu tenha cometido erros ao copiar e colar. Teste você mesmo para ver.


Se você tivesse não adicionou um campo versão de linha, então você precisa SELECT ... FOR UPDATEpara ser capaz de garantir de forma confiável ordenação.

Se você pensar bem, a SELECT ... FOR UPDATEé completamente redundante se você estiver imediatamente fazendo uma UPDATEreutilização de dados do SELECT, ou se você estiver usando o controle de versão de linha. O UPDATEbloqueio será bloqueado de qualquer maneira. Se alguém atualizar a linha entre a leitura e a gravação subsequente, sua versão não corresponderá mais, portanto a atualização falhará. É assim que o bloqueio otimista funciona.

O objetivo de SELECT ... FOR UPDATEé:

  • Gerenciar a ordem de bloqueio para evitar conflitos; e
  • Para estender o período de um bloqueio de linha para quando você deseja ler dados de uma linha, altere-os no aplicativo e escreva uma nova linha baseada na original sem precisar usar SERIALIZABLEisolamento ou controle de versão de linha.

Você não precisa usar o bloqueio otimista (versão de linha) e SELECT ... FOR UPDATE. Use um ou outro.

Craig Ringer
fonte
Obrigado craig. Você estava certo - o desenvolvedor estava enganado. Obrigado por executar este teste.
BestPractices
E o servidor SQL? Sempre existe um bloqueio adquirido na linha atualizada, independentemente do nível de isolamento da transação?
Plalx
@ Plalx Bem, o que a documentação diz? O que acontece se você executar um teste interativo como este?
Craig Ringer
@ CraigRinger, o que acontecerá se B conseguir o bloqueio antes de A confirmar, mas depois de A atualizar?
MengT
1
@MengT Não pode, é por isso que é uma trava.
Craig Ringer
0
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

Sem bloqueios (sem tabela, sem transação) necessários ou mesmo desejados:

  • UPDATE é atômico
  • LAST_INSERT_ID () é específico da sessão, portanto, seguro para threads.
Rick James
fonte