Det tidigare inlägget i den här serien visade hur en T-SQL-sats körs under läs-committed snapshot-isolation (RCSI ) ser normalt en ögonblicksbild av det engagerade tillståndet för databasen som det var när programsatsen började köras. Det är en bra beskrivning av hur saker och ting fungerar för påståenden som läser data, men det finns viktiga skillnader för satser som körs under RCSI som ändrar befintliga rader .
Jag betonar ändringen av befintliga rader ovan, eftersom följande överväganden endast gäller UPDATE
och DELETE
operationer (och motsvarande åtgärder för en MERGE
påstående). För att vara tydlig, INSERT
uttalanden är specifikt uteslutna från beteendet jag ska beskriva eftersom inlägg inte ändrar befintliga data.
Uppdatera lås och radversioner
Den första skillnaden är att uppdatera och ta bort uttalanden inte läser radversioner under RCSI när du söker efter källraderna att ändra. Uppdatera och ta bort uttalanden under RCSI skaffa istället uppdateringslås när du söker efter kvalificerade rader. Genom att använda uppdateringslås säkerställs att sökoperationen hittar rader att ändra med hjälp av senast registrerade data .
Utan uppdateringslås skulle sökningen baseras på en möjligen inaktuell version av datamängden (begärd data som den var när dataändringssatsen startade). Detta kan påminna dig om triggerexemplet vi såg förra gången, där en READCOMMITTEDLOCK
ledtråd användes för att återgå från RCSI till låsningsimplementeringen av läskommitterad isolering. Den antydan krävdes i det exemplet för att undvika att grunda en viktig åtgärd på inaktuell information. Samma typ av resonemang används här. En skillnad är att READCOMMITTEDLOCK
hint skaffar delade lås istället för uppdateringslås. Dessutom skaffar SQL Server automatiskt uppdateringslås för att skydda datamodifieringar under RCSI utan att vi behöver lägga till en explicit ledtråd.
Att ta uppdateringslås säkerställer också att uppdaterings- eller borttagningssatsen blockeras om den stöter på ett inkompatibelt lås, till exempel ett exklusivt lås som skyddar en datamodifiering under flygning som utförs av en annan samtidig transaktion.
En ytterligare komplikation är att det modifierade beteendet endast gäller till tabellen som är målet av uppdateringen eller raderingen. Övriga tabeller i samma ta bort eller uppdatera uttalande, inklusive ytterligare referenser till måltabellen, fortsätt att använda radversioner .
Det krävs förmodligen några exempel för att göra dessa förvirrande beteenden lite tydligare...
Testa installationen
Följande skript säkerställer att vi alla är inställda på att använda RCSI, skapar en enkel tabell och lägger till två exempelrader till 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);
Nästa steg måste köras i en separat session . Den startar en transaktion och tar bort båda raderna från testtabellen (verkar konstigt, men allt detta kommer att bli vettigt inom kort):
BEGIN TRANSACTION; DELETE dbo.Test WHERE RowID IN (1, 2);
Observera att transaktionen avsiktligt lämnas öppen . Detta bibehåller exklusiva lås på båda raderna som tas bort (tillsammans med de vanliga avsiktsexklusiva låsen på den innehållande sidan och själva tabellen) som frågan nedan kan användas för att visa:
SELECT resource_type, resource_description, resource_associated_entity_id, request_mode, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID;
Väljtestet
Byter tillbaka till den ursprungliga sessionen , det första jag vill visa är att vanliga urvalssatser som använder RCSI fortfarande ser de två raderna som tas bort. Väljfrågan nedan använder radversioner för att returnera den senaste bekräftade informationen vid den tidpunkt då uttalandet börjar:
SELECT * FROM dbo.Test;
Om det verkar överraskande, kom ihåg att om raderna visas som borttagna skulle det innebära att visa en oengagerad vy av data, vilket inte är tillåtet vid läsbeslutad isolering.
Delete Test
Trots framgången med det valda testet, ett försök att ta bort samma rader från den aktuella sessionen kommer att blockeras. Du kanske föreställer dig att denna blockering inträffar när operationen försöker få exklusiv låser, men så är inte fallet.
Borttagningen använder inte radversionering för att lokalisera raderna som ska raderas; den försöker skaffa uppdateringslås istället. Uppdateringslås är inkompatibla med de exklusiva radlåsen som innehas av sessionen med den öppna transaktionen, så frågan blockerar:
DELETE dbo.Test WHERE RowID IN (1, 2);
Den uppskattade frågeplanen för denna sats visar att raderna som ska raderas identifieras av en vanlig sökoperation innan en separat operatör utför den faktiska raderingen:
Vi kan se låsen som hålls i detta skede genom att köra samma låsningsfråga som tidigare (från en annan session) och komma ihåg att ändra SPID-referensen till den som används av den blockerade frågan. Resultaten ser ut så här:
Vår raderingsfråga är blockerad hos Clustered Index Seek-operatören, som väntar på att få ett uppdateringslås för att läsa data. Detta visar att lokalisering av raderna att radera under RCSI erhåller uppdateringslås snarare än att läsa potentiellt inaktuella versionsdata. Det visar också att blockeringen inte beror på att borttagningsdelen av operationen väntar på att få ett exklusivt lås.
Uppdateringstestet
Avbryt den blockerade frågan och försök med följande uppdatering istället:
UPDATE dbo.Test SET Data = Data + 1000 WHERE RowID IN (1, 2);
Den beräknade genomförandeplanen liknar den som sågs i raderingstestet:
Compute Scalar är till för att bestämma resultatet av att lägga till 1000 till det aktuella värdet i datakolumnen i varje rad, som läses av Clustered Index Seek. Detta uttalande kommer också att blockera när den körs, på grund av uppdateringslåset som begärs av läsoperationen. Skärmdumpen nedan visar låsen som hålls när frågan blockeras:
Som tidigare blockeras frågan vid sökningen i väntan på att det inkompatibla exklusiva låset ska släppas så att ett uppdateringslås kan erhållas.
Infogningstestet
Nästa test innehåller en sats som infogar en ny rad i vår testtabell med hjälp av kolumnen Data från den befintliga raden med ID 1 i tabellen. Kom ihåg att den här raden fortfarande är uteslutande låst av session med den öppna transaktionen:
INSERT dbo.Test (RowID, Data) SELECT 3, Data FROM dbo.Test WHERE RowID = 1;
Utförandeplanen liknar återigen de tidigare testerna:
Den här gången är frågan inte blockerad . Detta visar att uppdateringslås inte erhölls vid läsning data för insatsen. Den här frågan använde istället radversionering för att hämta datakolumnvärdet för den nyligen infogade raden. Uppdateringslås hämtades inte eftersom detta uttalande inte hittade några rader att modifiera , den läser bara data som ska användas i bilagan.
Vi kan se den här nya raden i tabellen med hjälp av den valda testfrågan från tidigare:
Observera att vi är kunna uppdatera och ta bort den nya raden (som kommer att kräva uppdateringslås) eftersom det inte finns något motstridande exklusivt lås. Sessionen med den öppna transaktionen har endast exklusiva lås på rad 1 och 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;
Det här testet bekräftar att infoga uttalanden inte får uppdateringslås vid läsning , eftersom de till skillnad från uppdateringar och borttagningar inte ändras en befintlig rad. Läsdelen av en inlaga uttalandet använder det normala RCSI-radversionsbeteendet.
Test med flera referenser
Jag nämnde tidigare att endast den enstaka tabellreferensen som används för att hitta rader att modifiera får uppdateringslås; andra tabeller i samma uppdatering eller delete-sats läser fortfarande radversioner. Som ett specialfall av den allmänna principen, en datamodifieringssats med flera referenser till samma tabell tillämpar endast uppdateringslås på en instans används för att hitta rader som ska ändras. Detta sista test illustrerar detta mer komplexa beteende, steg för steg.
Det första vi behöver är en ny tredje rad för vår testtabell, denna gång med en nolla i kolumnen Data:
INSERT dbo.Test (RowID, Data) VALUES (3, 0);
Som förväntat fortsätter denna infogning utan att blockeras, vilket resulterar i en tabell som ser ut så här:
Kom ihåg att den andra sessionen fortfarande är exklusiv låser på rad 1 och 2 vid denna tidpunkt. Vi är fria att skaffa lås på rad 3 om vi behöver. Följande fråga är den vi kommer att använda för att visa beteendet med flera referenser till 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;
Detta är en mer komplex fråga, men dess funktion är relativt enkel. Det finns två referenser till testtabellen, en som jag har kallat ReadRef och den andra som WriteRef. Tanken är att läsa från rad 1 (med en radversion) via ReadRef och till uppdatering den tredje raden (som kommer att behöva ett uppdateringslås) med WriteRef.
Frågan specificerar rad 1 uttryckligen i where-satsen för lästabellreferensen. Den ansluter till skrivreferensen till samma tabell genom att lägga till 2 till det rad-ID (så att identifiera rad 3). Uppdateringssatsen använder också en utdatasats för att returnera en resultatuppsättning som visar värdena som läses från källtabellen och de resulterande ändringarna som gjorts på rad 3.
Den uppskattade frågeplanen för detta uttalande är följande:
Egenskaperna för sökningen märkt (1) visa att denna sökning finns på ReadRef alias, läser data från raden med RowID 1:
Denna sökoperation hittar inte en rad som kommer att uppdateras, så uppdateringslås är inte tagen; läsningen utförs med hjälp av versionsdata. Läsningen blockeras inte av de exklusiva låsen som innehas av den andra sessionen.
Beräkningsskalären märkt (2) definierar ett uttryck märkt 1004 som beräknar det uppdaterade datakolumnvärdet. Uttryck 1009 beräknar rad-ID som ska uppdateras (1 + 2 =rad-ID 3):
Den andra sökningen är en referens till samma tabell (3). Denna sökning lokaliserar raden som kommer att uppdateras (rad 3) med hjälp av uttryck 1009:
Eftersom denna sökning lokaliserar en rad som ska ändras, ett uppdateringslås tas istället för att använda radversioner. Det finns inget motstridande exklusivt lås på rad ID 3, så låsbegäran beviljas omedelbart.
Den sista markerade operatorn (4) är själva uppdateringsoperationen. Uppdateringslåset på rad 3 är uppgraderat till ett exklusivt lås vid denna punkt, precis innan ändringen faktiskt utförs. Den här operatorn returnerar också de data som anges i output-satsen av uppdateringsförklaringen:
Resultatet av uppdateringssatsen (genererad av utdatasatsen) visas nedan:
Det slutliga tillståndet för tabellen är som visas nedan:
Vi kan bekräfta låsningarna som togs under körningen med hjälp av en Profiler-spårning:
Detta visar att endast en enda uppdatering radnyckellås har förvärvats. När den här raden når uppdateringsoperatören konverteras låset till ett exklusivt låsa. I slutet av uttalandet frigörs låset.
Du kanske kan se från spårningsutgången att låshashvärdet för den uppdateringslåsta raden är (98ec012aa510) i min testdatabas. Följande fråga visar att denna låshash verkligen är associerad med RowID 3 i det klustrade indexet:
SELECT RowID, %%LockRes%% FROM dbo.Test;
Observera att uppdateringslåsen som tas i dessa exempel är kortare än uppdateringslåsen om vi anger en UPDLOCK
antydan. Dessa interna uppdateringslås släpps i slutet av uttalandet, medan UPDLOCK
lås hålls till slutet av transaktionen.
Detta avslutar demonstrationen av fall där RCSI skaffar uppdateringslås för att läsa aktuella engagerade data istället för att använda radversionering.
Delat och nyckelområdeslås under RCSI
Det finns ett antal andra scenarier där databasmotorn fortfarande kan förvärva lås under RCSI. Dessa situationer relaterar alla till behovet av att bevara korrektheten som skulle hotas av att förlita sig på potentiellt inaktuella versionsdata.
Delade lås tagna för validering av utländsk nyckel
För två tabeller i en enkel relation med främmande nyckel måste databasmotorn vidta åtgärder för att säkerställa att begränsningar inte överträds genom att förlita sig på potentiellt inaktuella versionsläsningar. Den nuvarande implementeringen gör detta genom att byta till låsning av läsbekräftad vid åtkomst till data som en del av en automatisk kontroll av främmande nyckel.
Genom att ta delade lås säkerställs att integritetskontrollen läser de allra senaste säkrade uppgifterna (inte en gammal version), eller blockerar på grund av en samtidig modifiering under flygningen. Bytet till låsning av läs committed gäller endast den särskilda åtkomstmetod som används för att kontrollera främmande nyckeldata; annan dataåtkomst i samma programsats fortsätter att använda radversioner.
Detta beteende gäller endast uttalanden som ändrar data, där ändringen direkt påverkar en främmande nyckelrelation. För ändringar av den refererade (överordnade) tabellen betyder detta uppdateringar som påverkar det refererade värdet (såvida det inte är satt till NULL
) och alla raderingar. För referenstabellen (underordnad) betyder detta alla infogningar och uppdateringar (igen, om inte nyckelreferensen är NULL
). Samma överväganden gäller för komponenteffekterna av en MERGE
.
Ett exempel på utförandeplan som visar en uppslagning av främmande nyckel som tar delade lås visas nedan:
Serialiserbar för kaskadkoppling av främmande nycklar
Där den främmande nyckelrelationen har en kaskadåtgärd, kräver korrekthet en lokal upptrappning till serialiserbar isoleringssemantik. Det betyder att du kommer att se nyckelintervallslås som tas för en överlappande referensåtgärd. Som var fallet för uppdateringslåsen som vi sett tidigare, är dessa nyckelintervallslås inriktade på uttalandet, inte transaktionen. Ett exempel på utförandeplan som visar var de interna serialiserbara låsen tas under RCSI visas nedan:
Andra scenarier
Det finns många andra specifika fall där motorn automatiskt förlänger livslängden för lås, eller lokalt eskalerar isoleringsnivån för att säkerställa korrekthet. Dessa inkluderar den serialiserbara semantiken som används vid underhåll av en relaterad indexerad vy eller vid underhåll av ett index som har IGNORE_DUP_KEY
alternativ inställd.
Takeaway-meddelandet är att RCSI minskar mängden låsning, men kan inte alltid eliminera det helt.
Nästa gång
Nästa inlägg i den här serien tittar på isoleringsnivån för ögonblicksbilder.
[ Se indexet för hela serien ]