sql >> Database teknologi >  >> RDS >> Database

SNAPSHOT-isolationsniveauet

[ Se indekset for hele serien ]

Samtidighedsproblemer er svære på samme måde som multi-threaded programmering er svært. Medmindre der bruges serialiserbar isolation, kan det være svært at kode T-SQL-transaktioner, der altid vil fungere korrekt, når andre brugere foretager ændringer i databasen på samme tid.

De potentielle problemer kan være ikke-trivielle, selvom den pågældende 'transaktion' er en simpel enkelt SELECT udmelding. For komplekse transaktioner med flere sætninger, der læser og skriver data, kan potentialet for uventede resultater og fejl under høj samtidighed hurtigt blive overvældende. Forsøg på at løse subtile og svære at gengive samtidighedsproblemer ved at anvende tilfældige låsetips eller andre prøv-og-fejl-metoder kan være en ekstremt frustrerende oplevelse.

I mange henseender virker snapshot-isolationsniveauet som en perfekt løsning på disse samtidighedsproblemer. Den grundlæggende idé er, at hver snapshot-transaktion opfører sig, som om den blev udført i forhold til sin egen private kopi af databasens forpligtede tilstand, taget i det øjeblik, transaktionen startede. At give hele transaktionen et uforanderligt syn på forpligtede data garanterer naturligvis ensartede resultater for skrivebeskyttede operationer, men hvad med transaktioner, der ændrer data?

Snapshot-isolering håndterer dataændringer optimistisk, implicit under forudsætning af, at konflikter mellem samtidige forfattere vil være relativt sjældne. Hvor der opstår en skrivekonflikt, vinder den første committer, og den tabende transaktion får sine ændringer rullet tilbage. Det er selvfølgelig uheldigt for den tilbagerullede transaktion, men hvis dette er en sjælden nok hændelse, kan fordelene ved snapshot-isolering let opveje omkostningerne ved en lejlighedsvis fejl og forsøg igen.

Den relativt enkle og rene semantik af snapshot-isolering (sammenlignet med alternativerne) kan være en væsentlig fordel, især for folk, der ikke udelukkende arbejder i databaseverdenen og derfor ikke kender de forskellige isolationsniveauer godt. Selv for erfarne databaseprofessionelle kan et relativt 'intuitivt' isolationsniveau være en kærkommen lettelse.

Selvfølgelig er tingene sjældent så enkle, som de først ser ud, og snapshot-isolering er ingen undtagelse. Den officielle dokumentation gør et ret godt stykke arbejde med at beskrive de store fordele og ulemper ved snapshot-isolering, så hovedparten af ​​denne artikel koncentrerer sig om at udforske nogle af de mindre kendte og overraskende problemer, du kan støde på. Først dog et hurtigt kig på de logiske egenskaber ved dette isolationsniveau:

ACID-egenskaber og snapshot-isolering

Snapshot-isolation er ikke et af de isolationsniveauer, der er defineret i SQL-standarden, men det sammenlignes stadig ofte ved at bruge 'samtidighedsfænomenerne', der er defineret der. For eksempel er følgende sammenligningstabel gengivet fra SQL Server Technical Article, "SQL Server 2005 Row Versioning-Based Transaction Isolation" af Kimberly L. Tripp og Neal Graves:

Ved at give en tidsvisning af forpligtede data , snapshot-isolering giver beskyttelse mod alle tre samtidighedsfænomener, der vises der. Beskidte læsninger forhindres, fordi kun forpligtede data er synlige, og øjebliksbilledets statiske karakter forhindrer både ikke-gentagelige læsninger og fantomer i at blive stødt på.

Denne sammenligning (og det fremhævede afsnit i særdeleshed) viser dog kun, at snapshot- og serialiserbare isolationsniveauer forhindrer de samme tre specifikke fænomener. Det betyder ikke, at de er ligeværdige i alle henseender. Det er vigtigt, at SQL-92-standarden ikke definerer serialiserbar isolation alene med hensyn til de tre fænomener. Afsnit 4.28 i standarden giver den fulde definition:

Udførelsen af ​​samtidige SQL-transaktioner på isolationsniveau SERIALIZABLE er garanteret serialiserbar. En serialiserbar udførelse er defineret til at være en udførelse af operationerne ved samtidig udførelse af SQL-transaktioner, der producerer den samme effekt som en seriel udførelse af de samme SQL-transaktioner. En seriel eksekvering er en, hvor hver SQL-transaktion udføres til fuldførelse, før den næste SQL-transaktion begynder.

Omfanget og betydningen af ​​de underforståede garantier her går ofte glip af. For at angive det i et simpelt sprog:

Enhver serialiserbar transaktion, der udføres korrekt, når den køres alene, vil fortsætte med at udføre korrekt med enhver kombination af samtidige transaktioner, eller den vil blive rullet tilbage med en fejlmeddelelse (typisk et dødvande i SQL Servers implementering).

Ikke-serialiserbare isolationsniveauer, inklusive snapshot-isolering, giver ikke de samme stærke garantier for korrekthed.

Uaktuelle data

Snapshot-isolation virker næsten forførende enkel. Læsninger kommer altid fra forpligtede data fra et enkelt tidspunkt, og skrivekonflikter registreres og håndteres automatisk. Hvordan er dette ikke en perfekt løsning til alle samtidighedsrelaterede vanskeligheder?

Et potentielt problem er, at snapshot-læsninger ikke nødvendigvis afspejler den aktuelle forpligtede tilstand af databasen. En snapshot-transaktion ignorerer fuldstændigt alle forpligtede ændringer foretaget af andre samtidige transaktioner, efter at snapshot-transaktionen begynder. En anden måde at sige det på er at sige, at en øjebliksbilledetransaktion ser forældede, forældede data. Selvom denne adfærd kan være præcis, hvad der er nødvendigt for at generere en nøjagtig rapport på tidspunktet, er den måske ikke helt så egnet under andre omstændigheder (f.eks. når den bruges til at håndhæve en regel i en trigger).

Skriv skævt

Snapshot-isolation er også sårbar over for et noget relateret fænomen kendt som skriveskævhed. Læsning af forældede data spiller en rolle i dette, men dette problem hjælper også med at afklare, hvad snapshot 'skrivekonfliktdetektion' gør og ikke gør.

Skriveskævhed opstår, når to samtidige transaktioner hver læser data, som den anden transaktion ændrer. Der opstår ingen skrivekonflikt, fordi de to transaktioner ændrer forskellige rækker. Ingen af ​​transaktionerne ser ændringerne foretaget af den anden, fordi begge læser fra et tidspunkt, før disse ændringer blev foretaget.

Et klassisk eksempel på skriveskævhed er problemet med hvid og sort marmor, men jeg vil gerne vise et andet simpelt eksempel her:

-- 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 snapshot-isolering ender begge tabeller i det script med en enkelt række, der indeholder en nulværdi. Dette er et korrekt resultat, men det kan ikke serialiseres:det svarer ikke til nogen mulig seriel transaktionsudførelsesordre. I enhver virkelig seriel tidsplan skal den ene transaktion fuldføres, før den anden starter, så den anden transaktion vil tælle rækken, der er indsat af den første. Dette lyder måske som en teknikalitet, men husk, at de kraftfulde serialiserbare garantier kun gælder, når transaktioner virkelig kan serialiseres.

En subtilitet til at opdage konflikter

En snapshot-skrivekonflikt opstår, når en snapshot-transaktion forsøger at ændre en række, der er blevet ændret af en anden transaktion, der blev udført, efter at snapshot-transaktionen begyndte. Der er to finesser her:

  1. Transaktionerne behøver faktisk ikke at ændres eventuelle dataværdier; og
  2. Transaktionerne behøver ikke at ændre nogen fælles kolonner .

Følgende script demonstrerer begge punkter:

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

Bemærk følgende:

  • Hver transaktion lokaliserer den samme række ved hjælp af et andet indeks
  • Ingen opdatering resulterer i en ændring af de data, der allerede er gemt
  • De to transaktioner 'opdaterer' forskellige kolonner i rækken.

På trods af alt dette, når den første transaktion begår, afsluttes den anden transaktion med en opdateringskonfliktfejl:

Opsummering:Konfliktdetektion fungerer altid på niveau med en hel række, og en 'opdatering' behøver faktisk ikke at ændre nogen data. (Hvis du undrede dig, tæller ændringer af LOB- eller SLOB-data uden for rækken også som en ændring af rækken til konfliktdetekteringsformål).

Problemet med fremmednøgle

Konfliktregistrering gælder også for den overordnede række i et fremmednøgleforhold. Når du ændrer en underordnet række under snapshot-isolering, kan en ændring af den overordnede række i en anden transaktion udløse en konflikt. Som før gælder denne logik for hele den overordnede række - forældreopdateringen behøver ikke at påvirke selve kolonnen med fremmednøgle. Enhver handling på den underordnede tabel, der kræver en automatisk kontrol af fremmednøgle i eksekveringsplanen, kan resultere i en uventet konflikt.

For at demonstrere dette skal du først oprette følgende tabeller og eksempeldata:

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

Udfør nu følgende fra to separate forbindelser som angivet i kommentarerne:

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

Læsningen fra dummy-tabellen er der for at sikre, at snapshot-transaktionen officielt er startet. Udsender BEGIN TRANSACTION er ikke nok til at gøre dette; vi skal udføre en form for dataadgang på en brugertabel.

Den første opdatering af Child-tabellen forårsager ikke en konflikt, fordi indstilling af referencekolonnen til NULL kræver ikke en overordnet tabelkontrol i udførelsesplanen (der er intet at kontrollere). Forespørgselsprocessoren rører ikke den overordnede række i udførelsesplanen, så der opstår ingen konflikt.

Den anden opdatering til Child-tabellen udløser en konflikt, fordi der automatisk udføres en fremmednøglekontrol. Når den overordnede række tilgås af forespørgselsprocessoren, kontrolleres den også for en opdateringskonflikt. Der opstår en fejl i dette tilfælde, fordi den refererede overordnede række har oplevet en forpligtet ændring, efter at øjebliksbilledetransaktionen startede. Bemærk, at ændringen af ​​den overordnede tabel ikke påvirkede selve kolonnen med fremmednøgle.

En uventet konflikt kan også opstå, hvis en ændring af tabellen underordnet refererer til en overordnet række, der er oprettet ved en samtidig transaktion (og den transaktion begået efter øjebliksbilledetransaktionen startede).

Resumé:En forespørgselsplan, der inkluderer en automatisk udenlandsk nøglekontrol, kan give en konfliktfejl, hvis den refererede række har oplevet nogen form for ændring (inklusive oprettelse!), siden snapshot-transaktionen startede.

Truncate Table Issue

En snapshot-transaktion vil mislykkes med en fejl, hvis en tabel, den får adgang til, er blevet afkortet, siden transaktionen begyndte. Dette gælder, selvom den trunkerede tabel ikke havde nogen rækker til at begynde med, som nedenstående script viser:

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;

Det endelige SELECT mislykkes med fejlen:

Dette er endnu en subtil bivirkning, du skal tjekke efter, før du aktiverer snapshot-isolering på en eksisterende database.

Næste gang

Det næste (og sidste) indlæg i denne serie vil tale om det læste uforpligtende isolationsniveau (kærligt kendt som "nolock").

[ Se indekset for hele serien ]


  1. Hvordan undgår man at SSIS FTP-opgave mislykkes, når der ikke er nogen filer at downloade?

  2. Kan ikke logge på SQL Server + SQL Server Authentication + Fejl:18456

  3. Hvad er bedre i MYSQL count(*) eller count(1)?

  4. JSON_REPLACE() – Erstat værdier i et JSON-dokument i MySQL