Transacties in Firebird
Transacties in Firebird: ACID, isolatieniveaus, deadlocks en het oplossen van updateconflicten
Alexey Kovyazin, met hulp van Vlad Khorsun en Dmitry Kuzmenko, 08-APR-2019
Inhoud:
- Hoe we over transacties zullen praten
- ACID in schema’s
- Transactie-isolatieniveaus in Firebird
- Het oplossen van updateconflicten: de Wait-optie
- Echte deadlock
Is het nodig om te weten hoe transacties werken?
Waarschijnlijk wel, omdat het begrip van transacties eenvoudig is, onderschatten veel ontwikkelaars het belang van het correct gebruiken van transacties in Firebird. Pas wanneer je een grondig begrip hebt van hoe transacties werken, kun je veel mysterieuze zaken met betrekking tot prestaties begrijpen, zoals plotselinge databasevertragingen (veroorzaakt door het opruimen van overmatige recordversies die ontstaan door slecht transactiebeheer).
Over het algemeen wordt het begrip van een transactie toegepast op elk dynamisch systeem dat van de ene toestand naar de andere overgaat. Een klassiek voorbeeld van een transactie is het overboeken van geld van de ene rekening naar de andere. Meestal ziet dat er ongeveer zo uit:
Begin --- geld overboeken van rekening 1 naar rekening 2
--rekening 1 verlagen
--rekening 2 verhogen
Einde - de transactie vastleggen
Het voorbeeld komt erop neer dat het geld tegelijkertijd van rekening 1 moet verdwijnen en op rekening 2 moet verschijnen, anders is er tijdelijk sprake van een overschot of een onverklaarbaar tekort aan geld in het systeem.
Vanuit het standpunt van databases wordt een transactie meestal gedefinieerd als een groep bewerkingen die op een database worden uitgevoerd en die als onafhankelijk van andere transacties wordt beschouwd. Naar mijn mening is deze definitie niet beter of slechter dan andere definities, maar zoals elke definitie heeft deze weinig zin zonder kennis van de daadwerkelijke interne werking en logica van een DBMS.
Er wordt aangenomen dat een transactie in een database moet voldoen aan de zogenaamde ACID-vereisten
A - Atomiciteit
C - Consistentie
I - Isolatie
D - Duurzaamheid
Veel ontwikkelaars van databaseapplicaties zijn zo geïnspireerd door dit acroniem dat ze vaak argumenten gebruiken zoals “je hebt geen D in ACID” wanneer het gaat om het vergelijken van verschillende DBMS’en (wat meestal onmiddellijk wordt gevolgd door “het kan me niet schelen wat jij denkt”).
Eigenlijk is alles vrij eenvoudig - ACID is een reeks vereisten met betrekking tot de implementatie van een transactie in een bepaald DBMS, sommige zijn zeer strikt (bijvoorbeeld D - duurzaamheid is natuurlijk belangrijk!) terwijl andere minder strikt zijn - wanneer we naar de niveaus van transactie-isolatie kijken, zullen we zien dat isolatie kan variëren.
Daarom is het niet de moeite waard om onmiddellijk te proberen te begrijpen wat dit acroniem letterlijk betekent. In plaats daarvan zullen we de logica onderzoeken van hoe DBMS’en (en transacties in het bijzonder) werken en ACID bekijken vanuit het standpunt van “hoe het is gemaakt” in plaats van “wat het betekent”.
Omdat transactieaspecten complex zijn, hebben we een grafische weergave nodig - een soort schema’s - om het werk en de interactie van transacties te tonen. Met behulp van deze schema’s kunnen we een logisch verhaal opbouwen en in detail bekijken hoe transacties werken.
Allereerst introduceren we een tijdlijn omdat transacties zich in de tijd ontwikkelen. De tijdlijn wordt gemarkeerd zoals wij dat nodig hebben - we hebben geen seconden of minuten nodig, maar de belangrijkste stappen van interactie tussen transacties:

Vervolgens voegen we een transactie toe aan deze tijdlijn - laten we die tekenen in de vorm van een rechthoek waarvan de zijden overeenkomen met het begin en einde van de transactie. Omdat alle transacties in Firebird genummerd zijn, geven we ook het transactienummer aan.

Het schema toont dus transactie nummer 11 die op tijdstip t3 is gestart en op tijdstip t10 is geëindigd. Er zijn twee manieren waarop een transactie kan eindigen - COMMIT, d.w.z. alle wijzigingen binnen de transactie toepassen, en ROLLBACK, d.w.z. alle wijzigingen binnen de transactie annuleren. We tonen de manier waarop een transactie eindigt als volgt:

Om verder te kunnen gaan, moeten we verschillende parameters van transacties op deze schema’s specificeren en we specificeren die in de linkerbenedenhoek van de rechthoek die de betreffende transactie vertegenwoordigt - dit voorbeeld toont dat transactie #11 het snapshot-isolatieniveau heeft.

Wanneer we zeggen dat “transactie X gegevens invoegt” of “transactie Y dit en dat leest” - is dat formeel onjuist, omdat we zouden moeten zeggen “er zijn wijzigingen aangebracht binnen transactie X”. Alleen SQL-instructies kunnen gegevens lezen of invoegen, dus als het belangrijk is voor het verhaal, tonen we deze instructies binnen de transactierechthoek:

In dit voorbeeld hebben we de INSERT-bewerking voor tabel T1, veld i1, waarde 100 - deze bewerking wordt uitgevoerd binnen transactie #11 en wordt vastgelegd.
Ook zullen we soms het resultaat van een bewerking moeten tonen, bijvoorbeeld in het volgende voorbeeld:

Dit voorbeeld toont het volgende:
- Transactie #11 met de isolatieniveauparameter ingesteld op snapshot (isolatieniveaus worden later besproken, hier wordt het alleen getoond om het volledige beeld te schetsen) wordt gestart op tijdstip t3
- De bewerking INSERT INTO T1(i1) values (100) die waarde 100 invoegt in veld i1 van tabel t1 start op tijdstip t5 EN eindigt op tijdstip t7
- De bewerking SELECT i1 from T1 die waarde i1 gelijk aan 100 retourneert start op tijdstip t8
- Transactie #11 eindigt met de COMMIT-instructie, d.w.z. wijzigingen die door transactie #11 zijn aangebracht, worden aan de database vastgelegd
Zo kunnen we met behulp van transactieschema’s in detail beschrijven wat er in de database gebeurt en leren hoe transacties werken.
Nu we transactieschema’s hebben, laten we eens kijken wat het ACID-acroniem werkelijk betekent.
Atomiciteit
Atomiciteit betekent dat ofwel alle bewerkingen die een transactie vormen worden uitgevoerd, ofwel geen enkele wordt uitgevoerd: “alles of niets”. Het lijkt eenvoudig, maar dan komen de details aan het licht.
Ten eerste heeft het DBMS (niet alleen Firebird maar bijna allemaal) 2 soorten atomiciteit: atomiciteit op het niveau van een instructie en atomiciteit op het niveau van een groep instructies binnen een transactie.
Atomiciteit op instructieniveau betekent dat de instructie UPDATET1 SETX=1 WHEREY=2 altijd ofwel met succes wordt uitgevoerd ofwel niet.
Atomiciteit op het niveau van een groep instructies werkt anders (we gebruiken hier pseudocode om aan te geven wanneer de transactie wordt gestart en vastgelegd):
Start transactie 11
INSERT ..100
INSERT ..200
INSERT ..300
Commit 11
Dit is wat het ongeveer op het schema zal tonen:

Het betekent dat alle drie de INSERT-instructies met succes worden uitgevoerd en dat de wijzigingen die ze aanbrengen worden vastgelegd op het moment dat transactie #11 wordt vastgelegd.
De vraag die ik vaak stel tijdens workshops over transacties - zal de COMMIT-instructie met succes worden uitgevoerd voor transactie 11 als INSERT INTO..300 een uitzondering genereert:

Een aanzienlijk deel van het publiek antwoordt altijd dat de COMMIT-instructie niet met succes zal worden uitgevoerd! (Interessant genoeg zal dit in sommige andere DBMS’en ervoor zorgen dat de transactie wordt teruggedraaid!)
Dat is echter niet waar - voer gewoon isql uit en doe een experiment met een willekeurige database (isql heeft een eenvoudige en rechtstreekse implementatie van bewerkingen, zonder “raden” voor de gebruiker).
Het punt is dat atomiciteit op het niveau van instructiegroepen die worden gegarandeerd door het vastleggen van transacties een kwestie is van bedrijfslogica. De ontwikkelaar van een applicatie moet beslissen of een transactie moet worden vastgelegd in geval van een uitzondering in de derde INSERT-instructie of niet. Als de bedrijfslogica het mogelijk maakt om het resultaat vast te leggen, kan de COMMIT-instructie gemakkelijk worden uitgevoerd.
De atomiciteitsvereiste in ACID is dus de vereiste dat het DBMS de resultaten van een groep instructies die binnen één transactie worden uitgevoerd, kan vastleggen of terugdraaien. De beslissing om het vast te leggen of terug te draaien hangt af van de bedrijfslogica die je moet implementeren.
En laten we het nogmaals benadrukken - hoewel de atomiciteit van een transactie voor een groep instructies de mogelijkheid betekent om de hele groep vast te leggen of terug te draaien ongeacht de resultaten (en de keuze hangt af van de bedrijfslogica), wordt de atomiciteit van één instructie gegarandeerd door de implementatie van het DBMS, d.w.z. het is onmogelijk om één instructie (bijvoorbeeld UPDATE) “onvolledig” (niet atomair) uit te voeren.
Consistentie
Consistentie betekent dat gegevens in de database geen tegenstrijdigheden vertonen. Natuurlijk zien we hier een heel domein voor speculatie omdat “wat betekent ‘geen tegenstrijdigheden vertonen’ eigenlijk”?
Er worden meestal twee consistentieniveaus onderscheiden:
- Databaseniveau waar consistentie de overeenkomst van gegevens met databasebeperkingen betekent, zoals Primary, Unique en Foreign keys, Checks. Dit consistentieniveau wordt gegarandeerd door het feit dat databasebeperkingen het niet mogelijk maken om gegevens in te voegen die niet aan de beperkingen voldoen: bijv. CHECK(x>0) staat niet toe dat een negatief getal in het betreffende veld wordt ingevoegd.
- Bedrijfslogicaniveau waar de consistentie wordt gegarandeerd door de ontwikkelaar van de applicatie met behulp van tools die door het DBMS worden aangeboden, zoals transacties.
Hoe helpen transacties om consistentie op bedrijfslogicaniveau te garanderen? Heel eenvoudig - als we het voorbeeld van geldoverboeking nemen, moet de ontwikkelaar ervoor zorgen dat alle wijzigingen worden teruggedraaid in geval van een uitzondering en het gebruik van een transactie helpt hem daarbij.
Starttransactie
Verlaag het geldbedrag op rekening 1…. Succes
Verhoog het op rekening 2… Mislukt
Rollback ---- in geval van een uitzondering!
Met andere woorden, de ontwikkelaar moet code zo schrijven dat de gegevens worden teruggedraaid in geval van een uitzondering en zo wordt de consistentie van gegevens behouden vanuit het standpunt van bedrijfslogica.
Op deze manier betekent de consistentievereiste in het ACID-acroniem dat het noodzakelijk is dat het DBMS de mogelijkheid heeft om de consistentie van gegevens te handhaven met behulp van het transactiemechanisme.
Isolatie
De vereiste van transactie-isolatie komt voort uit de noodzaak om het resultaat van een reeks bewerkingen te garanderen, ongeacht de volgorde waarin ze worden uitgevoerd.
Simpel gezegd moet elke transactie met hetzelfde resultaat worden uitgevoerd, ongeacht of er gelijktijdig actieve transacties zijn.
Het mechanisme van transacties zou consistentie op het niveau van bedrijfslogica moeten garanderen, maar het zou transacties ook moeten beschermen tegen tijdelijke onbevestigde gegevens die kunnen verschijnen tijdens het proces van het uitvoeren van gelijktijdige transacties.
In de praktijk ziet het er zo uit:

We zien transactie #11 die op tijdstip t2 is gestart, waarbinnen op tijdstip t3-t5 een invoeging in de tabel wordt gemaakt. Transactie #11 wordt niet onmiddellijk na de invoeging vastgelegd, maar blijft actief tot tijdstip t8.
Gelijktijdig wordt transactie #12 gestart en voert de SELECT-instructie uit voor de tabellrecords waarbinnen transactie #11 gegevens invoegt. De eerste SELECT-instructie wordt uitgevoerd op tijdstip t6, dat is wanneer de invoegbewerking al is voltooid, maar deze instructie retourneert een leeg resultaat omdat transactie #12 geen niet-vastgelegde gegevens van andere transacties kan zien.
Transactie #11 wordt vastgelegd op tijdstip t8 en de SELECT-instructie binnen transactie #12 wordt uitgevoerd op tijdstip t9. Het retourneert het resultaat gelijk aan 100 omdat de gegevens die binnen transactie #11 zijn gemaakt nu zijn vastgelegd (en omdat het isolatieniveau van transactie #12 read committed is, maar daar zullen we later over praten).
Dit voorbeeld is al voldoende om de isolatievereiste te illustreren - in tegenstelling tot atomiciteit en consistentie wordt isolatie geïmplementeerd als strikte regels, zogenaamde isolatieniveaus, en elke transactie moet een parameter hebben die het isolatieniveau instelt waarmee deze werkt.
Duurzaamheid
Het concept van duurzaamheid stelt de ontwikkelaar in staat om er volledig op te vertrouwen dat gegevens die binnen een vastgelegde transactie zijn gemaakt, onmiddellijk in de database verschijnen en er niet uit verdwijnen (zonder expliciete verklaringen die ze verwijderen of wijzigen, natuurlijk), ongeacht wat er daarna gebeurt.
Zoals u kunt zien, is de duurzaamheidsvereiste gewoon gezond verstand - niemand zou immers instemmen met het gebruik van een systeem waarvan de gegevens plotseling kunnen verdwijnen.
ACID: samenvatting
ACID betekent de vereisten voor hoe transacties moeten werken:
- Atomiciteit
- Verklaringen zijn altijd atomair
- Groepen verklaringen kunnen atomair worden gemaakt met behulp van transacties
- Consistentie
- Twee consistentieniveaus: databasebeperkingen en bedrijfslogica
- Isolatie
- Gegarandeerd door het mechanisme van transacties met behulp van de daarvoor ingestelde isolatieniveaus
- Duurzaamheid
- Alle vastgelegde gegevens worden permanent
Zoals u ziet, is alles vrij logisch. In de praktijk vormen de isolatieniveaus de grootste moeilijkheid, dus laten we in detail bekijken hoe ze werken.
Het isolatieniveau van een transactie bepaalt welke vastgelegde gegevens deze transactie kan zien.
Er zijn isolatieniveaus die conventioneel standaard worden genoemd. Ze worden beschreven in de ANSI SQL-standaard (verschillende revisies). Voor zover ik weet, is er geen enkel DBMS waarin ze exact worden geïmplementeerd zoals ze in de standaard worden beschreven, maar niemand maakt zich daar zorgen over, aangezien de werkelijke transactiemechanismen in specifieke DBMS’en alle benodigde opties bieden voor het implementeren van bedrijfslogica.
U kunt de klassieke definitie van isolatieniveaus vinden in “A Critique of ANSI SQL Isolation Levels”
Voor degenen die dit artikel hebben gelezen, is hier de tabel die de klassieke isolatieniveaus vergelijkt met vergelijkbare niveaus in Firebird. Natuurlijk is de overeenkomst niet eenvoudig, omdat isolatieniveaus in Firebird, net als in andere DBMS’en, niet 100% voldoen aan de ANSI SQL-definities, maar ze lijken er wel sterk op.
| ANSI-isolatieniveaus | Isolatieniveau in Firebird |
| Read Uncommitted | n.v.t. |
| Read Committed | Read Committed |
| Repeatable Read | Snapshot |
| Serializable | Snapshot table stability |
Net als elk ander DBMS heeft Firebird zijn eigenaardigheden in de implementatie van isolatie. Nu zullen we ons richten op hoe isolatieniveaus in Firebird werken, in plaats van hoe goed ze voldoen aan de standaard.
Snapshot-isolatieniveau
Het Snapshot-isolatieniveau was het eerste in de oorspronkelijke code van InterBase en blijft het standaardniveau voor de Firebird-kern-API en hulpprogramma’s (bijvoorbeeld isql.exe). Dit is mogelijk de reden waarom het het gemakkelijkst te begrijpen is.
Snapshot isoleert de transactie van alle wijzigingen die zijn aangebracht vanaf het moment dat de transactie start.
Laten we eens kijken naar het transactiediagram hieronder: het toont transactie #10 die is gestart met het snapshot-isolatieniveau. Binnen deze transactie worden verschillende SELECT-verklaringen uitgevoerd voor tabel T1 die in dit voorbeeld geen records bevat.

Gelijktijdige transactie #15, die na de start van transactie #10 is gestart, voegt gegevens in in tabel T1 en deze transactie eindigt met de COMMIT-verklaring op moment t9, d.w.z. de gegevens worden op dit moment in de database vastgelegd en zijn beschikbaar voor verklaringen van andere transacties.
De verklaring in transactie #10 die op moment t10 wordt uitgevoerd (d.w.z. nadat transactie #15 is vastgelegd) ziet de ingevoegde gegevens echter niet, omdat het snapshot-isolatieniveau alleen vastgelegde gegevens toestaat die vóór de start van transactie #10 zijn ingevoegd of gewijzigd.
Het Snapshot-isolatieniveau stelt u dus in staat om met de database te werken alsof deze bevroren is op het moment dat de transactie start. Het is meestal nodig om ingewikkelde rapporten te bouwen op basis van snel veranderende gegevens: snapshot wordt gebruikt om de situatie te vermijden waarin het eerste deel van het rapport is gebaseerd op bepaalde gegevens en het laatste deel op andere gegevens.
Deze geweldige functie heeft echter zijn prijs - wanneer we later onderzoeken hoe isolatie in Firebird wordt geïmplementeerd, zult u zien dat het starten van zeer lange transacties met het snapshot-isolatieniveau leidt tot overmatige recordversies en lagere prestaties.
Read Committed-isolatieniveau
Een transactie met het read committed-isolatieniveau kan de vastgelegde gegevens zien van andere transacties die worden vastgelegd terwijl deze actief is (in tegenstelling tot het snapshot-niveau, waarbij u alleen gegevens kunt zien die vóór het moment van starten van de transactie zijn vastgelegd).
Laten we laten zien hoe het read committed-isolatieniveau werkt met behulp van het volgende diagram:

Het toont een voorbeeld dat praktisch identiek is aan het vorige: twee gelijktijdige transacties waarvan de ene regelmatig gegevens uit tabel T1 leest, terwijl de tweede gegevens invoegt en vastlegt.
In tegenstelling tot het geval met het snapshot-isolatieniveau, ziet transactie #10 in dit voorbeeld de gegevens die door transactie #15 zijn ingevoegd en vastgelegd.
Dit voorbeeld geeft ons een idee van de impact van het read committed-isolatieniveau: verklaringen binnen een transactie met dit isolatieniveau kunnen gegevens zien die zijn vastgelegd vóór het moment dat de overeenkomstige verklaring wordt uitgevoerd.
Het volgende diagram toont een voorbeeld waarin twee gelijktijdige transacties #11 en #18 gegevens wijzigen.
Merk op dat transactie #11 start vóór de start van transactie #14 die gegevens leest, terwijl transactie #18 erna start, maar dit heeft geen invloed op het resultaat: als de gegevens zijn vastgelegd, kunnen ze worden gezien door de gelijktijdige transactie met het read committed-isolatieniveau.

Deze mogelijkheid maakt het read committed-isolatieniveau een natuurlijke keuze voor die SQL-verklaringen die regelmatig worden uitgevoerd om de nieuwste databasestatus te tonen (bijvoorbeeld om de nieuwste orders te tonen).
Het deel dat aan garbage collection is gewijd, zal laten zien dat read committed-transacties met de read-only-modifier in Firebird tot versie 4 de beste keuze zijn voor “oneindige” leestransacties, omdat ze worden gestart als vooraf vastgelegd.
Snapshot table stability-isolatieniveau
Het is mogelijk om het verhaal over de snapshot table stability-isolatiemodus, die een tegenhanger is van de standaard Serializable-isolatiemodus, heel kort of vrij lang en gedetailleerd te maken.
De korte versie van het verhaal is als volgt: dit niveau is volledig vergelijkbaar met het snapshot-niveau, met als extra het vergrendelen van de tabel (de tabel moet expliciet worden opgegeven in de transactieparameters) voor schrijven en lezen. Dit betekent dat het mogelijk is om een transactie te starten die de opgegeven tabel volledig in beslag neemt en dat andere transacties toegangsfouten krijgen.
Met andere woorden, een transactie met het snapshot table stability-isolatieniveau plaatst feitelijk alle query’s op de opgegeven tabel in een wachtrij. In feite worden alleen leesbewerkingen in reguliere transacties buiten hun beurt uitgevoerd (zoals gebruikelijk), terwijl alle andere modi een wachtrij vormen (dit hangt natuurlijk af van de interactie).
Als dit zonder voorzichtigheid wordt geïmplementeerd, kan het vergrendelingen en onmogelijkheid om met de database te werken veroorzaken, daarom kunnen Firebird-databasetoepassingsontwikkelaars bang zijn om dit isolatieniveau te gebruiken.
Als het echter correct wordt geïmplementeerd, maakt het Serializable-isolatieniveau het mogelijk om gemakkelijk wachtrijen te vormen en sequentiële wijzigingen in databaserecords aan te brengen, wat zeer nuttig kan zijn voor het implementeren van tellers, sequentiële documentnummers en andere dergelijke objecten.
Om correct te beschrijven hoe een wachtrij kan worden gevormd met behulp van een transactie met het snapshot table stability-isolatieniveau, moeten we naar nog een transactieparameter kijken: wait/nowait - en dan terugkeren naar het wachtrijvoorbeeld.
Eerder hebben we een dergelijke interactiewijze tussen transacties onderzocht waarbij gegevens binnen de ene transactie worden gewijzigd en binnen een andere transactie worden gelezen.
Het komt echter vaak in de praktijk voor dat verschillende transacties proberen dezelfde gegevens te wijzigen en aangezien slechts één resultaat in de database wordt opgeslagen, krijgt de gelijktijdige transactie een conflictmelding - in feite een uitzondering die de uitvoering van die specifieke verklaring die probeert de reeds gewijzigde gegevens te wijzigen, onderbreekt (en annuleert).
De wait-optie definieert hoe een transactie moet reageren op het updateconflict. Er zijn drie manieren om deze optie te configureren:
- Wait (geen parameters) = wachten tot de gelijktijdige transactie eindigt
- Wait Timeout N sec = wachten tot de gelijktijdige transactie eindigt, maar niet langer dan N seconden
- Nowait - niet wachten tot de gelijktijdige transactie eindigt
Merk op dat de wait-optie hier in pseudocode wordt gespecificeerd, terwijl de namen kunnen verschillen in de API en in de specifieke componenten, hoewel de betekenis hetzelfde blijft.
Laten we in detail bekijken wat er gebeurt bij updateconflicten met verschillende varianten van de wait-optie.
Wait
Laten we ons dus twee gelijktijdig actieve transacties (#11 en #14) voorstellen waarbinnen de UPDATE-verklaring wordt uitgevoerd die één en hetzelfde record in één en dezelfde tabel T1 moet wijzigen.
Transactie #14 wordt uitgevoerd met de wait-optie (als u isql gebruikt om de voorbeelden te reproduceren, is wait standaard ingesteld).
De UPDATE-verklaring in transactie #11 start op moment t3 en eindigt op moment t5, maar de transactie is nog niet vastgelegd - d.w.z. de COMMIT-verklaring is er pas op moment t6.
Het diagram hieronder toont deze situatie:

De UPDATE-verklaring wordt ook uitgevoerd in transactie #14 en probeert hetzelfde record in dezelfde tabel bij te werken, maar start later - ongeveer op moment t4.
Aangezien er een updateconflict is met de update van transactie #11 en wait is gespecificeerd in transactie #14, zal de UPDATE-verklaring wachten tot de conflicterende transactie #11 eindigt.
Als transactie #11 lang genoeg duurt, zal de UPDATE-verklaring in transactie #14 bevroren lijken vanuit het standpunt van de gebruiker die de uitvoering van deze verklaring bekijkt.
Als u deze situatie reproduceert met behulp van twee isql.exe, toont de volgende afbeelding het moment waarop de tweede transactie (om precies te zijn de transactie waarin de gelijktijdige UPDATE-verklaring later start - het is transactie #14 in ons voorbeeld) wacht tot de eerste transactie eindigt (het is transactie #11 in ons voorbeeld).

Nadat de COMMIT-verklaring in transactie #11 is uitgevoerd, wordt transactie #14 die erop wacht onmiddellijk op de hoogte gesteld en eindigt de conflicterende update met een uitzondering.
Hieronder ziet u een voorbeeld van een dergelijk foutbericht (het nummer van de gelijktijdige transactie komt niet overeen met ons voorbeeld omdat de transactienummers in elke database vanaf het begin starten en vervolgens alleen toenemen, terwijl ze alleen worden gereset na backup/restore):
SQL> update T1 set i1 = 2 where i1=1;
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 19
SQL>
Let op het woord “deadlock” in het foutbericht - er is nu eigenlijk geen deadlock volgens de klassieke definitie. In plaats daarvan is er een updateconflict, maar Firebird-ontwikkelaars veranderen het foutbericht niet omdat het al meer dan 35 jaar wordt gebruikt. We zullen later het echte “klassieke” deadlock behandelen.
We hebben dus de situatie onderzocht waarin de transactie met de gelijktijdige UPDATE-verklaring eindigt met de COMMIT-verklaring. Laten we nu een vergelijkbare situatie bekijken, maar waarbij deze wordt teruggedraaid - u kunt dit zien op het diagram hieronder:

De situatie is volledig vergelijkbaar met de vorige - twee UPDATE-verklaringen proberen één en hetzelfde record bij te werken, maar gelijktijdige transactie #20 wordt deze keer teruggedraaid en de wijzigingen binnen transactie #15 worden als resultaat zonder fout in de database opgeslagen.
Dus de wachtoptie maakt het mogelijk om de bedrijfslogica van updates zo te organiseren dat conflicterende updates oneindig wachten in een wachtrij, hopend tot het laatste moment dat de transactie die met hen in conflict is, eindigt met de ROLLBACK-instructie.
Heeft deze tactiek altijd zin? Natuurlijk hangt het af van de implementatie van de bedrijfslogica, maar Firebird biedt ook andere opties voor het oplossen van updateconflicten met behulp van de wachtoptie.
Wachten met time-out
In de eerste plaats kan het een goed idee zijn om de wachttijd te beperken - in plaats van oneindig te wachten in geval van een conflict, kunt u de wachttijd beperken door een time-out op te geven voor de wachtoptie.
In isql.exe wordt zo’n parameter opgegeven met behulp van de volgende instructie:
SET TRANSACTION WAIT LOCK TIMEOUT N;
Waarbij N de tijd (in seconden) is die de gelijktijdige transactie zal wachten totdat het conflict is opgelost.
Meer details over transactiebeheerinstructies vindt u in de Firebird Language Reference. Houd er rekening mee dat er verschillende manieren kunnen zijn om de time-out op te geven in specifieke stuurprogramma’s of toegangscomponenten (meestal met behulp van de API-parameter).
U kunt een voorbeeld zien in isql op de onderstaande afbeelding:

Laten we met behulp van transactiediagrammen bestuderen hoe transacties op elkaar inwerken als u de time-out voor de wachtoptie opgeeft.
De situatie is dus hetzelfde - twee gelijktijdige transacties #11 en #14 waarbinnen de UPDATE-instructie wordt uitgevoerd die probeert hetzelfde record in tabel T1 bij te werken.

In dit geval wacht de instructie binnen transactie #14 echter totdat transactie #11 eindigt of totdat de opgegeven time-out (3 seconden) is verstreken - wat het eerst komt.
In dit voorbeeld verstrijkt de time-out eerder, de instructie eindigt met een uitzondering:
SQL> update T1 set i1=6 where i1=1;
Statement failed, SQLSTATE = 40001
lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 40
Merk op dat “deadlock” opnieuw in het foutbericht staat, maar het is nog steeds geen “echte” deadlock.
De situatie is dus vergelijkbaar met die van de wachtoptie, maar beperkt door de time-out - als de opgegeven time-out eerder verstrijkt dan dat de gelijktijdige transactie eindigt.
Het opgeven van de wachtoptie met time-out kan een goede oplossing zijn om bedrijfslogica te implementeren als u zeker weet dat alle schrijvende transacties vrij kort zijn (bijvoorbeeld niet langer dan 1-2 seconden).
Nowait
Het is heel gemakkelijk om vanuit formeel oogpunt uit te leggen wat Nowait is - het is wachten met een time-out van nul. Als u nowait opgeeft in transacties, zullen conflicterende updates onmiddellijk een uitzondering veroorzaken.

In dit geval hebben we opnieuw gelijktijdige transacties #11 en #14 (nowait) waarin de gelijktijdige UPDATE-instructies worden uitgevoerd. De instructie binnen de transactie met de nowait-optie wacht niet wanneer deze een gelijktijdige update ziet, maar veroorzaakt onmiddellijk op het moment van de update de volgende uitzondering (alleen het transactienummer is anders):
SQL> update T1 set i1=5 where i1=1;
Statement failed, SQLSTATE = 40001
lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 50
SQL>
Zo ziet het eruit in een voorbeeld met twee isql-tools:

Merk op dat de nowait-transactie er niet om geeft wanneer en hoe de transactie met de gelijktijdige UPDATE-instructie eindigt - of het nu de COMMIT- of ROLLBACK-instructie is, de uitzondering wordt nog steeds veroorzaakt.
Vanuit het oogpunt van bedrijfslogica kan de nowait-transactie handig zijn als u zeker weet dat de gelijktijdige update ongetwijfeld moet leiden tot het annuleren van de acties van de huidige instructie.
Veel Firebird-stuurprogramma’s gebruiken de nowait-optie als standaardwaarde en zolang veel ontwikkelaars niet weten dat het mogelijk is om een minder strikt niveau in te stellen voor het oplossen van updateconflicten (bijvoorbeeld wait lock timeout 1), lijden hun applicaties (en soms ook gebruikers) onder onnodige fouten als gevolg van conflicten.
Aangezien het trefwoord “deadlock” aanwezig is in elke uitzondering die verband houdt met updateconflicten, zijn veel applicatieontwikkelaars ervan overtuigd dat dit is wat een echte deadlock daadwerkelijk is (sommigen denken zelfs dat een zekere Dead een rol speelde in deze fout).
Tegelijkertijd, als we kijken naar het configuratiebestand firebird.conf, zien we de parameter DeadlockTimeout daar (standaard 10 seconden), en als we kijken naar de uitvoerheader van het fb_lock_print-hulpprogramma, zien we ook de parameter “Deadlock scans”.
Het punt is dat een “echte deadlock” mogelijk is in Firebird en dat het trefwoord “deadlock” dat in alle uitzonderingen verschijnt die verband houden met updateconflicten, er geen direct verband mee heeft. Gelukkig komt de echte deadlock vrij zelden voor.
Laten we eens kijken wat deze “echte deadlock” is. Om dit te doen, bekijken we het volgende diagram van transactie-interactie:

We hebben twee gelijktijdige transacties met de wachtoptie waarin de UPDATE-instructie wordt uitgevoerd. In tegenstelling tot een eenvoudig updateconflict, zien we hier een onderling afhankelijk updateconflict:
- Transactie #11 werkt het record met de sleutel = 20 bij, en transactie #12 werkt het record met de sleutel = 10 bij;
- Daarna werkt transactie #11 het record met de sleutel = 10 bij, en transactie #12 werkt het record met de sleutel = 20 bij;
Als gevolg hiervan hebben we een situatie waarin elke transactie moet wachten tot de andere eindigt en beide mogelijk oneindig kunnen wachten omdat beide de wachtoptie hebben opgegeven. Natuurlijk kan de server dat niet toestaan, dus een van de transacties zal gedwongen worden teruggedraaid na de time-out die is opgegeven in de parameter DeadlockTimeout die standaard is ingesteld op 10 seconden.
We kunnen deze situatie reproduceren met behulp van twee isql:

Nadat de tweede transactie is gestart, doet zich een situatie van echte deadlock voor. Om dit zeker te weten, start de server een procedure genaamd Deadlock scan - deze wordt gestart met intervallen gelijk aan DeadlockTimeout die standaard 10 seconden is.
Merk op dat de client (in dit geval isql) een gewoon updateconflictbericht krijgt, maar het wordt na 10 seconden geïnitieerd, zelfs als de transactie is gestart met de wachtoptie.
Nadat de server het onderling afhankelijke slot van twee transacties detecteert, verhoogt het ook de interne deadlock-teller (u kunt dit zien in de fb_lock_print-uitvoer).
Praktisch gebruik van Snapshot Table Stability
Nu we weten hoe transacties werken met conflicterende UPDATE-instructies, kunnen we terugkeren naar het isolatieniveau Snapshot Table Stability en er een praktisch gebruik voor vinden.
Wanneer dit isolatieniveau wordt opgegeven, wordt de tabel vergrendeld voor schrijven en zelfs voor lezen.
Merk op dat als de tabel niet expliciet is opgegeven in de transactieparameters, alle tabellen die instructies binnen deze transactie benaderen, worden vergrendeld en dit gebeurt tijdens de eerste toegang tot een tabel. Het is duidelijk dat als dit isolatieniveau zonder voorzichtigheid wordt gebruikt, het gemakkelijk zal leiden tot een groot aantal updateconflicten.
De Reserving TableNN-clausule stelt u in staat om een specifieke tabel (of meerdere tabellen) op te geven die aan het begin van de transactie moet worden vergrendeld (het is ook mogelijk om de reserving-modus op te geven).
Deze geweldige functie samen met de wachtoptie stelt u in staat om een zeer effectieve sequentiële wachtrij te implementeren voor het wijzigen van een specifieke tabel.
In de praktijk ziet het er zo uit - die clients die een wachtrij naar een bepaalde tabel moeten creëren, starten de SNAPSHOT TABLE STABILITY-transactie met opgave van deze tabel en proberen vervolgens binnen deze transactie een bewerking uit te voeren en deze onmiddellijk te beëindigen.
We willen bijvoorbeeld een sequentieel oplopende teller creëren in een tabel met het enige record van het type CREATE TABLE Table1(i1 integer not null), maar we kunnen om de een of andere reden geen generator gebruiken.
De pseudocode ziet er ongeveer zo uit:
set transaction snapshot table stability reserving TABLE1 for protected write
UPDATE Table1 Set i1 = i1+1;
SELECT i1 from Table1;
COMMIT;
Als we deze code niet uitvoeren met het isolatieniveau Snapshot Table Stability (Table1), maar met een lager isolatieniveau, is het mogelijk dat een gelijktijdige UPDATE-instructie interfereert tussen het begin van de transactie en vóór de UPDATE-instructie. Als gevolg hiervan krijgen we ofwel onmiddellijk een update-uitzondering (nowait) ofwel bevriest de instructie tot het einde van de gelijktijdige transactie (wait) ofwel de time-out (wait interval) - met andere woorden, het conflict wordt op de een of andere manier op instructieniveau opgelost.
Met het isolatieniveau snapshot table stability zijn we hiertegen beschermd omdat de tabel aan het begin van de transactie is gereserveerd - deze is ofwel volledig van ons ofwel volledig niet van ons. Als we de wachtoptie opgeven voor het oplossen van conflicten, vormen de parallelle verbindingen automatisch een wachtrij zonder fouten af te handelen.

Natuurlijk kan deze aanpak alleen worden toegepast op korte transacties (zoals in ons voorbeeld).
In de praktijk wordt het isolatieniveau Snapshot table stability gebruikt om wachtrijen te vormen en complexe logica opnieuw te berekenen in de exclusieve modus (in relatief kleine tabellen of wanneer er geen andere gebruikers zijn).
Binnen de engine gebruikt Firebird het isolatieniveau Snapshot Table Stability om indexen te creëren - d.w.z. wanneer u de instructie ALTER INDEX indexname ACTIVE; uitvoert, zal Firebird de tabel waarvoor de index wordt gebouwd volledig bezetten.
Wat nu?
Dit artikel geeft slechts een inleiding tot de concepten van Firebird-transacties. Om volledig te begrijpen hoe transacties in Firebird werken, is het noodzakelijk om de multi-generationele architectuur te overwegen (recordversies en garbage collection-concepten), transactiemarkers te overwegen (Oldest Interesting, Oldest Active, Oldest Snapshot, next) en andere zaken.
Het artikel is gebaseerd op de materialen van het seminar/workshop “All About Transactions”, dat voor het eerst werd geïntroduceerd in 2013 tijdens de Firebird Tour-seminars, en op basis van de training van IBSurgeon " Firebird Transaction in details".
Contacten
[email protected] Neem gerust contact met ons op met vragen of suggesties: [email protected]