sql >> Databasteknik >  >> RDS >> Database

Isoleringsnivån Läs oengagerad

[ Se indexet för hela serien ]

Read uncommitted är den svagaste av de fyra transaktionsisoleringsnivåerna som definieras i SQL Standard (och av de sex implementerade i SQL Server). Den tillåter alla tre så kallade "samtidsfenomen", smutsiga läsningar , icke-repeterbara läsningar , och fantomer:

De flesta databasmänniskor är medvetna om dessa fenomen, åtminstone i kontur, men inte alla inser att de inte fullständigt beskriver de isoleringsgarantier som erbjuds; De beskriver inte heller intuitivt de olika beteenden man kan förvänta sig i en specifik implementering som SQL Server. Mer om det senare.

Transaktionsisolering – "Jag" i ACID

Varje SQL-kommando körs inom en transaktion (explicit, implicit eller auto-commit). Varje transaktion har en tillhörande isoleringsnivå, som avgör hur isolerad den är från effekterna av andra samtidiga transaktioner. Detta något tekniska koncept har viktiga konsekvenser för hur frågor utförs och kvaliteten på de resultat de producerar.

Tänk på en enkel fråga som räknar alla rader i en tabell. Om denna fråga kunde exekveras omedelbart (eller med noll samtidiga dataändringar), kan det bara finnas ett korrekt svar:antalet rader som fysiskt finns i tabellen vid det ögonblicket. I verkligheten tar det en viss tid att köra frågan, och resultatet kommer att bero på hur många rader exekveringsmotorn faktiskt stöter på när den korsar vilken fysisk struktur som helst som har valts för att komma åt data.

Om rader läggs till (eller tas bort från) tabellen av samtidiga transaktioner medan räkneoperationen pågår, kan olika resultat erhållas beroende på om radräkningstransaktionen stöter på alla, några eller inga av dessa samtidiga ändringar – som i sin tur beror på isoleringsnivån för radräkningstransaktionen.

Beroende på isoleringsnivå, fysiska detaljer och tidpunkten för de samtidiga operationerna kan vår räknetransaktion till och med ge ett resultat som aldrig var en sann återspegling av tabellens engagerade tillstånd vid någon tidpunkt under transaktionen.

Exempel

Tänk på en radräkningstransaktion som startar vid tidpunkten T1 och skannar tabellen från början till slut (i klustrad indexnyckelordning, för argumentets skull). I det ögonblicket finns det 100 säkrade rader i tabellen. En tid senare (vid tidpunkt T2) har vår räknetransaktion stött på 50 av dessa rader. I samma ögonblick infogar en samtidig transaktion två rader i tabellen, och commiter en kort tid senare vid tidpunkten T3 (innan räkningstransaktionen slutar). En av de infogade raderna råkar falla inom hälften av den klustrade indexstrukturen som vår räknetransaktion redan har bearbetat, medan den andra infogade raden sitter i den oräknade delen.

När radräkningstransaktionen är klar kommer den att rapportera 101 rader i detta scenario; 100 rader initialt i tabellen plus den enstaka infogade raden som påträffades under skanningen. Detta resultat stämmer inte överens med tabellhistoriken:det fanns 100 committerade rader vid tidpunkterna T1 och T2, sedan 102 committerade rader vid tidpunkten T3. Det fanns aldrig en tid då det fanns 101 engagerade rader.

Det överraskande (kanske, beroende på hur djupt du har tänkt på de här sakerna tidigare) är att detta resultat är möjligt på standard (låsande) läs-kommitterad isoleringsnivå, och även under repeterbar läsisolering. Båda dessa isoleringsnivåer är garanterade att endast läsa engagerad data, ändå fick vi ett resultat som inte representerar något engagerat tillstånd för databasen!

Analys

Den enda transaktionsisoleringsnivån som ger fullständig isolering från samtidiga effekter är serialiserbar. SQL Server-implementeringen av den serialiserbara isoleringsnivån innebär att en transaktion kommer att se de senaste committed data, från det ögonblick då data först låstes för åtkomst. Dessutom är uppsättningen av data som påträffas under serialiserbar isolering garanterat att inte ändra sitt medlemskap innan transaktionen avslutas.

Exemplet med radräkning belyser en grundläggande aspekt av databasteorin:vi måste vara tydliga med vad ett "korrekt" resultat betyder för en databas som upplever samtidiga modifieringar, och vi måste förstå de avvägningar vi gör när vi väljer en isolering nivå lägre än serialiserbar.

Om vi ​​behöver en punkt-i-tidsvy av det engagerade tillståndet för databasen, bör vi använda ögonblicksbildsisolering (för garantier på transaktionsnivå) eller läsa engagerad ögonblicksbildsisolering (för garantier på uttalandenivå). Observera dock att en punkt-i-tid-vy betyder att vi inte nödvändigtvis arbetar på det aktuella tillståndet för databasen; i själva verket kan vi använda inaktuell information. Å andra sidan, om vi är nöjda med resultat som endast baseras på engagerad data (om än möjligen från olika tidpunkter), kan vi välja att hålla fast vid standardnivån för låsning av läs engagerad isolering.

För att vara säkra på att producera resultat (och fatta beslut!) baserat på den senaste uppsättningen av engagerad data, för viss seriell historia av operationer mot databasen, skulle vi behöva serialiserbar transaktionsisolering. Naturligtvis är detta alternativ vanligtvis det dyraste när det gäller resursanvändning och minskad samtidighet (inklusive en ökad risk för dödlägen).

I exemplet med radräkning skulle båda isoleringsnivåerna för ögonblicksbilder (SI och RCSI) ge ett resultat på 100 rader, vilket representerar antalet bekräftade rader i början av uttalandet (och transaktionen i detta fall). Att köra frågan vid låsning av läsbekräftad eller repeterbar läsisolering kan ge resultatet av 100, 101 eller 102 rader – beroende på timing, låsgranularitet, radinfogningsposition och den fysiska åtkomstmetoden som valts. Under serialiserbar isolering skulle resultatet vara antingen 100 eller 102 rader, beroende på vilken av de två samtidiga transaktionerna som anses ha utförts först.

Hur dåligt är läsning oengagerad?

Efter att ha introducerat läs icke-engagerad isolering som den svagaste av de tillgängliga isoleringsnivåerna, bör du förvänta dig att den erbjuder ännu lägre isoleringsgarantier än låsning av read committed (näst högsta isoleringsnivå). Det gör det verkligen; men frågan är:hur mycket värre än att låsa läs engagerad isolering är det?

Så att vi börjar med rätt sammanhang, här är en lista över de huvudsakliga samtidighetseffekterna som kan upplevas under SQL Servers standardlåsning läs engagerad isoleringsnivå:

  • Begärda rader saknas
  • Rader har påträffats flera gånger
  • Olika versioner av samma rad påträffas i en enda sats/frågeplan
  • Kommitterade kolumndata från olika tidpunkter på samma rad (exempel)

Dessa samtidiga effekter beror alla på att låsningsimplementeringen av läs-committed endast tar mycket kortsiktiga delade lås vid läsning av data. Den avlästa oengagerade isoleringsnivån går ett steg längre, genom att inte ta delade lås alls, vilket resulterar i ytterligare möjlighet till "smutsiga läsningar."

Smutsiga läsningar

Som en snabb påminnelse hänvisar en "smutsig läsning" till att läsa data som ändras av en annan samtidig transaktion (där "ändring" inkluderar infogning, uppdatering, radering och sammanfogning). Med andra ord, en smutsig läsning inträffar när en transaktion läser data som en annan transaktion har ändrat, innan den modifierande transaktionen har genomfört eller avbrutit dessa ändringar.

Fördelar och nackdelar

De främsta fördelarna med läs-oengagerad isolering är den minskade potentialen för blockering och blockering på grund av inkompatibla lås (inklusive onödig blockering på grund av låseskalering), och möjligen ökad prestanda (genom att undvika behovet av att skaffa och frigöra delade lås).

Den mest uppenbara potentiella nackdelen med läs oengagerad isolering är (som namnet antyder) att vi kan läsa oengagerad data (även data som aldrig är begått, i fallet med en återställning av transaktionen). I en databas där återställningar är relativt sällsynta, kan frågan om att läsa oengagerad data ses som en ren tidsfråga, eftersom den aktuella informationen säkert kommer att begås i något skede, och förmodligen ganska snart. Vi har redan sett timingrelaterade inkonsekvenser i exemplet med radräkning (som fungerade på en högre isoleringsnivå) så man kan mycket väl fråga sig hur mycket av en oro det är att läsa data "för tidigt."

Svaret beror helt klart på lokala prioriteringar och sammanhang, men ett välgrundat beslut att använda läs oengagerad isolering verkar verkligen möjligt. Det finns dock mer att tänka på. SQL Server-implementeringen av den läs-oengagerade isoleringsnivån inkluderar några subtila beteenden som vi måste vara medvetna om innan vi gör det "medvetna valet."

Sökning av allokeringsorder

Att använda läs-oengagerad isolering tas av SQL Server som en signal om att vi är beredda att acceptera de inkonsekvenser som kan uppstå som ett resultat av en tilldelningsordnad skanning.

Vanligtvis kan lagringsmotorn bara välja en tilldelningsordnad skanning om den underliggande informationen garanterat inte ändras under genomsökningen (eftersom till exempel databasen är skrivskyddad eller en tabelllåsningstips angavs). Men när läs oengagerad isolering används, kan lagringsmotorn fortfarande välja en allokeringsordnad skanning även där underliggande data kan modifieras av samtidiga transaktioner.

Under dessa omständigheter kan den allokeringsbeställda skanningen missa vissa engagerade data helt, eller stöta på andra engagerade data mer än en gång. Tonvikten där ligger på att sakna eller dubbelräkna begärda data (ej att läsa oengagerad data) så det är inte ett fall av "dirty reads" som sådan. Detta designbeslut (att tillåta allokeringsbeställda skanningar under läs oengagerad isolering) ses av vissa människor som ganska kontroversiellt.

Som en varning bör jag vara tydlig med att den mer generella risken för att missa eller dubbelräkna begångna rader inte är begränsad till att läsa oengagerad isolering. Det är säkert möjligt att se liknande effekter under låsning av läs committed och repeterbar läsning (som vi såg tidigare) men detta sker via en annan mekanism. Begärda rader saknas eller stöter på dem flera gånger på grund av en tilldelningsordnad skanning över ändring av data är specifik för att använda läs oengagerad isolering.

Läser "korrupta" data

Resultat som tycks trotsa logik (och till och med kontrollera begränsningar!) är möjliga under låsning av läs engagerad isolering (igen, se den här artikeln av Craig Freedman för några exempel). För att sammanfatta, poängen är att låsning av läs committed kan se committed data från olika tidpunkter – även för en enskild rad om till exempel frågeplanen använder tekniker som index intersection.

Dessa resultat kan vara oväntade, men de är helt i linje med garantin att endast läsa engagerad data. Det går bara inte att komma ifrån det faktum att högre datakonsistensgarantier kräver högre isoleringsnivåer.

Dessa exempel kan till och med vara ganska chockerande, om du inte har sett dem tidigare. Samma resultat är naturligtvis möjliga under läs oengagerad isolering, men att tillåta smutsiga läsningar ger en extra dimension:resultaten kan inkludera engagerade och oengagerade data från olika tidpunkter, även för samma rad.

Om man går längre, är det till och med möjligt för en läs ej engagerad transaktion att läsa ett värde i en kolumn i ett blandat tillstånd av engagerad och oengagerad data. Detta kan inträffa när du läser ett LOB-värde (till exempel xml eller någon av "max"-typerna) om värdet lagras på flera datasidor. En oengagerad läsning kan stöta på engagerad eller oengagerad data från olika tidpunkter på olika sidor, vilket resulterar i ett slutligt enkolumnvärde som är en blandning av värden!

För att ta ett exempel, överväg en enda varchar(max) kolumn som initialt innehåller 10 000 "x" tecken. En samtidig transaktion uppdaterar detta värde till 10 000 "y"-tecken. En läs oengagerad transaktion kan läsa 'x'-tecken från en sida i LOB, och 'y'-tecken från en annan, vilket resulterar i ett slutgiltigt läsvärde som innehåller en blandning av 'x'- och 'y'-tecken. Det är svårt att hävda att detta inte representerar läsning av "korrupta" data.

Demo

Skapa en klustrad tabell med en enda rad med LOB-data:

CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    LOB varchar(max) NOT NULL,
);
 
INSERT dbo.Test
    (RowID, LOB)
VALUES
    (1, REPLICATE(CONVERT(varchar(max), 'X'), 16100));

Kör följande skript i en separat session för att läsa LOB-värdet vid läs oengagerad isolering:

-- Run this in session 2
SET NOCOUNT ON;
 
DECLARE 
    @ValueRead varchar(max) = '',
    @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100),
    @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100);
 
WHILE 1 = 1
BEGIN
    SELECT @ValueRead = T.LOB
    FROM dbo.Test AS T WITH (READUNCOMMITTED)
    WHERE T.RowID = 1;
 
    IF @ValueRead NOT IN (@AllXs, @AllYs)
    BEGIN
    	PRINT LEFT(@ValueRead, 8000);
        PRINT RIGHT(@ValueRead, 8000);
        BREAK;
    END
END;

I den första sessionen kör du det här skriptet för att skriva alternerande värden till LOB-kolumnen:

-- Run this in session 1
SET NOCOUNT ON;
 
DECLARE 
    @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100),
    @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100);
 
WHILE 1 = 1
BEGIN
    UPDATE dbo.Test
    SET LOB = @AllYs
    WHERE RowID = 1;
 
    UPDATE dbo.Test
    SET LOB = @AllXs
    WHERE RowID = 1;
END;

Efter en kort tid kommer skriptet i session två att avslutas, efter att ha läst ett blandat tillstånd för LOB-värdet, till exempel:

Det här specifika problemet är begränsat till läsningar av LOB-kolumnvärden som är spridda över flera sidor, inte på grund av några garantier som tillhandahålls av isoleringsnivån, utan på grund av att SQL Server råkar använda sidnivålås för att säkerställa fysisk integritet. En bieffekt av denna implementeringsdetalj är att den förhindrar sådan "korrupt" dataläsning om data för en enda läsoperation råkar finnas på en enda sida.

Beroende på vilken version av SQL Server du har, om "blandat tillstånd"-data läses för en xml-kolumn, kommer du antingen att få ett fel som är ett resultat av det eventuellt missbildade xml-resultatet, inget fel alls eller det oengagerade specifika felet 601 , "kunde inte fortsätta skanna med NOLOCK på grund av datarörelse." Att läsa blandade tillståndsdata för andra LOB-typer resulterar i allmänhet inte i ett felmeddelande; den konsumerande applikationen eller frågan har inget sätt att veta att den just har upplevt den värsta sortens smutsiga läsning. För att slutföra analysen rapporteras aldrig en rad med blandat tillstånd som inte är LOB som läses som ett resultat av en indexkorsning som ett fel.

Budskapet här är att om du använder läs oengagerad isolering accepterar du att smutsiga läsningar inkluderar möjligheten att läsa "korrupta" LOB-värden med blandade tillstånd.

NOLOCK-tipset

Jag antar att ingen diskussion om den lästa oengagerade isoleringsnivån skulle vara komplett utan att åtminstone nämna detta (överanvända och missförstådda) tabelltips. Själva tipset är bara en synonym till tabelltipset READUNCOMMITTED. Den utför exakt samma funktion:objektet som den appliceras på nås med läs oengagerad isoleringssemantik (även om det finns ett undantag).

När det gäller namnet "NOLOCK" betyder det helt enkelt att inga delade lås tas vid läsning av data . Andra lås (schemastabilitet, exklusiva lås för datamodifiering och så vidare) tas fortfarande som normalt.

Generellt sett bör NOLOCK-tips vara ungefär lika vanliga som andra tabelltips på isoleringsnivå per objekt som SERIALIZABLE och READCOMMITTEDLOCK. Det vill säga:inte särskilt vanligt alls, och används endast där det inte finns något bra alternativ, ett väldefinierat syfte med det och en komplett förståelse för konsekvenserna.

Ett exempel på en legitim användning av NOLOCK (eller READUNCOMMITTED) är vid åtkomst till DMV:er eller andra systemvyer, där en högre isoleringsnivå kan orsaka oönskade konflikter på icke-användardatastrukturer. Ett annat edge-case-exempel kan vara där en fråga behöver komma åt en betydande del av en stor tabell, vilket garanterat aldrig kommer att uppleva dataändringar medan den antydda frågan körs. Det skulle behöva finnas en bra anledning att inte använda ögonblicksbild eller läsa engagerad ögonblicksbildsisolering istället, och de förväntade prestandaökningarna skulle behöva testas, valideras och jämföras med, till exempel, med hjälp av en enda delad tabelllåstips.

Den minst önskvärda användningen av NOLOCK är den som tyvärr är vanligast:att tillämpa den på varje objekt i en fråga som en sorts go-faster magic switch. Med den bästa viljan i världen finns det bara inget bättre sätt att få SQL Server-kod att se helt amatörmässigt ut. Om du legitimt behöver läsa oengagerad isolering för en fråga, kodblock eller modul, är det förmodligen bättre att ställa in sessionsisoleringsnivån på lämpligt sätt och ge kommentarer för att motivera åtgärden.

Sluta tankar

Läs oengagerad är ett legitimt val för transaktionsisoleringsnivå, men det måste vara ett välgrundat val. Som en påminnelse, här är några av de samtidighetsfenomen som är möjliga under SQL Servers standardlåsning läs engagerad isolering:

  • Det saknas tidigare bekräftade rader
  • Bekräftade rader har påträffats flera gånger
  • Olika engagerade versioner av samma rad påträffas i ett enda uttalande/frågeplan
  • Bekräftad data från olika tidpunkter på samma rad (men olika kolumner)
  • Bekräftade dataläsningar som verkar motsäga aktiverade och kontrollerade begränsningar

Beroende på din synvinkel kan det vara en ganska chockerande lista över möjliga inkonsekvenser för standardisoleringsnivån. Till den listan, läs oengagerad isolering lägger till:

  • Smutsiga läsningar (påträffar data som ännu inte har begåtts, och kanske aldrig kommer att göras)
  • Rader som innehåller en blandning av engagerad och oengagerad data
  • Missade/duplicerade rader på grund av tilldelningsordnade skanningar
  • Blandat tillstånd ("korrupt") individuella (enkolumn) LOB-värden
  • Fel 601 – "det gick inte att fortsätta skanna med NOLOCK på grund av datarörelse" (exempel).

Om dina primära transaktionsbekymmer handlar om bieffekterna av att låsa läsbetingad isolering – blockering, låsning av overhead, minskad samtidighet på grund av låseskalering och så vidare – kan du vara bättre betjänt av en radversionsisoleringsnivå som isolering av läs engagerad ögonblicksbild (RCSI) eller ögonblicksbildsisolering (SI). Dessa är dock inte gratis, och särskilt uppdateringar under RCSI har en del kontraintuitiva beteenden.

För scenarier som kräver de allra högsta nivåerna av konsistensgarantier förblir serialiserbar det enda säkra valet. För prestandakritiska operationer på skrivskyddad data (till exempel stora databaser som i praktiken är skrivskyddade mellan ETL-fönster) kan det också vara ett bra val att explicit ställa in databasen till READ_ONLY (delade lås används inte när databasen är skrivskyddad, och det finns ingen risk för inkonsekvens).

Det kommer också att finnas ett relativt litet antal applikationer där läs oengagerad isolering är det rätta valet. Dessa applikationer måste vara nöjda med ungefärliga resultat och möjligheten till ibland inkonsekventa, uppenbarligen ogiltiga (när det gäller begränsningar) eller "förmodligen korrupta" data. Om data ändras relativt sällan är risken för dessa inkonsekvenser också lägre.

[ Se indexet för hela serien ]


  1. Ta bort eller trimma första eller sista tecknen i MySQL-databasen med SQL

  2. FROM_UNIXTIME() Exempel – MySQL

  3. Varning:mysqli_connect():(HY000/1045):Åtkomst nekad för användaren 'användarnamn'@'localhost' (med lösenord:YES)

  4. Oracle PL/SQL - tips för omedelbar utskrift / konsolutskrift