SQL Server leverer to fysiske implementeringer af read committed isolationsniveau defineret af SQL-standarden, låser læst committet og read committed snapshot isolation (RCSI ). Selvom begge implementeringer opfylder kravene i SQL-standarden for læse-committed isolationsadfærd, har RCSI en helt anden fysisk adfærd end den låseimplementering, vi så på i det forrige indlæg i denne serie.
Logiske garantier
SQL-standarden kræver, at en transaktion, der opererer på det læseforpligtede isolationsniveau, ikke oplever nogen beskidte læsninger. En anden måde at udtrykke dette krav på er at sige, at en læst forpligtet transaktion kun må støde på forpligtede data .
Standarden siger også, at læste forpligtede transaktioner kan opleve samtidighedsfænomenerne kendt som ikke-gentagelige læsninger og fantomer (selvom de faktisk ikke er forpligtet til at gøre det). Som det sker, kan begge fysiske implementeringer af læseforpligtet isolation i SQL Server opleve ikke-gentagelige læsninger og fantomrækker, selvom de præcise detaljer er ret forskellige.
Et punkt-i-tidsbillede af forpligtede data
Hvis databaseindstillingen READ_COMMITTED_SNAPSHOT
i ON
, SQL Server bruger en rækkeversionsimplementering af det læseforpligtede isolationsniveau. Når dette er aktiveret, bruger transaktioner, der anmoder om læst committed isolation, automatisk RCSI-implementeringen; der kræves ingen ændringer til eksisterende T-SQL-kode for at bruge RCSI. Bemærk dog omhyggeligt, at dette ikke er det samme som at sige, at koden vil opføre sig på samme måde under RCSI, som når du bruger låseimplementeringen af read committed, er dette faktisk ret generelt ikke tilfældet .
Der er intet i SQL-standarden, der kræver, at data læst af en læst forpligtet transaktion er den senest forpligtede data. SQL Server RCSI-implementeringen udnytter dette til at give transaktioner en punkt-i-tidsvisning af forpligtede data, hvor det tidspunkt er det øjeblik, hvor den aktuelle erklæring begyndte eksekvering (ikke det øjeblik, en hvilken som helst indeholdende transaktion startede).
Dette er helt anderledes end adfærden for SQL Server-låseimplementeringen af read committed, hvor sætningen ser de senest begåede data fra det øjeblik, hvert element fysisk læses . Låsning af læseforpligtede udgivelser frigiver delte låse så hurtigt som muligt, så det sæt af data, der stødes på, kan komme fra meget forskellige tidspunkter.
For at opsummere, låsning af læst committed ser hver række som den var på det tidspunkt, blev den kortvarigt låst og fysisk læst; RCSI ser alle rækker som de var på det tidspunkt, hvor redegørelsen begyndte. Begge implementeringer vil med garanti aldrig se uforpligtende data, men de data, de støder på, kan være meget forskellige.
Konsekvenserne af et punkt-i-tidssyn
At se et punkt-i-tidsbillede af forpligtede data kan virke indlysende overlegent i forhold til låseimplementeringens mere komplekse adfærd. Det er f.eks. klart, at et punkt-i-tidssyn ikke kan lide under problemerne med manglende rækker eller støder på den samme række flere gange , som begge er mulige under låsning af læst committed isolation.
En anden vigtig fordel ved RCSI er, at den ikke erhverver delte låse ved læsning af data, fordi dataene kommer fra rækkeversionslageret i stedet for at blive tilgået direkte. Manglen på delte låse kan dramatisk forbedre samtidighed ved at eliminere konflikter med samtidige transaktioner, der ønsker at anskaffe inkompatible låse. Denne fordel opsummeres almindeligvis ved at sige, at læsere ikke blokerer forfattere under RCSI, og omvendt. Som en yderligere konsekvens af at reducere blokering på grund af inkompatible låseanmodninger, er muligheden for deadlocks er normalt meget reduceret, når du kører under RCSI.
Disse fordele kommer dog ikke uden omkostninger og forbehold . For det første bruger vedligeholdelse af versioner af forpligtede rækker systemressourcer, så det er vigtigt, at det fysiske miljø er konfigureret til at klare dette, primært i forhold til tempdb krav til ydeevne og hukommelse/diskplads.
Den anden advarsel er lidt mere subtil:RCSI giver et øjebliksbillede af forpligtede data som det var i starten af sætningen, men der er intet til hinder for, at de reelle data bliver ændret (og disse ændringer begås), mens RCSI-sætningen udføres. Der er ingen fælles låse, husk. En umiddelbar konsekvens af dette andet punkt er, at T-SQL-kode, der kører under RCSI, kan tage beslutninger baseret på forældede oplysninger , sammenlignet med den aktuelle forpligtede tilstand af databasen. Vi vil tale mere om dette snart.
Der er en sidste (implementeringsspecifik) observation, jeg vil gøre om RCSI, før vi går videre. Skalære og multi-sætningsfunktioner udføres ved hjælp af en anden intern T-SQL-kontekst end den indeholdende sætning. Dette betyder, at point-in-time-visningen, der ses inde i en skalar- eller multi-sætningsfunktion, kan være senere end point-in-time-visningen, der ses af resten af sætningen. Dette kan resultere i uventede uoverensstemmelser, da forskellige dele af den samme erklæring ser data fra forskellige tidspunkter . Denne mærkelige og forvirrende adfærd gør ikke gælder for in-line funktioner, som ser det samme øjebliksbillede som det udsagn, de vises i.
Ikke-gentagelige læsninger og fantomer
Givet et punkt-i-tidsbillede på erklæringsniveau af databasens forpligtede tilstand, er det muligvis ikke umiddelbart indlysende, hvordan en læst forpligtet transaktion under RCSI kan opleve de ikke-gentagelige læse- eller fantomrække-fænomener. Faktisk, hvis vi begrænser vores tankegang til omfanget af et enkelt udsagn , ingen af disse fænomener er mulige under RCSI.
Læsning af de samme data flere gange inden for samme erklæring under RCSI vil altid returnere de samme dataværdier, ingen data forsvinder mellem disse aflæsninger, og der vil heller ikke dukke nye data op. Hvis du undrer dig over, hvilken slags udsagn der kan læse de samme data mere end én gang, så tænk på forespørgsler, der refererer til den samme tabel mere end én gang, måske i en underforespørgsel.
Læsekonsistens på erklæringsniveau er en indlysende konsekvens af, at læsningerne udstedes mod et fast øjebliksbillede af dataene. Årsagen til at RCSI ikke gør det give beskyttelse mod ikke-gentagelige læsninger og fantomer er, at disse SQL-standardfænomener er defineret på transaktionsniveau. Flere erklæringer inden for en transaktion, der kører på RCSI, kan se forskellige data, fordi hver erklæring ser et tidspunkt på det tidspunkt den pågældende erklæring startede.
For at opsummere, hver erklæring inden for en RCSI-transaktion ser et statisk forpligtet datasæt, men det sæt kan skifte mellem udsagn i den samme transaktion.
Forældede data
Muligheden for, at vores T-SQL-kode træffer en vigtig beslutning baseret på forældede oplysninger, er mere end en lille smule foruroligende. Overvej et øjeblik, at det øjebliksbillede, der bruges af en enkelt sætning, der kører under RCSI, kan være vilkårligt gammelt .
En erklæring, der kører i en længere periode, vil fortsætte med at se databasens forpligtede tilstand, som den var, da erklæringen begyndte. I mellemtiden mangler erklæringen alle de forpligtede ændringer, der er sket i databasen siden det tidspunkt.
Dette betyder ikke, at problemer forbundet med at få adgang til forældede data under RCSI er begrænset til langvarige udsagn, men problemerne kan helt sikkert være mere udtalte i sådanne tilfælde.
Et spørgsmål om timing
Denne udgave af forældede data gælder i princippet for alle RCSI-erklæringer, uanset hvor hurtigt de udfyldes. Hvor lille tidsvinduet end er, er der altid en chance for, at en samtidig operation kan ændre det datasæt, vi arbejder med, uden at vi er klar over den ændring. Lad os se igen på et af de simple eksempler, vi brugte før, da vi udforskede adfærden ved at låse læst committed:
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 );
Når den køres under RCSI, kan denne sætning ikke se eventuelle forpligtede databaseændringer, der opstår, efter at sætningen begynder at udføre. Selvom vi ikke vil støde på problemerne med ubesvarede eller mange gange stødte rækker, som er mulige under låseimplementeringen, kan en samtidig transaktion tilføje en betaling, der burde for at forhindre en kunde i at få tilsendt et strengt advarselsbrev om en forsinket betaling, efter at ovenstående erklæring begynder at udføre.
Du kan sikkert tænke på mange andre potentielle problemer, der kan opstå i dette scenarie, eller i andre, der er konceptuelt ens. Jo længere erklæringen løber, jo mere forældet bliver dens syn på databasen, og jo større er muligheden for mulige utilsigtede konsekvenser.
Selvfølgelig er der masser af formildende faktorer i dette specifikke eksempel. Adfærden kan meget vel ses som helt acceptabel. At sende en rykkerskrivelse, fordi en betaling kom et par sekunder for sent, er jo en let forsvarlig handling. Princippet består dog.
Fejl i forretningsregler og integritetsrisici
Der kan opstå mere alvorlige problemer ved brugen af forældede oplysninger end at sende et advarselsbrev et par sekunder før tid. Et godt eksempel på denne svaghedsklasse kan ses med trigger-kode bruges til at håndhæve en integritetsregel, der måske er for kompleks til at håndhæve med deklarative referentielle integritetsbegrænsninger. For at illustrere kan du overveje følgende kode, som bruger en trigger til at gennemtvinge en variation af en fremmednøglebegrænsning, men en, der kun håndhæver relationen for visse underordnede tabelrækker:
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);
Overvej nu en transaktion, der kører i en anden session (brug et andet SSMS-vindue til dette, hvis du følger med), som sletter overordnet række #1, men som ikke binder endnu:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION; DELETE FROM dbo.Parent WHERE ParentID = 1;
Tilbage i vores oprindelige session forsøger vi at indsætte en (afkrydset) underordnet række, der refererer til denne forælder:
INSERT dbo.Child (ParentID, CheckMe) VALUES (1, 1);
Udløserkoden udføres, men fordi RCSI kun ser committed data på det tidspunkt, hvor erklæringen startede, ser den stadig den overordnede række (ikke den uforpligtede sletning), og indsættelsen lykkes !
Transaktionen, der slettede den overordnede række, kan nu udføre sin ændring med succes, hvilket efterlader databasen i en inkonsekvent stat i forhold til vores triggerlogik:
COMMIT TRANSACTION; SELECT P.* FROM dbo.Parent AS P; SELECT C.* FROM dbo.Child AS C;
Dette er selvfølgelig et forenklet eksempel, og et som nemt kunne omgås ved hjælp af de indbyggede begrænsningsfaciliteter. Meget mere komplekse forretningsregler og pseudointegritetsbegrænsninger kan skrives inden for og uden for triggere . Potentialet for ukorrekt adfærd under RCSI burde være indlysende.
Blokeringsadfærd og seneste begåede data
Jeg nævnte tidligere, at T-SQL-kode ikke er garanteret at opføre sig på samme måde under RCSI read committed, som den gjorde ved brug af låseimplementeringen. Det foregående triggerkodeeksempel er en god illustration af det, men jeg skal understrege, at det generelle problem ikke er begrænset til triggere .
RCSI er typisk ikke et godt valg for nogen T-SQL-kode, hvis korrekthed afhænger af blokering, hvis der eksisterer en samtidig uforpligtet ændring. RCSI er muligvis heller ikke det rigtige valg, hvis koden afhænger af at læse aktuel forpligtede data, snarere end de seneste forpligtede data på det tidspunkt, hvor erklæringen startede. Disse to overvejelser hænger sammen, men de er ikke det samme.
Låsning af læsning begået under RCSI
SQL Server giver én måde at anmode om låsning read committed, når RCSI er aktiveret ved hjælp af tabeltip READCOMMITTEDLOCK
. Vi kan ændre vores trigger for at undgå de problemer, der er vist ovenfor, ved at tilføje dette tip til tabellen, der kræver blokeringsadfærd for at fungere korrekt:
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;
Med denne ændring på plads blokerer forsøget på at indsætte den potentielt forældreløse underordnede række, indtil slettetransaktionen forpligtes (eller afbrydes). Hvis sletningen forpligter, registrerer triggerkoden integritetskrænkelsen og rejser den forventede fejl.
Identifikation af forespørgsler, der måske ikke fungerer korrekt under RCSI er en ikke-triviel opgave, der kan kræve omfattende test for at komme rigtigt (og husk venligst, at disse problemer er ret generelle og ikke begrænset til triggerkode!) Tilføjelse af READCOMMITTEDLOCK
hint til hvert bord, der har brug for det, kan være en kedelig og fejltilbøjelig proces. Indtil SQL Server giver en bredere mulighed for at anmode om låseimplementering, hvor det er nødvendigt, sidder vi fast med at bruge tabeltip.
Næste gang
Det næste indlæg i denne serie fortsætter vores undersøgelse af læst engageret snapshot-isolering med et kig på den overraskende opførsel af datamodifikationsudsagn under RCSI.
[ Se indekset for hele serien ]