Je hebt geen zin om alles in één grote query te willen samenvatten, want dat lost ook niets op, het maakt het alleen minder waarschijnlijk.
Wat je nodig hebt zijn sloten op de rijen, of sloten op de index waar de nieuwe rij zou worden ingevoegd.
Dus hoe krijgen we exclusieve sloten?
Twee verbindingen, mysql1 en mysql2, die elk een exclusieve vergrendeling aanvragen met SELECT ... FOR UPDATE
. De tabel 'geschiedenis' heeft een kolom 'user_id' die is geïndexeerd. (Het is ook een externe sleutel.) Er zijn geen rijen gevonden, dus ze lijken allebei normaal te verlopen alsof er niets ongewoons gaat gebeuren. De user_id 2808 is geldig maar heeft niets in de geschiedenis.
mysql1> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql2> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql1> select * from history where user_id = 2808 for update;
Empty set (0.00 sec)
mysql2> select * from history where user_id = 2808 for update;
Empty set (0.00 sec)
mysql1> insert into history(user_id) values (2808);
... en ik krijg mijn prompt niet terug ... geen reactie ... omdat een andere sessie ook een slot heeft ... maar dan:
mysql2> insert into history(user_id) values (2808);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Dan retourneert mysql1 onmiddellijk succes op de insert.
Query OK, 1 row affected (3.96 sec)
Het enige dat overblijft is voor mysql1 om COMMIT
en op magische wijze hebben we voorkomen dat een gebruiker met 0 items meer dan 1 item invoegde. De impasse deed zich voor omdat er voor beide sessies onverenigbare dingen moesten gebeuren:mysql1 had mysql2 nodig om zijn lock vrij te geven voordat het kon committen en mysql2 had mysql1 nodig om zijn lock vrij te geven voordat het kon worden ingevoegd. Iemand moet dat gevecht verliezen, en over het algemeen is de draad die het minste werk heeft gedaan de verliezer.
Maar wat als er al 1 of meer rijen bestonden toen ik de SELECT ... FOR UPDATE
deed ? In dat geval zou het slot op de rijen zijn geweest, dus de tweede sessie om te proberen SELECT
zou eigenlijk het wachten op de SELECT
. blokkeren totdat de eerste sessie besloot om ofwel COMMIT
of ROLLBACK
, op welk moment de tweede sessie een nauwkeurige telling van het aantal rijen zou hebben gezien (inclusief eventuele ingevoegde of verwijderde rijen door de eerste sessie) en nauwkeurig had kunnen beslissen dat de gebruiker al het toegestane maximum had.
Je kunt een raceconditie niet overtreffen, maar je kunt ze wel buitensluiten.