MySQL: transações vs tabelas de bloqueio

110

Estou um pouco confuso com transações vs tabelas de bloqueio para garantir a integridade do banco de dados e ter certeza de que SELECT e UPDATE permaneçam sincronizados e nenhuma outra conexão interfira com isso. Eu preciso:

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) {
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...
}

Preciso garantir que nenhuma outra consulta interfira e execute o mesmo SELECT(lendo o 'valor antigo' antes que a conexão termine de atualizar a linha.

Eu sei que posso usar como padrão LOCK TABLES tableapenas ter certeza de que apenas 1 conexão está fazendo isso por vez e desbloqueá-la quando terminar, mas isso parece um exagero. Encapsular isso em uma transação faria a mesma coisa (garantindo que nenhuma outra conexão tentasse o mesmo processo enquanto outra ainda estivesse em processamento)? Ou seria um SELECT ... FOR UPDATEou SELECT ... LOCK IN SHARE MODEseria melhor?

Ryan
fonte

Respostas:

173

O bloqueio de tabelas evita que outros usuários do banco de dados afetem as linhas / tabelas que você bloqueou. Mas as travas, por si mesmas, NÃO garantirão que sua lógica saia em um estado consistente.

Pense em um sistema bancário. Quando você paga uma conta online, há pelo menos duas contas afetadas pela transação: Sua conta, da qual o dinheiro é retirado. E a conta do receptor, para a qual o dinheiro é transferido. E a conta do banco, na qual depositarão de bom grado todas as taxas de serviço cobradas na transação. Dado (como todos sabem atualmente) que os bancos são extraordinariamente estúpidos, digamos que seu sistema funcione assim:

$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) {
    charge_huge_overdraft_fees();
}
$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance

Agora, sem bloqueios e sem transações, este sistema está vulnerável a várias condições de corrida, a maior das quais são vários pagamentos realizados em sua conta ou na conta do receptor em paralelo. Embora seu código tenha seu saldo recuperado e esteja executando o grande_overdraft_fees () e outros enfeites, é inteiramente possível que algum outro pagamento execute o mesmo tipo de código em paralelo. Eles recuperarão seu saldo (digamos, $ 100), farão suas transações (retire os $ 20 que você está pagando e os $ 30 com os quais estão enganando você) e agora os dois caminhos de código têm dois saldos diferentes: $ 80 e $ 70. Dependendo de qual terminar por último, você acabará com um desses dois saldos em sua conta, em vez dos $ 50 que deveria ter recebido ($ 100 - $ 20 - $ 30). Neste caso, "erro bancário a seu favor"

Agora, digamos que você use bloqueios. O pagamento da sua conta ($ 20) chega primeiro, então ganha e bloqueia o registro da sua conta. Agora você tem uso exclusivo, podendo deduzir os $ 20 do saldo, e reaver o novo saldo de volta em paz ... e sua conta fica com $ 80 como esperado. Mas ... uhoh ... Você tenta atualizar a conta do receptor, e ela está bloqueada, e bloqueada por mais tempo do que o código permite, expirando sua transação ... Estamos lidando com bancos estúpidos, então, em vez de ter o erro adequado manipulando, o código apenas puxa um exit(), e seus $ 20 desaparecem em uma nuvem de elétrons. Agora você está sem $ 20 e ainda deve $ 20 ao receptor, e seu telefone é retomado.

Então ... insira as transações. Você inicia uma transação, debita $ 20 de sua conta, tenta creditar o receptor com $ 20 ... e algo explode novamente. Mas desta vez, em vez de exit(), o código pode apenas fazer rollback, e puf, seus $ 20 são magicamente adicionados de volta à sua conta.

No final, tudo se resume a isso:

Os bloqueios impedem que qualquer outra pessoa interfira nos registros do banco de dados com os quais você está lidando. As transações evitam que quaisquer erros "posteriores" interfiram nas coisas "anteriores" que você fez. Nenhum dos dois pode garantir que tudo dê certo no final. Mas juntos, eles fazem.

na lição de amanhã: The Joy of Deadlocks.

Marc B
fonte
4
Eu também estou / ainda estou confuso. Digamos que a conta do receptor tivesse $ 100 para começar e estamos adicionando o pagamento da conta de $ 20 de nossa conta. Meu entendimento sobre transações é que, quando elas começam, qualquer operação dentro da transação vê o banco de dados no estado em que estava no início da transação. ou seja: até que o alteremos, a conta do receptor tem $ 100. Então ... quando adicionamos $ 20, estabelecemos um saldo de $ 120. Mas o que acontecerá se, durante nossa transação, alguém esvaziar a conta do receptor para $ 0? Isso é evitado de alguma forma? Eles magicamente recebem $ 120 novamente? É por isso que os bloqueios também são necessários?
Russ
Sim, é aí que os bloqueios entram em jogo. Um sistema adequado travaria a gravação do registro para que ninguém mais pudesse atualizar o registro enquanto a transação está em andamento. Um sistema paranóico colocaria um bloqueio incondicional no registro para que ninguém pudesse ler o saldo "velho" também.
Marc B
1
Basicamente, olhe para as transações como algo seguro dentro do caminho do código. Bloqueia coisas seguras em caminhos de código "paralelos". Até o impasse chegar ...
Marc B
1
@MarcB, então por que temos que fazer o bloqueio explicitamente se usar apenas transações já garante que os bloqueios estão no lugar? Haverá mesmo um caso em que devemos fazer bloqueio explícito porque as transações por si só são insuficientes?
Pacerier
2
Esta resposta não está correta e pode levar a conclusões erradas. Esta declaração: "Os bloqueios impedem que qualquer outra pessoa interfira nos registros do banco de dados com os quais você está lidando. As transações impedem que quaisquer erros" posteriores "interfiram nas coisas" anteriores "que você fez. Nenhum dos dois isoladamente pode garantir que as coisas funcionem bem no fim. Mas juntos, eles fazem. " - faria com que você fosse demitido, é extremamente errado e estúpido. Consulte os artigos: en.wikipedia.org/wiki/ACID , en.wikipedia.org/wiki/Isolation_(database_systems) e dev.mysql.com/doc/refman/5.1/ en /…
Nikola Svitlica
14

Você quer um SELECT ... FOR UPDATEou SELECT ... LOCK IN SHARE MODEdentro de uma transação, como você disse, já que normalmente SELECTs, não importa se eles estão em uma transação ou não, não irão bloquear uma tabela. Qual você escolher depende se você deseja que outras transações possam ler essa linha enquanto sua transação está em andamento.

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOTnão resolverá o problema para você, pois outras transações ainda podem surgir e modificar essa linha. Isso é mencionado bem no topo do link abaixo.

Se outras sessões atualizam simultaneamente a mesma [...] tabela, você pode ver a tabela em um estado que nunca existiu no banco de dados.

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

Alison R.
fonte
7

Os conceitos de transação e bloqueios são diferentes. No entanto, a transação usava bloqueios para ajudá-la a seguir os princípios do ACID. Se você deseja que a tabela evite que outros leiam / escrevam no mesmo ponto de tempo enquanto você está lendo / gravando, você precisa de um bloqueio para fazer isso. Se você deseja garantir a integridade e consistência dos dados, é melhor usar as transações. Acho que conceitos mistos de níveis de isolamento em transações com bloqueios. Por favor, procure níveis de isolamento de transações, SERIALIZE deve ser o nível que você deseja.

tczhaodachuan
fonte
Esta deve ser a resposta correta. O bloqueio é para evitar condições de corrida e as transações são para atualizar várias tabelas com dados dependentes. Dois conceitos totalmente diferentes, apesar de as transações usarem travas.
Água Azul,
6

Tive um problema semelhante ao tentar um IF NOT EXISTS ...e depois executar um INSERTque causou uma condição de corrida quando vários threads estavam atualizando a mesma tabela.

Eu encontrei a solução para o problema aqui: Como escrever consultas INSERT IF NOT EXISTS no SQL padrão

Sei que isso não responde diretamente à sua pergunta, mas o mesmo princípio de realizar uma verificação e inserir como uma única instrução é muito útil; você deve ser capaz de modificá-lo para realizar sua atualização.

Tony
fonte
2

Você está confuso com bloqueio e transação. São duas coisas diferentes no RMDB. O bloqueio evita operações simultâneas enquanto a transação se concentra no isolamento de dados. Confira este ótimo artigo para esclarecimentos e algumas soluções elegantes.

David
fonte
1
Os bloqueios evitam que outras pessoas interfiram nos registros com os quais você está trabalhando, descreve o que ele faz de forma sucinta e as transações evitam que erros posteriores (aqueles de outras pessoas fazendo alterações em paralelo) interfiram em coisas anteriores que você fez (permitindo a reversão no caso de alguém fazer algo em paralelo) praticamente resume as transações ... o que está confuso sobre sua compreensão desses tópicos?
steviesama
1

Eu usaria um

START TRANSACTION WITH CONSISTENT SNAPSHOT;

para começar, e um

COMMIT;

para terminar.

Qualquer coisa que você fizer no meio é isolada dos outros usuários do seu banco de dados se o seu mecanismo de armazenamento suportar transações (que é InnoDB).

Martin Schapendonk
fonte
1
Exceto que a tabela da qual ele está selecionando não será bloqueada para outras sessões, a menos que ele especificamente a bloqueie (ou até que seu UPDATE aconteça), o que significa que outras sessões podem vir e modificá-la entre SELECT e UPDATE.
Alison R.
Depois de ler START TRANSACTION WITH CONSISTENT SNAPSHOT na documentação do MySQL, não vejo onde ele realmente bloqueia outra conexão de atualizar a mesma linha. Meu entendimento é que veria porém a mesa começada no início da transação. Portanto, se outra transação está em andamento, já obteve uma linha e está prestes a atualizá-la, a 2ª transação ainda verá a linha antes de ter sido atualizada. Portanto, ele poderia tentar atualizar a mesma linha que a outra transação está prestes a atualizar. Isso está correto ou estou faltando alguma coisa no andamento?
Ryan
1
@Ryan não faz nenhum bloqueio; você está certo. O bloqueio (ou não) é determinado pelo tipo de operação que você faz (SELECT / UPDATE / DELETE).
Alison R.
4
Entendo. Ele dá consistência de leitura à sua própria transação, mas não impede que outros usuários modifiquem uma linha antes de você.
Martin Schapendonk