sql >> Databasteknik >  >> RDS >> Database

SNAPSHOT-isoleringsnivån

[ Se indexet för hela serien ]

Samtidighetsproblem är svåra på samma sätt som flertrådsprogrammering är svårt. Om inte serialiserbar isolering används kan det vara svårt att koda T-SQL-transaktioner som alltid kommer att fungera korrekt när andra användare gör ändringar i databasen samtidigt.

De potentiella problemen kan vara icke-triviala även om "transaktionen" i fråga är en enkel enkel SELECT påstående. För komplexa transaktioner med flera påståenden som läser och skriver data kan potentialen för oväntade resultat och fel under hög samtidighet snabbt bli överväldigande. Att försöka lösa subtila och svåråterskapliga samtidighetsproblem genom att använda slumpmässiga låstips eller andra prova-och-fel-metoder kan vara en extremt frustrerande upplevelse.

I många avseenden verkar isoleringsnivån för ögonblicksbilder vara en perfekt lösning på dessa samtidighetsproblem. Grundidén är att varje ögonblicksbildstransaktion beter sig som om den exekveras mot sin egen privata kopia av databasens tillstånd, tagen vid det ögonblick då transaktionen startade. Att förse hela transaktionen med en oföränderlig syn på engagerad data garanterar självklart konsekventa resultat för skrivskyddade operationer, men hur är det med transaktioner som ändrar data?

Snapshot-isolering hanterar dataförändringar optimistiskt, underförstått under förutsättning att konflikter mellan samtidiga skribenter kommer att vara relativt sällsynta. Om en skrivkonflikt inträffar vinner den första committen och den förlorande transaktionen får sina ändringar tillbaka. Det är naturligtvis olyckligt för den återställda transaktionen, men om detta är en tillräckligt sällsynt händelse kan fördelarna med ögonblicksbildsisolering lätt uppväga kostnaderna för ett tillfälligt misslyckande och försök igen.

Den relativt enkla och rena semantiken för ögonblicksbildsisolering (jämfört med alternativen) kan vara en betydande fördel, särskilt för personer som inte enbart arbetar i databasvärlden och därför inte känner till de olika isoleringsnivåerna väl. Även för erfarna databasproffs kan en relativt "intuitiv" isoleringsnivå vara en välkommen lättnad.

Naturligtvis är saker sällan så enkla som de först verkar, och ögonblicksbildsisolering är inget undantag. Den officiella dokumentationen gör ett ganska bra jobb med att beskriva de stora fördelarna och nackdelarna med ögonblicksbildsisolering, så huvuddelen av den här artikeln koncentrerar sig på att utforska några av de mindre välkända och överraskande problem som du kan stöta på. Men först, en snabb titt på de logiska egenskaperna hos denna isoleringsnivå:

ACID-egenskaper och ögonblicksbildsisolering

Snapshot-isolering är inte en av de isoleringsnivåer som definieras i SQL-standarden, men den jämförs fortfarande ofta med de "samtidsfenomen" som definieras där. Till exempel är följande jämförelsetabell återgiven från SQL Server Technical Article, "SQL Server 2005 Row Versioning-Based Transaction Isolation" av Kimberly L. Tripp och Neal Graves:

Genom att tillhandahålla en tidpunktvy av engagerad data , ögonblicksbildsisolering ger skydd mot alla tre samtidighetsfenomen som visas där. Smutsiga läsningar förhindras eftersom endast engagerad data är synlig, och ögonblicksbildens statiska karaktär förhindrar att både icke-repeterbara läsningar och fantomer påträffas.

Denna jämförelse (och det markerade avsnittet i synnerhet) visar dock bara att ögonblicksbilden och serialiserbara isoleringsnivåer förhindrar samma tre specifika fenomen. Det betyder inte att de är likvärdiga i alla avseenden. Viktigt är att SQL-92-standarden inte definierar serialiserbar isolering endast i termer av de tre fenomenen. Avsnitt 4.28 i standarden ger den fullständiga definitionen:

Utförandet av samtidiga SQL-transaktioner på isoleringsnivå SERIALIZABLE är garanterat serialiserbart. En serialiserbar exekvering definieras som en exekvering av operationerna för att samtidigt utföra SQL-transaktioner som ger samma effekt som någon seriell exekvering av samma SQL-transaktioner. En seriell exekvering är en där varje SQL-transaktion körs till slut innan nästa SQL-transaktion börjar.

Omfattningen och betydelsen av de underförstådda garantierna här missas ofta. För att uttrycka det på ett enkelt språk:

Alla serialiserbara transaktioner som körs korrekt när de körs ensamma kommer att fortsätta att köras korrekt med valfri kombination av samtidiga transaktioner, eller så kommer den att rullas tillbaka med ett felmeddelande (vanligtvis ett dödläge i SQL Servers implementering).

Icke-serialiserbara isoleringsnivåer, inklusive ögonblicksbildsisolering, ger inte samma starka garantier för korrekthet.

Inaktuella data

Snapshot-isolering verkar nästan förföriskt enkelt. Läsningar kommer alltid från engagerad data från en enda tidpunkt, och skrivkonflikter upptäcks och hanteras automatiskt. Hur är detta inte en perfekt lösning för alla samtidighetsrelaterade svårigheter?

Ett potentiellt problem är att ögonblicksbildläsningar inte nödvändigtvis återspeglar det aktuella tillståndet för databasen. En ögonblicksbildstransaktion ignorerar fullständigt alla genomförda ändringar som görs av andra samtidiga transaktioner efter att ögonblicksbildstransaktionen har börjat. Ett annat sätt att uttrycka det är att säga att en ögonblicksbildstransaktion ser inaktuella, inaktuella data. Även om detta beteende kan vara exakt vad som behövs för att generera en korrekt tidpunktsrapport, kanske det inte är fullt så lämpligt under andra omständigheter (till exempel när det används för att upprätthålla en regel i en utlösare).

Skriv skevt

Snapshot-isolering är också sårbart för ett något relaterat fenomen som kallas skrivskev. Att läsa inaktuella data spelar en roll i detta, men det här problemet hjälper också till att klargöra vad ögonblicksbilden "skrivkonfliktdetektering" gör och inte gör.

Skrivskevning uppstår när två samtidiga transaktioner var och en läser data som den andra transaktionen ändrar. Ingen skrivkonflikt uppstår eftersom de två transaktionerna ändrar olika rader. Ingen av transaktionerna ser ändringarna som gjorts av den andra, eftersom båda läser från en tidpunkt innan dessa ändringar gjordes.

Ett klassiskt exempel på skrivskev är problemet med vit och svart marmor, men jag vill visa ett annat enkelt exempel här:

-- Create two empty tables
CREATE TABLE A (x integer NOT NULL);
CREATE TABLE B (x integer NOT NULL);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT A (x) SELECT COUNT_BIG(*) FROM B;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT B (x) SELECT COUNT_BIG(*) FROM A;
COMMIT TRANSACTION;
 
-- Connection 1
COMMIT TRANSACTION;

Under ögonblicksbildsisolering slutar båda tabellerna i det skriptet med en enda rad som innehåller ett nollvärde. Detta är ett korrekt resultat, men det är inte ett serialiserbart resultat:det motsvarar inte en eventuell seriell transaktionsexekveringsorder. I vilket som helst seriellt schema måste en transaktion slutföras innan den andra börjar, så den andra transaktionen skulle räkna raden som infogats av den första. Detta kan låta som en teknisk detalj, men kom ihåg att de kraftfulla serialiserbara garantierna endast gäller när transaktioner verkligen är serialiserbara.

En subtilitet för att upptäcka konflikter

En skrivkonflikt för ögonblicksbild uppstår när en ögonblicksbildstransaktion försöker ändra en rad som har modifierats av en annan transaktion som genomfördes efter att ögonblicksbildstransaktionen började. Det finns två subtiliteter här:

  1. Transaktionerna behöver faktiskt inte ändras eventuella datavärden; och
  2. Transaktionerna behöver inte ändra några vanliga kolumner .

Följande skript visar båda punkterna:

-- Test table
CREATE TABLE dbo.Conflict
(
    ID1 integer UNIQUE,
    Value1 integer NOT NULL,
    ID2 integer UNIQUE,
    Value2 integer NOT NULL
);
 
-- Insert one row
INSERT dbo.Conflict
    (ID1, ID2, Value1, Value2)
VALUES
    (1, 1, 1, 1);
 
-- Connection 1
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value1 = 1
WHERE ID1 = 1;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value2 = 1
WHERE ID2 = 1;
 
-- Connection 1
COMMIT TRANSACTION;

Lägg märke till följande:

  • Varje transaktion lokaliserar samma rad med ett annat index
  • Ingen uppdatering resulterar i en ändring av den data som redan lagrats
  • De två transaktionerna "uppdaterar" olika kolumner i raden.

Trots allt detta, när den första transaktionen genomförs avslutas den andra transaktionen med ett uppdateringskonfliktfel:

Sammanfattning:Konfliktdetektering fungerar alltid på nivån för en hel rad, och en "uppdatering" behöver faktiskt inte ändra någon data. (Om du undrade så räknas ändringar av LOB- eller SLOB-data utanför raden också som en ändring av raden för konfliktdetektering).

Problemet med främmande nyckel

Konfliktdetektering gäller även den överordnade raden i en främmande nyckelrelation. När du ändrar en underordnad rad under ögonblicksbildsisolering kan en ändring av den överordnade raden i en annan transaktion utlösa en konflikt. Som tidigare gäller denna logik för hela den överordnade raden – den överordnade uppdateringen behöver inte påverka själva kolumnen för främmande nyckel. Varje operation på den underordnade tabellen som kräver en automatisk kontroll av främmande nyckel i exekveringsplanen kan resultera i en oväntad konflikt.

För att visa detta, skapa först följande tabeller och exempeldata:

CREATE TABLE dbo.Dummy
(
    x integer NULL
);
 
CREATE TABLE dbo.Parent
(
    ParentID integer PRIMARY KEY,
    ParentValue integer NOT NULL
);
 
CREATE TABLE dbo.Child 
(
    ChildID integer PRIMARY KEY,
    ChildValue integer NOT NULL,
    ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent
);
 
INSERT dbo.Parent 
    (ParentID, ParentValue) 
VALUES (1, 1);
 
INSERT dbo.Child 
    (ChildID, ChildValue, ParentID) 
VALUES (1, 1, 1);

Kör nu följande från två separata anslutningar som anges i kommentarerna:

-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.Dummy;
 
-- Connection 2 (any isolation level)
UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1;
 
-- Connection 1
UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1;
UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;

Avläsningen från dummytabellen finns där för att säkerställa att ögonblicksbildstransaktionen officiellt har startat. Utfärdar BEGIN TRANSACTION är inte tillräckligt för att göra detta; vi måste utföra någon form av dataåtkomst på en användartabell.

Den första uppdateringen av Child-tabellen orsakar ingen konflikt eftersom referenskolumnen ställs in på NULL kräver inte en överordnad tabellkontroll i exekveringsplanen (det finns inget att kontrollera). Frågeprocessorn rör inte den överordnade raden i exekveringsplanen, så ingen konflikt uppstår.

Den andra uppdateringen av Child-tabellen utlöser en konflikt eftersom en främmande nyckelkontroll utförs automatiskt. När den överordnade raden nås av frågeprocessorn kontrolleras den också för en uppdateringskonflikt. Ett fel uppstår i det här fallet eftersom den refererade överordnade raden har upplevt en bekräftad ändring efter att ögonblicksbildstransaktionen startade. Observera att modifieringen av överordnad tabell inte påverkade själva kolumnen för främmande nyckel.

En oväntad konflikt kan också uppstå om en ändring av tabellen Underordnade hänvisar till en överordnad rad som skapades av en samtidig transaktion (och den transaktion som genomfördes efter att ögonblicksbildstransaktionen startade).

Sammanfattning:En frågeplan som inkluderar en automatisk kontroll av främmande nyckel kan orsaka ett konfliktfel om den refererade raden har upplevt någon form av modifiering (inklusive skapande!) sedan ögonblicksbildstransaktionen startade.

Truncate Table Issue

En ögonblicksbildstransaktion kommer att misslyckas med ett fel om någon tabell som den kommer åt har trunkerats sedan transaktionen började. Detta gäller även om den trunkerade tabellen inte hade några rader till att börja med, vilket skriptet nedan visar:

CREATE TABLE dbo.AccessMe
(
    x integer NULL
);
 
CREATE TABLE dbo.TruncateMe
(
    x integer NULL
);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.AccessMe;
 
-- Connection 2
TRUNCATE TABLE dbo.TruncateMe;
 
-- Connection 1
SELECT COUNT_BIG(*) FROM dbo.TruncateMe;

Den slutliga SELECT misslyckas med ett fel:

Detta är ytterligare en subtil bieffekt att kontrollera innan du aktiverar ögonblicksbildsisolering på en befintlig databas.

Nästa gång

Nästa (och sista) inlägg i den här serien kommer att tala om den lästa oengagerade isoleringsnivån (kärleksmässigt känd som "nolock").

[ Se indexet för hela serien ]


  1. Förstå Dirty Read-problem med SQL Server

  2. Var uppmärksam på uppskattningar

  3. SQLite GLOB

  4. Android SQLite-fel:variabelnummer måste vara mellan ?1 och ?999