Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Consulta de atualização do MySQL - A condição 'onde' será respeitada na condição de corrida e no bloqueio de linha? (php, PDO, MySQL, InnoDB)


A condição where será respeitada durante uma situação de corrida, mas você deve ter cuidado ao verificar quem ganhou a corrida.

Considere a seguinte demonstração de como isso funciona e por que você deve ter cuidado.

Primeiro, configure algumas tabelas mínimas.
CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id desempenha o papel de id em sua tabela, updated_by_connection_id age como assignedPhone , e locked como reservationCompleted .

Agora vamos começar o teste de corrida. Você deve ter 2 janelas de linha de comando/terminal abertas, conectadas ao mysql e usando o banco de dados onde você criou essas tabelas.

Conexão 1
start transaction;

Conexão 2
start transaction;

Conexão 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Conexão 2
UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

A conexão 2 está agora esperando

Conexão 1
SELECT * FROM table1 WHERE id = 1;
commit;

Neste ponto, a conexão 2 é liberada para continuar e gera o seguinte:

Conexão 2
SELECT * FROM table1 WHERE id = 1;
commit;

Tudo parece bem. Vemos que sim, a cláusula WHERE foi respeitada em uma situação de corrida.

A razão pela qual eu disse que você tinha que ter cuidado, porém, é porque em uma aplicação real as coisas nem sempre são tão simples. Você PODE ter outras ações acontecendo dentro da transação, e isso pode realmente alterar os resultados.

Vamos redefinir o banco de dados com o seguinte:
delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

E agora, considere esta situação, onde um SELECT é executado antes do UPDATE.

Conexão 1
start transaction;

SELECT * FROM table2;

Conexão 2
start transaction;

SELECT * FROM table2;

Conexão 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Conexão 2
UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

A conexão 2 está agora esperando

Conexão 1
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Neste ponto, a conexão 2 é liberada para continuar e gera o seguinte:

Bom, vamos ver quem ganhou:

Conexão 2
SELECT * FROM table1 WHERE id = 1;

Espere o que? Por que locked 0 e updated_by_connection_id NULO??

Este é o cuidado que mencionei. O culpado é, na verdade, o fato de termos feito um select no início. Para obter o resultado correto, podemos executar o seguinte:
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Usando SELECT ... FOR UPDATE podemos obter o resultado correto. Isso pode ser muito confuso (como foi para mim, originalmente), pois um SELECT e um SELECT ... FOR UPDATE estão dando dois resultados diferentes.

A razão pela qual isso acontece é por causa do nível de isolamento padrão READ-REPEATABLE . Quando o primeiro SELECT é feito, logo após o start transaction; , um instantâneo é criado. Todas as leituras futuras sem atualização serão feitas a partir desse instantâneo.

Portanto, se você simplesmente SELECT ingenuamente depois de fazer a atualização, ele extrairá as informações desse instantâneo original, que é antes a linha foi atualizada. Ao fazer um SELECT ... FOR UPDATE você o força a obter as informações corretas.

No entanto, novamente, em uma aplicação real, isso pode ser um problema. Digamos, por exemplo, que sua solicitação seja encapsulada em uma transação e, após realizar a atualização, você deseja gerar algumas informações. A coleta e a saída dessas informações podem ser tratadas por código separado e reutilizável, que você NÃO deseja desordenar com cláusulas FOR UPDATE "por via das dúvidas". Isso levaria a muita frustração devido ao bloqueio desnecessário.

Em vez disso, você vai querer fazer uma trilha diferente. Você tem muitas opções aqui.

Um, é certificar-se de que você confirma a transação após a conclusão do UPDATE. Na maioria dos casos, esta é provavelmente a escolha melhor e mais simples.

Outra opção é não tentar usar SELECT para determinar o resultado. Em vez disso, você pode ler as linhas afetadas e usá-las (1 linha atualizada versus 0 linha atualizada) para determinar se o UPDATE foi bem-sucedido.

Outra opção, e que uso com frequência, pois gosto de manter uma única solicitação (como uma solicitação HTTP) totalmente envolvida em uma única transação, é garantir que a primeira instrução executada em uma transação seja a UPDATE ou um SELECT ... FOR UPDATE . Isso fará com que o instantâneo NÃO seja tirado até que a conexão tenha permissão para continuar.

Vamos redefinir nosso banco de dados de teste novamente e ver como isso funciona.
delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Conexão 1
start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Conexão 2
start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

A conexão 2 está agora esperando.

Conexão 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

A conexão 2 agora está liberada.

Conexão 2
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Aqui você pode realmente fazer com que seu código do lado do servidor verifique os resultados deste SELECT e saiba que é preciso, e nem mesmo continue com as próximas etapas. Mas, para completar, vou terminar como antes.
UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Agora você pode ver que na Conexão 2 o SELECT e SELECT ... FOR UPDATE dão o mesmo resultado. Isso ocorre porque o instantâneo do qual o SELECT lê não foi criado até que a Conexão 1 tenha sido confirmada.

Então, de volta à sua pergunta original:Sim, a cláusula WHERE é verificada pela instrução UPDATE, em todos os casos. No entanto, você deve ter cuidado com quaisquer SELECTs que possa estar fazendo, para evitar determinar incorretamente o resultado dessa UPDATE.

(Sim, outra opção é alterar o nível de isolamento da transação. No entanto, eu realmente não tenho experiência com isso e quaisquer pegadinhas que possam existir, então não vou entrar nisso.)