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

MySQL Update-forespørgsel - Vil 'hvor'-tilstanden blive respekteret på race-tilstand og rækkelåsning? (php, PDO, MySQL, InnoDB)

Hvor betingelsen vil blive respekteret under en løbssituation, men du skal være forsigtig, hvordan du tjekker for at se, hvem der vandt løbet.

Overvej følgende demonstration af, hvordan dette virker, og hvorfor du skal være forsigtig.

Først skal du opsætte nogle minimale borde.

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 spiller rollen som id i din tabel, updated_by_connection_id fungerer som assignedPhone , og locked som reservationCompleted .

Lad os nu starte racetesten. Du bør have 2 kommandolinje-/terminalvinduer åbne, forbundet til mysql og bruge databasen, hvor du har oprettet disse tabeller.

Forbindelse 1

start transaction;
 

Forbindelse 2

start transaction;
 

Forbindelse 1

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

Forbindelse 2

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

Forbindelse 2 venter nu

Forbindelse 1

SELECT * FROM table1 WHERE id = 1;
 
commit;
 

På dette tidspunkt frigives forbindelse 2 for at fortsætte og udsender følgende:

Forbindelse 2

SELECT * FROM table1 WHERE id = 1;
 
commit;
 

Alt ser fint ud. Vi ser, at ja, WHERE-klausulen blev respekteret i en racersituation.

Grunden til, at jeg sagde, at du skulle være forsigtig, er, at tingene ikke altid er så enkle i en rigtig applikation. Du MÅSKE have andre handlinger i gang i transaktionen, og det kan faktisk ændre resultaterne.

Lad os nulstille databasen med følgende:

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

Og overvej nu denne situation, hvor en SELECT udføres før OPDATERING.

Forbindelse 1

start transaction;

SELECT * FROM table2;
 

Forbindelse 2

start transaction;

SELECT * FROM table2;
 

Forbindelse 1

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

Forbindelse 2

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

Forbindelse 2 venter nu

Forbindelse 1

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

På dette tidspunkt frigives forbindelse 2 for at fortsætte og udsender følgende:

Ok, lad os se, hvem der vandt:

Forbindelse 2

SELECT * FROM table1 WHERE id = 1;
 

Vent, hvad? Hvorfor er locked 0 og updated_by_connection_id NULL?

Det er den forsigtighed, jeg nævnte. Synderen skyldes faktisk, at vi lavede et udvalg i begyndelsen. For at få det korrekte resultat kunne vi køre følgende:

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

Ved at bruge SELECT ... TIL OPDATERING kan vi få det rigtige resultat. Dette kan være meget forvirrende (som det var for mig, oprindeligt), da et SELECT og et SELECT ... TIL OPDATERING giver to forskellige resultater.

Årsagen til dette sker er på grund af standardisolationsniveauet READ-REPEATABLE . Når det første SELECT er foretaget, lige efter start transaction; , oprettes et øjebliksbillede. Alle fremtidige ikke-opdaterende læsninger vil blive udført fra det øjebliksbillede.

Derfor, hvis du bare naivt VÆLGER, efter du har foretaget opdateringen, vil det trække oplysningerne fra det originale snapshot, som er før rækken er blevet opdateret. Ved at gøre et VÆLG ... TIL OPDATERING tvinger du det til at få de korrekte oplysninger.

Men igen, i en rigtig applikation kan dette være et problem. Lad os f.eks. sige, at din anmodning er pakket ind i en transaktion, og efter at du har udført opdateringen, vil du udlæse nogle oplysninger. Indsamling og udlæsning af, at information kan håndteres af separat, genbrugelig kode, som du IKKE ønsker at fylde med FOR UPDATE-klausuler "for en sikkerheds skyld." Det ville føre til masser af frustration på grund af unødvendig låsning.

I stedet vil du gerne tage et andet spor. Du har mange muligheder her.

Den ene er at sikre, at du forpligter transaktionen, efter at OPDATERING er fuldført. I de fleste tilfælde er dette nok det bedste, enkleste valg.

En anden mulighed er ikke at prøve at bruge SELECT til at bestemme resultatet. I stedet kan du muligvis læse de berørte rækker og bruge den (1 række opdateret vs. 0 række opdatering) til at afgøre, om OPDATERING var en succes.

En anden mulighed, og en som jeg bruger ofte, da jeg godt kan lide at holde en enkelt anmodning (som en HTTP-anmodning) fuldstændig pakket ind i en enkelt transaktion, er at sikre, at den første sætning, der udføres i en transaktion, enten er OPDATERING eller en VÆLG ... TIL OPDATERING . Det vil medføre, at snapshotet IKKE tages, før forbindelsen får lov til at fortsætte.

Lad os nulstille vores testdatabase igen og se, hvordan det fungerer.

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

Forbindelse 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 

Forbindelse 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 

Forbindelse 2 venter nu.

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

Forbindelse 2 er nu frigivet.

Forbindelse 2

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

Her kunne du faktisk få din serversidekode til at tjekke resultaterne af denne SELECT og vide, at den er nøjagtig, og ikke engang fortsætte med de næste trin. Men for fuldstændighedens skyld slutter jeg som før.

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 kan du se, at i forbindelse 2 giver SELECT og SELECT ... FOR UPDATE det samme resultat. Dette skyldes, at det øjebliksbillede, som SELECT læser fra, ikke blev oprettet før, efter at forbindelse 1 var blevet overført.

Så tilbage til dit oprindelige spørgsmål:Ja, WHERE-sætningen kontrolleres i alle tilfælde af UPDATE-sætningen. Du skal dog være forsigtig med eventuelle SELECTS, du måtte foretage, for at undgå forkert bestemmelse af resultatet af denne OPDATERING.

(Ja en anden mulighed er at ændre transaktionsisolationsniveauet. Jeg har dog ikke rigtig erfaring med det og eventuelle gotchyaer, der måtte eksistere, så jeg vil ikke gå ind i det.)



  1. Sådan gendanner du MySQL-dump fra vært til Docker-container

  2. forespørg på mysql-database inde fra en klasse

  3. MySQL / PHP transaktionsadfærd

  4. PHP Fatal fejl Der mangler hukommelse