sql >> Databasteknik >  >> RDS >> Database

Läs Isolering av engagerad ögonblicksbild

[ Se indexet för hela serien ]

SQL Server tillhandahåller två fysiska implementeringar av read committed isoleringsnivå som definieras av SQL-standarden, låser läs committed och read committed ögonblicksbildisolering (RCSI ). Även om båda implementeringarna uppfyller kraven som fastställs i SQL-standarden för läskommitterade isoleringsbeteenden, har RCSI ganska olika fysiska beteenden från låsimplementeringen som vi tittade på i föregående inlägg i den här serien.

Logiska garantier

SQL-standarden kräver att en transaktion som arbetar på den läskommitterade isoleringsnivån inte upplever några smutsiga läsningar. Ett annat sätt att uttrycka detta krav är att säga att en läs-begärd transaktion endast får stöta på committed data .

Standarden säger också att läsa begångna transaktioner kan upplev samtidighetsfenomenen som kallas icke-repeterbara läsningar och fantomer (även om de faktiskt inte är skyldiga att göra det). Som det händer kan båda fysiska implementeringarna av läs-committed isolation i SQL Server uppleva icke-repeterbara läsningar och fantomrader, även om de exakta detaljerna är ganska olika.

En punkt-i-tid-vy av engagerad data

Om databasalternativet READ_COMMITTED_SNAPSHOT i ON , SQL Server använder en radversionsimplementering av den läsbetrodda isoleringsnivån. När detta är aktiverat använder transaktioner som begär läs-beslutad isolering automatiskt RCSI-implementeringen; inga ändringar av befintlig T-SQL-kod krävs för att använda RCSI. Observera dock noga att detta inte är samma som att säga att koden kommer att bete sig likadant under RCSI som när man använder låsimplementeringen av read committed, är detta faktiskt ganska generellt inte fallet .

Det finns inget i SQL-standarden som kräver att data som läses av en läsberättigad transaktion är den senaste engagerade uppgifter. SQL Server RCSI-implementeringen drar fördel av detta för att ge transaktioner en punkt-i-tidsvy av engagerad data, där den tidpunkten är det ögonblick då det aktuella uttalandet började exekvering (inte det ögonblick då någon innehållande transaktion startade).

Detta skiljer sig ganska mycket från beteendet hos SQL Server-låsningsimplementeringen av read committed, där satsen ser de senast committerade data från och med i det ögonblick varje objekt läses fysiskt . Låsning av läskommitterade utgåvor ger delade lås så snabbt som möjligt, så uppsättningen av data som påträffas kan komma från mycket olika tidpunkter.

För att sammanfatta, låsning av läs committed ser varje rad som det var vid den tiden var det kort låst och fysiskt läst; RCSI ser alla rader som de var när uttalandet började. Båda implementeringarna kommer garanterat aldrig att se oengagerad data, men den data de möter kan vara väldigt olika.

Konsekvenserna av en punkt-i-tidsvy

Att se en punkt-i-tidsvy av engagerade data kan verka självklart överlägsen det mer komplexa beteendet hos låsimplementeringen. Det är till exempel tydligt att en punkt-i-tidsvy inte kan lida av problemen med saknade rader eller stöter på samma rad flera gånger , som båda är möjliga under låsning av läs engagerad isolering.

En andra viktig fördel med RCSI är att den inte får delade lås vid läsning av data, eftersom data kommer från radversionslagret snarare än att de nås direkt. Bristen på delade lås kan dramatiskt förbättra samtidigheten genom att eliminera konflikter med samtidiga transaktioner som vill skaffa inkompatibla lås. Denna fördel sammanfattas vanligen genom att säga att läsare inte blockerar skribenter under RCSI, och vice versa. Som en ytterligare konsekvens av att minska blockeringen på grund av inkompatibla låsförfrågningar, är möjligheten för dödläge är vanligtvis kraftigt reducerad när du kör under RCSI.

Dessa fördelar kommer dock inte utan kostnader och varningar . För det första förbrukar underhåll av versioner av committerade rader systemresurser, så det är viktigt att den fysiska miljön är konfigurerad för att klara av detta, främst vad gäller tempdb prestanda och krav på minne/diskutrymme.

Den andra varningen är lite mer subtil:RCSI ger en ögonblicksbild av engagerad data som den var i början av satsen, men det finns inget som hindrar den verkliga datan från att ändras (och dessa ändringar committeras) medan RCSI-satsen körs. Det finns inga delade lås, kom ihåg. En omedelbar konsekvens av denna andra punkt är att T-SQL-kod som körs under RCSI kan fatta beslut baserat på inaktuell information , jämfört med det aktuella tillståndet för databasen. Vi kommer att prata mer om detta inom kort.

Det finns en sista (implementationsspecifik) observation jag vill göra om RCSI innan vi går vidare. Skalära funktioner och funktioner för flera påståenden exekvera med en annan intern T-SQL-kontext än den innehållande satsen. Detta innebär att punkt-i-tidsvyn som ses inuti en skalär eller multi-sats funktionsanrop kan vara senare än punkt-i-tidsvyn som ses av resten av uttalandet. Detta kan resultera i oväntade inkonsekvenser, eftersom olika delar av samma uttalande ser data från olika tidpunkter . Detta konstiga och förvirrande beteende inte tillämpas på in-line-funktioner, som ser samma ögonblicksbild som satsen de visas i.

Icke-repeterbara läsningar och fantomer

Givet en punkt-i-tid-vy på uttalandenivå av det engagerade tillståndet för databasen, kanske det inte är omedelbart uppenbart hur en läs-begärd transaktion under RCSI kan uppleva det icke-repeterbara läs- eller fantomradsfenomenet. Ja, om vi begränsar vårt tänkande till omfattningen av ett enskilt uttalande , inget av dessa fenomen är möjliga under RCSI.

Läser samma data flera gånger inom samma uttalande under RCSI kommer alltid att returnera samma datavärden, ingen data kommer att försvinna mellan dessa läsningar och inga nya data kommer heller att dyka upp. Om du undrar vilken typ av påstående som kan läsa samma data mer än en gång, tänk på frågor som refererar till samma tabell mer än en gång, kanske i en underfråga.

Läskonsistens på uttalandenivå är en uppenbar konsekvens av att läsningarna utfärdas mot en fast ögonblicksbild av data. Anledningen till att RCSI inte gör det ger skydd mot icke-repeterbara läsningar och fantomer är att dessa SQL-standardfenomen definieras på transaktionsnivå. Flera uttalanden inom en transaktion som körs på RCSI kan se olika data, eftersom varje uttalande ser en punkt-i-tidsvy från det ögonblick som det specifika uttalandet startade.

För att sammanfatta, varje påstående inom en RCSI-transaktion ser en statisk committed datamängd, men den uppsättningen kan ändras mellan uttalanden i samma transaktion.

Inaktuell data

Möjligheten att vår T-SQL-kod fattar ett viktigt beslut baserat på inaktuell information är mer än lite oroande. Tänk ett ögonblick på att ögonblicksbilden för tidpunkten som används av en enskild sats som körs under RCSI kan vara godtyckligt gammal .

Ett uttalande som körs under en längre tid kommer att fortsätta att se databasens tillstånd som det var när uttalandet började. Samtidigt saknar uttalandet alla begångna ändringar som har skett i databasen sedan den tiden.

Detta betyder inte att problem som är förknippade med åtkomst av inaktuella data under RCSI är begränsade till långvariga uttalanden, men frågorna kan säkert vara mer uttalade i sådana fall.

En fråga om timing

Detta nummer av inaktuella data gäller i princip alla RCSI-uttalanden, oavsett hur snabbt de kan slutföras. Hur litet tidsfönstret än är, finns det alltid en chans att en samtidig operation kan ändra datamängden vi arbetar med, utan att vi är medvetna om den förändringen. Låt oss återigen titta på ett av de enkla exemplen som vi använde tidigare när vi undersökte beteendet för låsning av läs 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örs under RCSI kan inte denna sats se alla begångna databasändringar som inträffar efter att satsen börjar köras. Även om vi inte kommer att stöta på problemen med missade eller multiplicerade rader som är möjliga under låsimplementeringen, kan en samtidig transaktion lägga till en betalning som bör för att förhindra att en kund får ett strängt varningsbrev om en försenad betalning efter att uttalandet ovan börjar verkställas.

Du kan förmodligen tänka på många andra potentiella problem som kan uppstå i detta scenario, eller i andra som är begreppsmässigt lika. Ju längre uttalandet pågår, desto mer inaktuell blir dess syn på databasen, och desto större utrymme för eventuellt oavsiktliga konsekvenser.

Naturligtvis finns det många förmildrande faktorer i detta specifika exempel. Beteendet kan mycket väl ses som helt acceptabelt. Att skicka ett påminnelsebrev för att en betalning kom några sekunder för sent är trots allt en lättförsvarad åtgärd. Principen kvarstår dock.

Företagsfel och integritetsrisker

Mer allvarliga problem kan uppstå vid användning av inaktuell information än att skicka ett varningsbrev några sekunder i förtid. Ett bra exempel på denna klass av svaghet kan ses med triggerkod används för att upprätthålla en integritetsregel som kanske är för komplex för att upprätthålla med deklarativa referensmässiga integritetsbegränsningar. För att illustrera, överväg följande kod, som använder en utlösare för att genomdriva en variant av en främmande nyckelbegränsning, men en som upprätthåller relationen för endast vissa underordnade tabellrader:

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);

Överväg nu en transaktion som körs i en annan session (använd ett annat SSMS-fönster för detta om du följer med) som tar bort överordnad rad #1, men som inte commit ännu:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
DELETE FROM dbo.Parent
WHERE ParentID = 1;

Tillbaka i vår ursprungliga session försöker vi infoga en (markerad) underordnad rad som refererar till denna förälder:

INSERT dbo.Child (ParentID, CheckMe)
VALUES (1, 1);

Triggerkoden körs, men eftersom RCSI endast ser committed data från den tidpunkt då uttalandet startade, ser det fortfarande den överordnade raden (inte den oengagerade raderingen) och infogningen lyckas !

Transaktionen som tog bort den överordnade raden kan nu genomföra sin ändring framgångsrikt, vilket lämnar databasen i en inkonsekvent tillstånd i termer av vår triggerlogik:

COMMIT TRANSACTION;
SELECT P.* FROM dbo.Parent AS P;
SELECT C.* FROM dbo.Child AS C;

Detta är naturligtvis ett förenklat exempel, och ett som lätt skulle kunna kringgås med hjälp av de inbyggda begränsningsfaciliteterna. Mycket mer komplexa affärsregler och pseudointegritetsbegränsningar kan skrivas inuti och utanför triggers . Potentialen för felaktigt beteende under RCSI bör vara uppenbar.

Blockeringsbeteende och senast engagerad data

Jag nämnde tidigare att T-SQL-koden inte garanteras att bete sig på samma sätt under RCSI read committed som den gjorde med låsimplementationen. Det föregående triggerkodexemplet är en bra illustration av det, men jag måste betona att det allmänna problemet inte är begränsat till triggers .

RCSI är vanligtvis inte ett bra val för någon T-SQL-kod vars korrekthet beror på blockering om en samtidig oengagerad ändring existerar. RCSI kanske inte heller är rätt val om koden är beroende av att läsa aktuell begångna uppgifter, snarare än de senaste begångna uppgifterna vid den tidpunkt då uttalandet började. Dessa två överväganden är relaterade, men de är inte samma sak.

Låsning av läsning begått under RCSI

SQL Server tillhandahåller ett sätt att begära låsning read committed när RCSI är aktiverat, med hjälp av tabelltipset READCOMMITTEDLOCK . Vi kan modifiera vår utlösare för att undvika problemen som visas ovan genom att lägga till denna ledtråd i tabellen som behöver blockeringsbeteende för att fungera 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 denna ändring på plats blockeras försöket att infoga de potentiellt föräldralösa underordnade raden tills borttagningstransaktionen genomförs (eller avbryts). Om borttagningen begår, upptäcker triggerkoden integritetsintrånget och höjer det förväntade felet.

Identifiera frågor som kanske inte fungerar korrekt under RCSI är en icke-trivial uppgift som kan kräva omfattande testning för att komma rätt (och kom ihåg att dessa problem är ganska allmänna och inte begränsade till utlösningskoden!) Lägg också till READCOMMITTEDLOCK tips till varje bord som behöver det kan vara en tråkig och felbenägen process. Tills SQL Server tillhandahåller ett mer omfattande alternativ för att begära låsimplementeringen där det behövs, har vi fastnat för att använda tabelltipsen.

Nästa gång

Nästa inlägg i den här serien fortsätter vår granskning av isolering av läs engagerad ögonblicksbild, med en titt på det överraskande beteendet hos datamodifieringssatser under RCSI.

[ Se indexet för hela serien ]


  1. SQLite ÄR NULL

  2. Hur kan jag extrahera filer från ett Oracle BLOB-fält?

  3. Hur ansluter man till mssql med pdo via PHP och Linux?

  4. BULK INSERT i MYSQL