Det forrige indlæg i denne serie viste, hvordan en T-SQL-sætning kører under læse-committed snapshot-isolation (RCSI ) ser normalt et øjebliksbillede af databasens forpligtede tilstand, som den var, da sætningen startede eksekveringen. Det er en god beskrivelse af, hvordan tingene fungerer for udsagn, der læser data, men der er vigtige forskelle for sætninger, der kører under RCSI, der ændrer eksisterende rækker .
Jeg understreger ændringen af eksisterende rækker ovenfor, fordi følgende overvejelser kun gælder for UPDATE
og DELETE
operationer (og de tilsvarende handlinger af en MERGE
udmelding). For at være tydelig, INSERT
udsagn er specifikt udelukket fra den adfærd, jeg er ved at beskrive, fordi inserts ikke ændrer eksisterende data.
Opdater låse og rækkeversioner
Den første forskel er, at opdatering og sletning af sætninger ikke læser rækkeversioner under RCSI, når du søger efter kilderækkerne, der skal ændres. Opdater og slet udsagn under RCSI anskaffer i stedet opdateringslåse når du søger efter kvalificerende rækker. Brug af opdateringslåse sikrer, at søgeoperationen finder rækker, der skal ændres ved hjælp af senest forpligtede data .
Uden opdateringslåse ville søgningen være baseret på en muligvis forældet version af datasættet (forpligtede data, som de var, da dataændringserklæringen startede). Dette kan minde dig om triggereksemplet, vi så sidste gang, hvor en READCOMMITTEDLOCK
tip blev brugt til at vende tilbage fra RCSI til låseimplementeringen af læseforpligtet isolation. Det hint var påkrævet i dette eksempel for at undgå at basere en vigtig handling på forældede oplysninger. Den samme slags ræsonnement bliver brugt her. En forskel er, at READCOMMITTEDLOCK
tip anskaffer delte låse i stedet for opdateringslåse. Derudover anskaffer SQL Server automatisk opdateringslåse for at beskytte dataændringer under RCSI uden at kræve, at vi tilføjer et eksplicit hint.
At tage opdateringslåse sikrer også, at opdaterings- eller sletningserklæringen blokerer hvis den støder på en inkompatibel lås, for eksempel en eksklusiv lås, der beskytter en dataændring under flyvningen udført af en anden samtidig transaktion.
En yderligere komplikation er, at den ændrede adfærd kun gælder til den tabel, der er målet af opdateringen eller sletningen. Andre tabeller i samme slet eller opdater erklæring, inklusive yderligere referencer til måltabellen, fortsæt med at bruge rækkeversioner .
Nogle eksempler er sandsynligvis nødvendige for at gøre denne forvirrende adfærd en smule tydeligere...
Testopsætning
Følgende script sikrer, at vi alle er sat op til at bruge RCSI, opretter en simpel tabel og tilføjer to eksempelrækker til den:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, Data integer NOT NULL ); GO INSERT dbo.Test (RowID, Data) VALUES (1, 1234), (2, 2345);
Det næste trin skal køre i en separat session . Det starter en transaktion og sletter begge rækker fra testtabellen (det virker mærkeligt, men det vil alt sammen give mening om kort tid):
BEGIN TRANSACTION; DELETE dbo.Test WHERE RowID IN (1, 2);
Bemærk, at transaktionen bevidst er åben . Dette bevarer eksklusive låse på begge rækker, der slettes (sammen med de sædvanlige hensigts-eksklusive låse på den indeholdende side og selve tabellen), som forespørgslen nedenfor kan bruges til at vise:
SELECT resource_type, resource_description, resource_associated_entity_id, request_mode, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID;
Vælg-testen
Skifter tilbage til den oprindelige session , den første ting, jeg vil vise, er, at almindelige udvalgte sætninger, der bruger RCSI, stadig ser de to rækker, der slettes. Den udvalgte forespørgsel nedenfor bruger rækkeversioner til at returnere de seneste forpligtede data på det tidspunkt, hvor erklæringen begynder:
SELECT * FROM dbo.Test;
Hvis det virker overraskende, skal du huske, at visning af rækkerne som slettede ville betyde, at du viser en uforpligtende visning af dataene, hvilket ikke er tilladt ved læseforpligtet isolation.
Slettesten
På trods af succesen med den udvalgte test, et forsøg på at slette de samme rækker fra den aktuelle session vil blive blokeret. Du forestiller dig måske, at denne blokering opstår, når operationen forsøger at opnå eksklusiv låse, men det er ikke tilfældet.
Sletningen bruger ikke rækkeversionering for at finde de rækker, der skal slettes; den forsøger i stedet at anskaffe opdateringslåse. Opdateringslåse er inkompatible med de eksklusive rækkelåse, der holdes af sessionen med den åbne transaktion, så forespørgslen blokerer:
DELETE dbo.Test WHERE RowID IN (1, 2);
Den estimerede forespørgselsplan for denne erklæring viser, at rækkerne, der skal slettes, identificeres af en almindelig søgeoperation, før en separat operatør udfører den faktiske sletning:
Vi kan se låsene holdt på dette trin ved at køre den samme låseforespørgsel som før (fra en anden session) og huske at ændre SPID-referencen til den, der bruges af den blokerede forespørgsel. Resultaterne ser således ud:
Vores sletteforespørgsel er blokeret hos Clustered Index Seek-operatøren, som venter på at få en opdateringslås til at læse data. Dette viser, at lokalisering af rækkerne, der skal slettes under RCSI, opnår opdateringslåse i stedet for at læse potentielt forældede versionsdata. Det viser også, at blokeringen ikke skyldes, at sletningsdelen af operationen venter på at få en eksklusiv lås.
Opdateringstesten
Annuller den blokerede forespørgsel, og prøv følgende opdatering i stedet:
UPDATE dbo.Test SET Data = Data + 1000 WHERE RowID IN (1, 2);
Den estimerede udførelsesplan ligner den, der ses i slettetesten:
Compute Scalar er der for at bestemme resultatet af at tilføje 1000 til den aktuelle værdi af datakolonnen i hver række, som læses af Clustered Index Seek. Denne erklæring vil også blokere når den udføres, på grund af opdateringslåsen, der blev anmodet om af læsehandlingen. Skærmbilledet nedenfor viser låsene, der holdes, når forespørgslen blokerer:
Som før blokeres forespørgslen ved søgningen og venter på, at den inkompatible eksklusive lås frigives, så en opdateringslås kan erhverves.
Indsæt-testen
Den næste test indeholder en erklæring, der indsætter en ny række i vores testtabel ved hjælp af datakolonnens værdi fra den eksisterende række med ID 1 i tabellen. Husk, at denne række stadig udelukkende er låst af session med den åbne transaktion:
INSERT dbo.Test (RowID, Data) SELECT 3, Data FROM dbo.Test WHERE RowID = 1;
Udførelsesplanen ligner igen de tidligere tests:
Denne gang er forespørgslen ikke blokeret . Dette viser, at opdateringslåse ikke blev erhvervet under læsning data til indsatsen. Denne forespørgsel brugte i stedet rækkeversionering til at hente datakolonnens værdi for den nyligt indsatte række. Opdateringslåse blev ikke hentet, fordi denne erklæring ikke fandt nogen rækker, der skulle ændres , den læser blot data, der skal bruges i indlægget.
Vi kan se denne nye række i tabellen ved hjælp af den valgte testforespørgsel fra før:
Bemærk, at vi er i stand til at opdatere og slette den nye række (som vil kræve opdateringslåse), fordi der ikke er nogen modstridende eksklusiv lås. Sessionen med den åbne transaktion har kun eksklusive låse på række 1 og 2:
-- Update the new row UPDATE dbo.Test SET Data = 9999 WHERE RowID = 3; -- Show the data SELECT * FROM dbo.Test; -- Delete the new row DELETE dbo.Test WHERE RowID = 3;
Denne test bekræfter, at indsæt sætninger ikke får opdateringslåse ved læsning , fordi de i modsætning til opdateringer og sletninger ikke ændrer en eksisterende række. Læsedelen af et indsæt sætningen bruger den normale RCSI-rækkeversioneringsadfærd.
Multiple reference test
Jeg nævnte før, at kun den enkelte tabelreference, der bruges til at lokalisere rækker, der skal ændres, opnår opdateringslåse; andre tabeller i samme opdaterings- eller delete-sætning læser stadig rækkeversioner. Som et særligt tilfælde af det generelle princip, en datamodifikationserklæring med flere referencer til samme tabel anvender kun opdateringslåse på en forekomst bruges til at finde rækker, der skal ændres. Denne sidste test illustrerer denne mere komplekse adfærd trin for trin.
Det første, vi skal bruge, er en ny tredje række til vores testtabel, denne gang med et nul i kolonnen Data:
INSERT dbo.Test (RowID, Data) VALUES (3, 0);
Som forventet fortsætter denne indsættelse uden blokering, hvilket resulterer i en tabel, der ser sådan ud:
Husk, at den anden session stadig er eksklusiv låser på række 1 og 2 på dette tidspunkt. Vi kan frit anskaffe låse på række 3, hvis vi har brug for det. Følgende forespørgsel er den, vi vil bruge til at vise adfærden med flere referencer til måltabellen:
-- Multi-reference update test UPDATE WriteRef SET Data = ReadRef.Data * 2 OUTPUT ReadRef.RowID, ReadRef.Data, INSERTED.RowID AS UpdatedRowID, INSERTED.Data AS NewDataValue FROM dbo.Test AS ReadRef JOIN dbo.Test AS WriteRef ON WriteRef.RowID = ReadRef.RowID + 2 WHERE ReadRef.RowID = 1;
Dette er en mere kompleks forespørgsel, men dens betjening er relativt enkel. Der er to referencer til testtabellen, den ene har jeg kaldet ReadRef, og den anden som WriteRef. Ideen er at læse fra række 1 (ved hjælp af en rækkeversion) via ReadRef og til opdatering den tredje række (som skal have en opdateringslås) ved hjælp af WriteRef.
Forespørgslen specificerer række 1 eksplicit i where-sætningen for læsetabelreferencen. Det slutter sig til skrivehenvisningen til samme tabel ved at tilføje 2 til det RowID (så at identificere række 3). Opdateringssætningen bruger også en output-klausul til at returnere et resultatsæt, der viser værdierne læst fra kildetabellen og de resulterende ændringer foretaget i række 3.
Den estimerede forespørgselsplan for denne erklæring er som følger:
Egenskaberne for søgningen mærket (1) vis, at denne søgning er på ReadRef alias, læsning af data fra rækken med RowID 1:
Denne søgeoperation finder ikke en række, der vil blive opdateret, så opdateringslåse er ikke taget; læsningen udføres ved hjælp af versionerede data. Læsningen er ikke blokeret af de eksklusive låse, som den anden session har.
Beregningsskalaren mærket (2) definerer et udtryk mærket 1004, der beregner den opdaterede datakolonneværdi. Udtryk 1009 beregner række-id'et, der skal opdateres (1 + 2 =række-id 3):
Den anden søgning er en reference til den samme tabel (3). Denne søgning lokaliserer rækken, der vil blive opdateret (række 3) ved hjælp af udtryk 1009:
Fordi denne søgning lokaliserer en række, der skal ændres, er en opdateringslås tages i stedet for at bruge rækkeversioner. Der er ingen modstridende eksklusiv lås på række ID 3, så låseanmodningen imødekommes med det samme.
Den endelige fremhævede operator (4) er selve opdateringshandlingen. Opdateringslåsen på række 3 er opgraderet til en eksklusiv lås på dette tidspunkt, lige før ændringen rent faktisk udføres. Denne operator returnerer også de data, der er angivet i outputsætningen af opdateringserklæringen:
Resultatet af opdateringssætningen (genereret af output-sætningen) er vist nedenfor:
Tabellens endelige tilstand er som vist nedenfor:
Vi kan bekræfte de låse, der blev taget under udførelsen ved hjælp af en Profiler-sporing:
Dette viser, at kun en enkelt opdatering række nøglelås er erhvervet. Når denne række når opdateringsoperatøren, konverteres låsen til en eksklusiv låse. I slutningen af erklæringen udløses låsen.
Du kan muligvis se fra sporingsoutputtet, at låsehashværdien for den opdateringslåste række er (98ec012aa510) i min testdatabase. Følgende forespørgsel viser, at denne låsehash faktisk er forbundet med RowID 3 i det klyngede indeks:
SELECT RowID, %%LockRes%% FROM dbo.Test;
Bemærk, at opdateringslåsene taget i disse eksempler er kortere end opdateringslåsene, hvis vi angiver en UPDLOCK
antydning. Disse interne opdateringslåse frigives i slutningen af sætningen, mens UPDLOCK
låse holdes til slutningen af transaktionen.
Dette afslutter demonstrationen af tilfælde, hvor RCSI anskaffer opdateringslåse til at læse aktuelle forpligtede data i stedet for at bruge rækkeversionering.
Delte og nøgleområdelåse under RCSI
Der er en række andre scenarier, hvor databasemotoren stadig kan erhverve låse under RCSI. Disse situationer er alle relateret til behovet for at bevare korrektheden, som ville blive truet ved at stole på potentielt forældede versionerede data.
Delte låse taget til validering af udenlandsk nøgle
For to tabeller i et ligefremt fremmednøgleforhold skal databasemotoren tage skridt til at sikre, at begrænsninger ikke overtrædes ved at stole på potentielt forældede versionslæser. Den nuværende implementering gør dette ved at skifte til låsning af læst committed når der tilgås data som led i en automatisk kontrol af fremmednøgle.
Ved at tage delte låse sikrer du, at integritetskontrollen læser de allerseneste forpligtede data (ikke en gammel version) eller blokerer på grund af en samtidig ændring under flyvningen. Skiftet til låsning af læst begået gælder kun for den særlige adgangsmetode, der bruges til at kontrollere fremmednøgledata; anden dataadgang i samme erklæring fortsætter med at bruge rækkeversioner.
Denne adfærd gælder kun for udsagn, der ændrer data, hvor ændringen direkte påvirker et fremmednøgleforhold. For ændringer af den refererede (overordnede) tabel betyder det opdateringer, der påvirker den refererede værdi (medmindre den er sat til NULL
) og alle sletninger. For referencetabellen (underordnet) betyder dette alle indsættelser og opdateringer (igen, medmindre nøglereferencen er NULL
). De samme overvejelser gælder for komponenteffekterne af en MERGE
.
Et eksempel på en eksekveringsplan, der viser en fremmednøgleopslag, der tager delte låse, er vist nedenfor:
Serialiserbar til cascading af fremmednøgler
Hvor fremmednøgleforholdet har en kaskadende handling, kræver korrekthed en lokal eskalering til serialiserbar isolationssemantik. Det betyder, at du vil se nøglerækkelåse taget til en kaskadende henvisningshandling. Som det var tilfældet for de tidligere set opdateringslåse, er disse nøgleområdelåse omfattet af erklæringen, ikke transaktionen. Et eksempel på en eksekveringsplan, der viser, hvor de interne serialiserbare låse er taget under RCSI, er vist nedenfor:
Andre scenarier
Der er mange andre specifikke tilfælde, hvor motoren automatisk forlænger levetiden for låse, eller lokalt eskalerer isolationsniveauet for at sikre korrekthed. Disse inkluderer den serialiserbare semantik, der bruges, når du vedligeholder en relateret indekseret visning, eller når du vedligeholder et indeks, der har IGNORE_DUP_KEY
indstilling indstillet.
Takeaway-meddelelsen er, at RCSI reducerer mængden af låsning, men ikke altid kan eliminere den helt.
Næste gang
Det næste indlæg i denne serie ser på snapshot-isolationsniveauet.
[ Se indekset for hele serien ]