Mysql
 sql >> Database >  >> RDS >> Mysql

Query MySQL Update - La condizione "where" verrà rispettata in race condition e blocco delle righe? (php, PDO, MySQL, InnoDB)

La condizione dove verrà rispettata durante una situazione di gara, ma devi fare attenzione a come controlli per vedere chi ha vinto la gara.

Considera la seguente dimostrazione di come funziona e perché devi stare attento.

Innanzitutto, imposta alcune tabelle minime.

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 svolge il ruolo di id nella tua tabella, updated_by_connection_id agisce come assignedPhone e locked come reservationCompleted .

Ora iniziamo il test di gara. Dovresti avere 2 finestre di riga di comando/terminale aperte, connesse a mysql e utilizzando il database in cui hai creato queste tabelle.

Collegamento 1

start transaction;

Collegamento 2

start transaction;

Collegamento 1

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

Collegamento 2

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

La connessione 2 è ora in attesa

Collegamento 1

SELECT * FROM table1 WHERE id = 1;
commit;

A questo punto, la connessione 2 viene rilasciata per continuare ed emette quanto segue:

Collegamento 2

SELECT * FROM table1 WHERE id = 1;
commit;

Tutto sembra a posto. Vediamo che sì, la clausola WHERE è stata rispettata in una situazione di gara.

Il motivo per cui ho detto che dovevi stare attento, però, è perché in un'applicazione reale le cose non sono sempre così semplici. Potresti avere altre azioni in corso all'interno della transazione e ciò può effettivamente cambiare i risultati.

Resettiamo il database con quanto segue:

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

E ora, considera questa situazione, in cui viene eseguita una SELEZIONE prima dell'AGGIORNAMENTO.

Collegamento 1

start transaction;

SELECT * FROM table2;

Collegamento 2

start transaction;

SELECT * FROM table2;

Collegamento 1

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

Collegamento 2

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

La connessione 2 è ora in attesa

Collegamento 1

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

A questo punto, la connessione 2 viene rilasciata per continuare ed emette quanto segue:

Ok, vediamo chi ha vinto:

Collegamento 2

SELECT * FROM table1 WHERE id = 1;

Aspetta cosa? Perché locked 0 e updated_by_connection_id NULLA??

Questo è l'essere attenti che ho menzionato. Il colpevole è in realtà dovuto al fatto che abbiamo fatto una selezione all'inizio. Per ottenere il risultato corretto, potremmo eseguire quanto segue:

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

Usando SELECT ... FOR UPDATE possiamo ottenere il risultato giusto. Questo può creare molta confusione (come lo era per me, in origine), poiché SELECT e SELECT ... FOR UPDATE stanno dando due risultati diversi.

Il motivo per cui ciò accade è a causa del livello di isolamento predefinito READ-REPEATABLE . Quando viene effettuata la prima SELECT, subito dopo start transaction; , viene creata un'istantanea. Tutte le letture future non aggiornate verranno eseguite da quello snapshot.

Pertanto, se selezioni ingenuamente SELEZIONA dopo aver eseguito l'aggiornamento, verranno recuperate le informazioni dall'istantanea originale, che è prima la riga è stata aggiornata. Facendo un SELECT ... FOR UPDATE lo forzi per ottenere le informazioni corrette.

Tuttavia, ancora una volta, in un'applicazione reale questo potrebbe essere un problema. Supponiamo, ad esempio, che la tua richiesta sia racchiusa in una transazione e, dopo aver eseguito l'aggiornamento, desideri fornire alcune informazioni. La raccolta e l'output di tali informazioni possono essere gestite da codice separato e riutilizzabile, che NON si desidera ingombrare con clausole FOR UPDATE "per ogni evenienza". Ciò porterebbe a molta frustrazione a causa del blocco non necessario.

Invece, ti consigliamo di prendere una pista diversa. Hai molte opzioni qui.

Uno, è assicurarsi di eseguire il commit della transazione dopo che l'AGGIORNAMENTO è stato completato. Nella maggior parte dei casi, questa è probabilmente la scelta migliore e più semplice.

Un'altra opzione è non provare a utilizzare SELECT per determinare il risultato. Invece, potresti essere in grado di leggere le righe interessate e utilizzarle (1 riga aggiornata vs 0 righe aggiornate) per determinare se l'AGGIORNAMENTO ha avuto successo.

Un'altra opzione, che uso frequentemente, poiché mi piace mantenere una singola richiesta (come una richiesta HTTP) completamente racchiusa in una singola transazione, è assicurarsi che la prima istruzione eseguita in una transazione sia UPDATE o SELEZIONA... PER AGGIORNARE . Ciò farà sì che l'istantanea NON venga acquisita fino a quando la connessione non sarà autorizzata a procedere.

Resettiamo nuovamente il nostro database di test e vediamo come funziona.

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

Collegamento 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Collegamento 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

La connessione 2 è ora in attesa.

Collegamento 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;

La connessione 2 è ora rilasciata.

Collegamento 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Qui potresti effettivamente fare in modo che il tuo codice lato server controlli i risultati di questo SELECT e sapere che è accurato e non continuare nemmeno con i passaggi successivi. Ma, per completezza, finisco come prima.

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;

Ora puoi vedere che nella connessione 2 SELECT e SELECT ... FOR UPDATE danno lo stesso risultato. Ciò è dovuto al fatto che lo snapshot da cui legge SELECT è stato creato solo dopo il commit della connessione 1.

Quindi, tornando alla tua domanda originale:Sì, la clausola WHERE è verificata dall'istruzione UPDATE, in tutti i casi. Tuttavia, devi fare attenzione con eventuali SELECT che potresti eseguire, per evitare di determinare in modo errato il risultato di tale AGGIORNAMENTO.

(Sì, un'altra opzione è quella di modificare il livello di isolamento della transazione. Tuttavia, non ho esperienza con questo e con qualsiasi gotchya che potrebbe esistere, quindi non entrerò nel merito.)