De waar-voorwaarde wordt gerespecteerd tijdens een racesituatie, maar je moet voorzichtig zijn hoe je controleert om te zien wie de race heeft gewonnen.
Overweeg de volgende demonstratie van hoe dit werkt en waarom u voorzichtig moet zijn.
Stel eerst enkele minimale tabellen in.
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
speelt de rol van id
in uw tabel, updated_by_connection_id
gedraagt zich als assignedPhone
, en locked
zoals reservationCompleted
.
Laten we nu beginnen met de racetest. Je moet 2 commandline/terminal-vensters open hebben staan, verbonden met mysql en de database gebruiken waarin je deze tabellen hebt aangemaakt.
Aansluiting 1
start transaction;
Aansluiting 2
start transaction;
Aansluiting 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Aansluiting 2
UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
Verbinding 2 wacht nu
Aansluiting 1
SELECT * FROM table1 WHERE id = 1;
commit;
Op dit punt wordt verbinding 2 vrijgegeven om door te gaan en wordt het volgende uitgevoerd:
Aansluiting 2
SELECT * FROM table1 WHERE id = 1;
commit;
Alles ziet er goed uit. We zien dat ja, de WHERE-clausule werd gerespecteerd in een racesituatie.
De reden dat ik zei dat je voorzichtig moest zijn, is omdat in een echte applicatie dingen niet altijd zo eenvoudig zijn. U MOGELIJK andere acties aan de gang hebben binnen de transactie, en dat kan de resultaten daadwerkelijk veranderen.
Laten we de database resetten met het volgende:
delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);
En denk nu eens na over deze situatie, waarbij een SELECT wordt uitgevoerd vóór de UPDATE.
Aansluiting 1
start transaction;
SELECT * FROM table2;
Aansluiting 2
start transaction;
SELECT * FROM table2;
Aansluiting 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Aansluiting 2
UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
Verbinding 2 wacht nu
Aansluiting 1
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;
Op dit punt wordt verbinding 2 vrijgegeven om door te gaan en wordt het volgende uitgevoerd:
Oké, laten we eens kijken wie er heeft gewonnen:
Aansluiting 2
SELECT * FROM table1 WHERE id = 1;
Wacht wat? Waarom is locked
0 en updated_by_connection_id
NULL??
Dit is het voorzichtig zijn dat ik noemde. De boosdoener is eigenlijk te wijten aan het feit dat we in het begin een selectie hebben gedaan. Om het juiste resultaat te krijgen, kunnen we het volgende uitvoeren:
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;
Door SELECT ... FOR UPDATE te gebruiken, kunnen we het juiste resultaat krijgen. Dit kan erg verwarrend zijn (zoals het oorspronkelijk voor mij was), omdat een SELECT en een SELECT ... VOOR UPDATE twee verschillende resultaten geven.
De reden dat dit gebeurt is vanwege het standaard isolatieniveau READ-REPEATABLE
. Wanneer de eerste SELECT is gemaakt, direct na de start transaction;
, wordt er een momentopname gemaakt. Alle toekomstige niet-bijgewerkte uitlezingen zullen vanaf die momentopname worden gedaan.
Daarom, als je gewoon naïef SELECTEERt nadat je de update hebt gedaan, zal het de informatie uit die originele momentopname halen, die voor is de rij is bijgewerkt. Door een SELECT ... VOOR UPDATE te doen, dwingt u het om de juiste informatie te krijgen.
Maar nogmaals, in een echte toepassing kan dit een probleem zijn. Stel dat uw verzoek bijvoorbeeld is verpakt in een transactie en dat u na het uitvoeren van de update wat informatie wilt uitvoeren. Het verzamelen en uitvoeren van die informatie kan worden afgehandeld door afzonderlijke, herbruikbare code, die u NIET wilt vervuilen met FOR UPDATE-clausules "voor het geval dat". Dat zou tot veel frustratie leiden vanwege onnodige vergrendeling.
In plaats daarvan wil je een ander spoor nemen. Je hebt hier veel opties.
Een daarvan is ervoor te zorgen dat u de transactie vastlegt nadat de UPDATE is voltooid. In de meeste gevallen is dit waarschijnlijk de beste, eenvoudigste keuze.
Een andere optie is om niet te proberen SELECT te gebruiken om het resultaat te bepalen. In plaats daarvan kunt u mogelijk de betreffende rijen lezen en die gebruiken (1 rij bijgewerkt versus 0 rijen bijwerken) om te bepalen of de UPDATE een succes was.
Een andere optie, en een die ik vaak gebruik, omdat ik graag een enkel verzoek (zoals een HTTP-verzoek) volledig in een enkele transactie verpakt, is om ervoor te zorgen dat de eerste instructie die in een transactie wordt uitgevoerd, de UPDATE is of een SELECT ... VOOR UPDATE . Dat zorgt ervoor dat de momentopname NIET wordt gemaakt totdat de verbinding wordt toegestaan.
Laten we onze testdatabase opnieuw resetten en kijken hoe dit werkt.
delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);
Aansluiting 1
start transaction;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Aansluiting 2
start transaction;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Verbinding 2 wacht nu.
Aansluiting 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;
Verbinding 2 is nu vrijgegeven.
Aansluiting 2
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
| 1 | 1 | 1 |
+----+--------+--------------------------+
Hier zou u uw server-side code de resultaten van deze SELECT kunnen laten controleren en weten dat deze juist is, en zelfs niet doorgaan met de volgende stappen. Maar voor de volledigheid zal ik eindigen zoals eerder.
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;
Nu kun je zien dat in Connection 2 de SELECT en SELECT ... FOR UPDATE hetzelfde resultaat geven. Dit komt omdat de snapshot waarvan de SELECT leest, pas is gemaakt nadat verbinding 1 was vastgelegd.
Dus, terug naar uw oorspronkelijke vraag:Ja, de WHERE-clausule wordt in alle gevallen gecontroleerd door de UPDATE-instructie. U moet echter voorzichtig zijn met eventuele SELECT's die u doet, om te voorkomen dat u het resultaat van die UPDATE verkeerd bepaalt.
(Ja, een andere optie is om het transactie-isolatieniveau te wijzigen. Ik heb daar echter niet echt ervaring mee en eventuele gotchya's, dus ik ga er niet op in.)