SQL Server biedt twee fysieke implementaties van de read commit isolatieniveau gedefinieerd door de SQL-standaard, vergrendeling van read-commit en read-committ snapshot-isolatie (RCSI ). Hoewel beide implementaties voldoen aan de vereisten die zijn vastgelegd in de SQL-standaard voor leescommitted isolatiegedrag, heeft RCSI heel ander fysiek gedrag dan de vergrendelingsimplementatie die we in het vorige bericht in deze serie hebben bekeken.
Logische garanties
De SQL-standaard vereist dat een transactie die op het leescommitt-isolatieniveau werkt, geen vuile leesbewerkingen ondervindt. Een andere manier om deze vereiste uit te drukken is om te zeggen dat een read-committe transactie alleen gecommitteerde gegevens mag tegenkomen .
De standaard zegt ook dat read-committed transacties mogelijk ervaar de gelijktijdigheidsverschijnselen die bekend staan als niet-herhaalbare reads en fantomen (hoewel ze dat niet echt nodig hebben). Het is namelijk zo dat beide fysieke implementaties van readcommitt isolation in SQL Server niet-herhaalbare reads en phantom rows kunnen ervaren, hoewel de precieze details nogal verschillen.
Een momentopname van vastgelegde gegevens
Als de database-optie READ_COMMITTED_SNAPSHOT
in ON
, SQL Server maakt gebruik van een rijversiebeheer-implementatie van het leescommittatie-isolatieniveau. Wanneer dit is ingeschakeld, gebruiken transacties die readcommit-isolatie aanvragen automatisch de RCSI-implementatie; er zijn geen wijzigingen in de bestaande T-SQL-code vereist om RCSI te gebruiken. Houd er echter rekening mee dat dit niet hetzelfde is door te zeggen dat code zich hetzelfde zal gedragen onder RCSI zoals bij het gebruik van de vergrendelingsimplementatie van read commit, in feite is dit vrij algemeen niet het geval .
Er is niets in de SQL-standaard dat vereist dat de gegevens die door een read-committe transactie worden gelezen, de meest recente zijn geëngageerde gegevens. De SQL Server RCSI-implementatie maakt hiervan gebruik om transacties te voorzien van een point-in-time view van vastgelegde gegevens, waarbij dat tijdstip het moment is waarop de huidige verklaring begon uitvoering (niet op het moment dat een transactie is gestart).
Dit is heel anders dan het gedrag van de SQL Server-vergrendelingsimplementatie van read commit, waarbij de instructie de meest recent vastgelegde gegevens ziet vanaf het moment dat elk item fysiek wordt gelezen . Vergrendelen lees vastgelegd geeft gedeelde vergrendelingen zo snel mogelijk vrij, dus de set gegevens die wordt aangetroffen, kan van heel verschillende tijdstippen komen.
Om samen te vatten, ziet bij het vergrendelen van leestoezegging elke rij zoals het was op het moment dat het kort was vergrendeld en fysiek werd gelezen; RCSI ziet alle rijen zoals ze waren op het moment dat de verklaring begon. Beide implementaties zullen gegarandeerd nooit niet-vastgelegde gegevens zien, maar de gegevens die ze tegenkomen kunnen heel verschillend zijn.
De implicaties van een point-in-time weergave
Het zien van een momentopname van vastgelegde gegevens lijkt vanzelfsprekend superieur aan het meer complexe gedrag van de vergrendelingsimplementatie. Het is bijvoorbeeld duidelijk dat een point-in-time view geen last kan hebben van de problemen van ontbrekende rijen of de dezelfde rij meerdere keren tegenkomen , die beide mogelijk zijn onder locking read commit isolation.
Een tweede belangrijk voordeel van RCSI is dat het geen gedeelde sloten verwerft bij het lezen van gegevens, omdat de gegevens afkomstig zijn uit de rijversieopslag in plaats van rechtstreeks toegankelijk te zijn. Het ontbreken van gedeelde vergrendelingen kan de gelijktijdigheid drastisch verbeteren door conflicten met gelijktijdige transacties op zoek naar incompatibele sloten te elimineren. Dit voordeel wordt gewoonlijk samengevat door te zeggen dat lezers geen schrijvers blokkeren onder RCSI, en vice versa. Als een ander gevolg van het verminderen van blokkering vanwege incompatibele vergrendelingsverzoeken, is de kans op deadlocks wordt meestal sterk verminderd bij gebruik onder RCSI.
Deze voordelen zijn echter niet zonder kosten en voorbehouden . Om te beginnen verbruikt het onderhouden van versies van vastgelegde rijen systeembronnen, dus het is belangrijk dat de fysieke omgeving is geconfigureerd om hiermee om te gaan, voornamelijk in termen van tempdb prestatie- en geheugen-/schijfruimtevereisten.
Het tweede voorbehoud is iets subtieler:RCSI biedt een momentopname van vastgelegde gegevens zoals het was aan het begin van de instructie, maar er is niets dat verhindert dat de echte gegevens worden gewijzigd (en die wijzigingen worden vastgelegd) terwijl de RCSI-instructie wordt uitgevoerd. Er zijn geen gedeelde sloten, onthoud. Een direct gevolg van dit tweede punt is dat T-SQL-code die onder RCSI draait, beslissingen kan nemen op basis van verouderde informatie , in vergelijking met de huidige vastgelegde status van de database. We zullen hier binnenkort meer over vertellen.
Er is nog een laatste (uitvoeringsspecifieke) opmerking die ik wil maken over RCSI voordat we verder gaan. Scalaire en multi-statement functies uitvoeren met een andere interne T-SQL-context dan de bevattende instructie. Dit betekent dat de point-in-time view die wordt gezien in een scalaire of multi-statement functieaanroep later kan zijn dan de point-in-time view die wordt gezien door de rest van de instructie. Dit kan leiden tot onverwachte inconsistenties, aangezien verschillende delen van dezelfde verklaring gegevens van verschillende tijdstippen zien . Dit rare en verwarrende gedrag doet niet zijn van toepassing op in-line functies, die dezelfde momentopname zien als het statement waarin ze verschijnen.
Niet-herhaalbare reads en fantomen
Gezien een point-in-time weergave op statement-niveau van de gecommitteerde status van de database, is het misschien niet meteen duidelijk hoe een read-commit-transactie onder RCSI de niet-herhaalbare read- of phantom row-fenomenen zou kunnen ervaren. Inderdaad, als we ons denken beperken tot de reikwijdte van een enkele verklaring , geen van deze verschijnselen is mogelijk onder RCSI.
Dezelfde gegevens meerdere keren lezen binnen de dezelfde instructie onder RCSI zal altijd dezelfde gegevenswaarden retourneren, er zullen geen gegevens verdwijnen tussen die uitlezingen en er zullen ook geen nieuwe gegevens verschijnen. Als u zich afvraagt wat voor soort instructie dezelfde gegevens meer dan eens kan lezen, denk dan eens aan zoekopdrachten die meer dan eens naar dezelfde tabel verwijzen, misschien in een subquery.
Leesconsistentie op statementniveau is een voor de hand liggend gevolg van het feit dat de leesbewerkingen worden uitgevoerd tegen een vast momentopname van de gegevens. De reden dat RCSI niet doet bescherming bieden tegen niet-herhaalbare reads en phantoms is dat deze SQL-standaardfenomenen op transactieniveau worden gedefinieerd. Meerdere overzichten binnen een transactie die bij RCSI wordt uitgevoerd, kunnen verschillende gegevens zien, omdat elk overzicht een point-in-time weergave ziet vanaf het moment dat die specifieke verklaring begonnen.
Om samen te vatten:elke uitspraak binnen een RCSI-transactie ziet een statische vastgelegde dataset, maar die set kan veranderen tussen instructies binnen dezelfde transactie.
Verouderde gegevens
De mogelijkheid dat onze T-SQL-code een belangrijke beslissing neemt op basis van verouderde informatie is meer dan een beetje verontrustend. Bedenk even dat de momentopname die wordt gebruikt door een enkele instructie die onder RCSI wordt uitgevoerd, willekeurig oud kan zijn .
Een instructie die gedurende een aanzienlijke periode wordt uitgevoerd, blijft de gecommitteerde status van de database zien zoals deze was toen de instructie begon. Ondertussen mist de instructie alle vastgelegde wijzigingen die sinds die tijd in de database zijn opgetreden.
Dit wil niet zeggen dat problemen in verband met toegang tot verouderde gegevens onder RCSI beperkt zijn tot langlopende verklaringen, maar de problemen kunnen in dergelijke gevallen zeker meer uitgesproken zijn.
Een kwestie van timing
Dit probleem van verouderde gegevens is in principe van toepassing op alle RCSI-statements, hoe snel ze ook kunnen worden voltooid. Hoe klein het tijdvenster ook is, er is altijd een kans dat een gelijktijdige bewerking de dataset waarmee we werken wijzigt, zonder dat we ons daarvan bewust zijn. Laten we nog eens kijken naar een van de eenvoudige voorbeelden die we eerder gebruikten bij het onderzoeken van het gedrag van locking read commits:
INSERT dbo.OverdueInvoices SELECT I.InvoiceNumber FROM dbo.Invoices AS I WHERE I.TotalDue > ( SELECT SUM(P.Amount) FROM dbo.Payments AS P WHERE P.InvoiceNumber = I.InvoiceNumber );
Wanneer uitgevoerd onder RCSI, kan deze instructie kan niet zie alle doorgevoerde databasewijzigingen die plaatsvinden nadat de instructie wordt uitgevoerd. Hoewel we de problemen van gemiste of meervoudig aangetroffen rijen die mogelijk zijn onder de vergrendelingsimplementatie niet zullen tegenkomen, kan een gelijktijdige transactie een betaling toevoegen die zou om te voorkomen dat een klant een strenge waarschuwingsbrief ontvangt over een achterstallige betaling nadat de bovenstaande verklaring is uitgevoerd.
U kunt waarschijnlijk veel andere mogelijke problemen bedenken die zich in dit scenario kunnen voordoen, of in andere die conceptueel vergelijkbaar zijn. Hoe langer de verklaring duurt, hoe verouderder de weergave van de database wordt en hoe groter de kans op mogelijk onbedoelde gevolgen.
Natuurlijk zijn er tal van verzachtende factoren in dit specifieke voorbeeld. Het gedrag kan heel goed als volkomen acceptabel worden beschouwd. Een herinneringsbrief sturen omdat een betaling enkele seconden te laat binnenkwam, is immers een makkelijk verdedigbare actie. Het principe blijft echter.
Mislukte zakelijke regels en integriteitsrisico's
Er kunnen ernstigere problemen ontstaan door het gebruik van verouderde informatie dan een paar seconden eerder een waarschuwingsbrief te sturen. Een goed voorbeeld van deze klasse van zwakte is te zien met triggercode gebruikt om een integriteitsregel af te dwingen die misschien te complex is om af te dwingen met declaratieve referentiële integriteitsbeperkingen. Bekijk ter illustratie de volgende code, die een trigger gebruikt om een variatie van een externe-sleutelbeperking af te dwingen, maar een die de relatie alleen voor bepaalde onderliggende tabelrijen afdwingt:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Parent (ParentID integer PRIMARY KEY); GO CREATE TABLE dbo.Child ( ChildID integer IDENTITY PRIMARY KEY, ParentID integer NOT NULL, CheckMe bit NOT NULL ); GO CREATE TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END; GO -- Insert parent row #1 INSERT dbo.Parent (ParentID) VALUES (1);
Overweeg nu een transactie die in een andere sessie wordt uitgevoerd (gebruik hiervoor een ander SSMS-venster als u meevolgt) die bovenliggende rij #1 verwijdert, maar nog niet vastlegt:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION; DELETE FROM dbo.Parent WHERE ParentID = 1;
Terug in onze oorspronkelijke sessie proberen we een (aangevinkte) onderliggende rij in te voegen die verwijst naar deze ouder:
INSERT dbo.Child (ParentID, CheckMe) VALUES (1, 1);
De triggercode wordt uitgevoerd, maar omdat RCSI alleen toegewezen . ziet gegevens vanaf het moment dat de instructie begon, ziet het nog steeds de bovenliggende rij (niet de niet-vastgelegde verwijdering) en de insert slaagt !
De transactie die de bovenliggende rij heeft verwijderd, kan de wijziging nu met succes vastleggen, waardoor de database in een inconsistente blijft. staat in termen van onze triggerlogica:
COMMIT TRANSACTION; SELECT P.* FROM dbo.Parent AS P; SELECT C.* FROM dbo.Child AS C;
Dit is natuurlijk een vereenvoudigd voorbeeld, en een voorbeeld dat gemakkelijk kan worden omzeild met behulp van de ingebouwde beperkingsfaciliteiten. Veel complexere bedrijfsregels en pseudo-integriteitsbeperkingen kunnen binnen en buiten triggers worden geschreven . Het potentieel voor onjuist gedrag onder RCSI moet duidelijk zijn.
Blokkeringsgedrag en laatst vastgelegde gegevens
Ik zei eerder dat T-SQL-code zich niet gegarandeerd op dezelfde manier gedraagt onder RCSI read-commit als bij het gebruik van de vergrendelingsimplementatie. Het voorgaande voorbeeld van triggercode is daar een goede illustratie van, maar ik moet benadrukken dat het algemene probleem niet beperkt is tot triggers .
RCSI is meestal geen goede keuze voor T-SQL-code waarvan de juistheid afhangt van blokkering als er een gelijktijdige niet-vastgelegde wijziging bestaat. RCSI is misschien ook niet de juiste keuze als de code afhankelijk is van het lezen van huidig vastgelegde gegevens, in plaats van de laatste vastgelegde gegevens op het moment dat de instructie begon. Deze twee overwegingen zijn gerelateerd, maar ze zijn niet hetzelfde.
Vergrendelen lezen vastgelegd onder RCSI
SQL Server biedt een manier om vergrendeling aan te vragen lees vastgelegd wanneer RCSI is ingeschakeld, met behulp van de tabelhint READCOMMITTEDLOCK
. We kunnen onze trigger aanpassen om de hierboven getoonde problemen te voorkomen door deze hint toe te voegen aan de tabel die blokkeergedrag nodig heeft om correct te presteren:
ALTER TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P WITH (READCOMMITTEDLOCK) -- NEW!! ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END;
Met deze wijziging is de poging om de mogelijk verweesde onderliggende rijblokken in te voegen totdat de verwijderingstransactie wordt vastgelegd (of afgebroken). Als het verwijderen wordt uitgevoerd, detecteert de triggercode de integriteitsschending en wordt de verwachte fout weergegeven.
Zoekopdrachten identificeren die mogelijk niet correct worden uitgevoerd onder RCSI is een niet-triviale taak waarvoor mogelijk uitgebreide tests nodig zijn om het goed te doen (en onthoud dat deze problemen vrij algemeen zijn en niet beperkt tot triggercode!) Voeg ook de READCOMMITTEDLOCK
toe hint naar elke tafel die het nodig heeft, kan een vervelend en foutgevoelig proces zijn. Totdat SQL Server een bredere optie biedt om waar nodig de vergrendelingsimplementatie aan te vragen, moeten we de tabelhints gebruiken.
Volgende keer
Het volgende bericht in deze serie vervolgt ons onderzoek naar de isolatie van gelezen vastgelegde snapshots, met een blik op het verrassende gedrag van instructies voor gegevenswijziging onder RCSI.
[ Zie de index voor de hele serie ]