sql >> Database >  >> RDS >> Database

Het niet-vastgelegde isolatieniveau lezen

[ Zie de index voor de hele serie ]

Read uncommitted is de zwakste van de vier transactie-isolatieniveaus die zijn gedefinieerd in de SQL-standaard (en van de zes geïmplementeerd in SQL Server). Het staat alle drie de zogenaamde "gelijktijdigheidsverschijnselen" toe, dirty reads , niet-herhaalbare leesbewerkingen , en fantomen:

De meeste databasemensen zijn zich bewust van deze verschijnselen, althans in hoofdlijnen, maar niet iedereen realiseert zich dat ze de geboden isolatiegaranties niet volledig beschrijven; evenmin beschrijven ze intuïtief de verschillende gedragingen die men kan verwachten in een specifieke implementatie zoals SQL Server. Daarover later meer.

Transactie-isolatie – de 'I' in ACID

Elke SQL-opdracht wordt uitgevoerd binnen een transactie (expliciet, impliciet of automatisch vastgelegd). Elke transactie heeft een bijbehorend isolatieniveau, dat bepaalt hoe geïsoleerd het is van de effecten van andere gelijktijdige transacties. Dit enigszins technische concept heeft belangrijke implicaties voor de manier waarop zoekopdrachten worden uitgevoerd en de kwaliteit van de resultaten die ze opleveren.

Overweeg een eenvoudige query die alle rijen in een tabel telt. Als deze query onmiddellijk zou kunnen worden uitgevoerd (of zonder gelijktijdige gegevenswijzigingen), zou er maar één correct antwoord kunnen zijn:het aantal rijen dat op dat moment fysiek in de tabel aanwezig is. In werkelijkheid zal het uitvoeren van de query enige tijd in beslag nemen, en het resultaat hangt af van het aantal rijen dat de uitvoeringsengine daadwerkelijk tegenkomt terwijl het de fysieke structuur doorloopt die is gekozen om toegang te krijgen tot de gegevens.

Als rijen worden toegevoegd aan (of verwijderd uit) de tabel door gelijktijdige transacties terwijl de telbewerking aan de gang is, kunnen verschillende resultaten worden verkregen, afhankelijk van of de rij-teltransactie alle, sommige of geen van die gelijktijdige wijzigingen tegenkomt - wat hangt op zijn beurt af van het isolatieniveau van de transactie voor het tellen van rijen.

Afhankelijk van het isolatieniveau, de fysieke details en de timing van de gelijktijdige bewerkingen, kan onze teltransactie zelfs een resultaat opleveren dat op geen enkel moment tijdens de transactie een waarheidsgetrouwe weerspiegeling was van de vastgelegde status van de tabel.

Voorbeeld

Overweeg een transactie voor het tellen van rijen die begint op tijdstip T1 en de tabel van begin tot eind scant (in geclusterde volgorde van de indexsleutel, omwille van het argument). Op dat moment zijn er 100 vastgelegde rijen in de tabel. Enige tijd later (op tijdstip T2) is onze teltransactie 50 van die rijen tegengekomen. Op hetzelfde moment voegt een gelijktijdige transactie twee rijen toe aan de tabel en wordt korte tijd later vastgelegd op tijdstip T3 (voordat de teltransactie eindigt). Een van de ingevoegde rijen valt toevallig binnen de helft van de geclusterde indexstructuur die onze teltransactie al heeft verwerkt, terwijl de andere ingevoegde rij zich in het niet-getelde gedeelte bevindt.

Wanneer de transactie voor het tellen van rijen is voltooid, worden in dit scenario 101 rijen gerapporteerd; 100 rijen aanvankelijk in de tabel plus de enkele ingevoegde rij die tijdens de scan is aangetroffen. Dit resultaat staat haaks op de vastgelegde geschiedenis van de tabel:er waren 100 vastgelegde rijen op tijdstip T1 en T2, daarna 102 vastgelegde rijen op tijdstip T3. Er was nooit een tijd dat er 101 gecommitteerde rijen waren.

Het verrassende (misschien, afhankelijk van hoe diep je eerder over deze dingen hebt nagedacht) is dat dit resultaat mogelijk is op het standaard (vergrendelende) leescommitteerde isolatieniveau, en zelfs onder herhaalbare leesisolatie. Beide isolatieniveaus lezen gegarandeerd alleen vastgelegde gegevens, maar we hebben een resultaat verkregen dat geen vastgelegde status van de database vertegenwoordigt!

Analyse

Het enige transactie-isolatieniveau dat volledige isolatie van gelijktijdigheidseffecten biedt, is serialiseerbaar. De SQL Server-implementatie van het serialiseerbare isolatieniveau betekent dat een transactie de laatste vastgelegde gegevens zal zien vanaf het moment dat de gegevens voor het eerst werden vergrendeld voor toegang. Bovendien wordt gegarandeerd dat de set gegevens die wordt aangetroffen onder serialiseerbare isolatie niet van lidmaatschap verandert voordat de transactie eindigt.

Het voorbeeld van het tellen van rijen benadrukt een fundamenteel aspect van de databasetheorie:we moeten duidelijk zijn over wat een "juist" resultaat betekent voor een database die gelijktijdige wijzigingen ondergaat, en we moeten de afwegingen begrijpen die we maken bij het selecteren van een isolatie niveau lager dan serialiseerbaar.

Als we een momentopname van de vastgelegde status van de database nodig hebben, moeten we snapshot-isolatie gebruiken (voor garanties op transactieniveau) of toegewijde snapshot-isolatie lezen (voor garanties op statement-niveau). Merk echter op dat een point-in-time weergave betekent dat we niet noodzakelijkerwijs werken met de huidige vastgelegde status van de database; in feite kunnen we verouderde informatie gebruiken. Aan de andere kant, als we tevreden zijn met resultaten die alleen zijn gebaseerd op vastgelegde gegevens (zij het mogelijk van verschillende tijdstippen), kunnen we ervoor kiezen om vast te houden aan het standaard vergrendelingsniveau voor vastgelegde vastgelegde isolatie.

Om er zeker van te zijn dat we resultaten produceren (en beslissingen nemen!) op basis van de laatste set vastgelegde gegevens, voor een bepaalde seriële geschiedenis van bewerkingen tegen de database, zouden we serialiseerbare transactie-isolatie nodig hebben. Natuurlijk is deze optie doorgaans de duurste in termen van gebruik van hulpbronnen en verminderde gelijktijdigheid (inclusief een verhoogd risico op impasses).

In het voorbeeld van het tellen van rijen zouden beide snapshot-isolatieniveaus (SI en RCSI) een resultaat van 100 rijen opleveren, wat het aantal vastgelegde rijen aan het begin van de instructie (en in dit geval de transactie) vertegenwoordigt. Het uitvoeren van de query bij het vergrendelen van leescommitte of herhaalbare leesisolatie kan een resultaat van 100, 101 of 102 rijen opleveren, afhankelijk van timing, granulariteit van het slot, rij-invoegpositie en de gekozen fysieke toegangsmethode. Bij serialiseerbare isolatie zou het resultaat 100 of 102 rijen zijn, afhankelijk van welke van de twee gelijktijdige transacties als eerste wordt uitgevoerd.

Hoe slecht is Read Uncommitted?

Na het introduceren van read uncommitted isolation als de zwakste van de beschikbare isolatieniveaus, zou je moeten verwachten dat het nog lagere isolatiegaranties biedt dan locking read commits (het op één na hoogste isolatieniveau). Dat doet het inderdaad; maar de vraag is:hoeveel erger dan het vergrendelen van read-committed isolation is het?

Om te beginnen met de juiste context, volgt hier een lijst van de belangrijkste gelijktijdigheidseffecten die kunnen worden ervaren onder het standaard vergrendelingsniveau van SQL Server voor leescommittatie:

  • Gecommitteerde rijen ontbreken
  • Rijen die meerdere keren zijn aangetroffen
  • Verschillende versies van dezelfde rij aangetroffen in een enkel instructie-/queryplan
  • Toegewezen kolomgegevens van verschillende tijdstippen in dezelfde rij (voorbeeld)

Deze gelijktijdigheidseffecten zijn allemaal te wijten aan de vergrendelingsimplementatie van read-commit die alleen zeer korte termijn gedeelde vergrendelingen neemt bij het lezen van gegevens. Het niet-gecommitteerde isolatieniveau voor lezen gaat nog een stap verder, door helemaal geen gedeelde sloten te gebruiken, wat resulteert in de extra mogelijkheid van "dirty reads".

Vuile teksten

Ter herinnering:een "vuile lezing" verwijst naar het lezen van gegevens die worden gewijzigd door een andere gelijktijdige transactie (waarbij "wijzigen" bewerkingen voor invoegen, bijwerken, verwijderen en samenvoegen omvat). Anders gezegd, een vuile lezing vindt plaats wanneer een transactie gegevens leest die een andere transactie heeft gewijzigd, voordat de wijzigende transactie die wijzigingen heeft doorgevoerd of afgebroken.

Voor- en nadelen

De belangrijkste voordelen van niet-gecommitteerde isolatie lezen zijn het verminderde potentieel voor blokkering en deadlocking vanwege incompatibele vergrendelingen (inclusief onnodige blokkering als gevolg van escalatie van vergrendelingen) en mogelijk betere prestaties (door de noodzaak om gedeelde vergrendelingen te verwerven en vrij te geven te vermijden).

Het meest voor de hand liggende potentiële nadeel van het lezen van niet-vastgelegde isolatie is (zoals de naam al doet vermoeden) dat we niet-vastgelegde gegevens kunnen lezen (zelfs gegevens die nooit vastgelegd, in het geval van een terugdraaiing van een transactie). In een database waar terugdraaiingen relatief zeldzaam zijn, kan de kwestie van het lezen van niet-vastgelegde gegevens worden gezien als louter een timingprobleem, aangezien de gegevens in kwestie zeker op een bepaald moment, en waarschijnlijk vrij snel, zullen worden vastgelegd. We hebben al timinggerelateerde inconsistenties gezien in het voorbeeld van het tellen van rijen (dat op een hoger isolatieniveau werkte), dus je kunt je afvragen hoeveel zorg het is om gegevens "te snel" te lezen.

Het antwoord hangt duidelijk af van lokale prioriteiten en context, maar een weloverwogen beslissing om read uncommitted isolation te gebruiken lijkt zeker mogelijk. Er is echter meer om over na te denken. De SQL Server-implementatie van het lees-niet-vastgelegde isolatieniveau omvat een aantal subtiele gedragingen waarvan we op de hoogte moeten zijn voordat we die "geïnformeerde keuze" maken.

Toewijzingsorderscans

Het gebruik van read uncommitted isolation wordt door SQL Server opgevat als een signaal dat we bereid zijn de inconsistenties te accepteren die kunnen ontstaan ​​als gevolg van een scan op basis van toewijzing.

Gewoonlijk kan de storage-engine alleen een scan op basis van toewijzing kiezen als de onderliggende gegevens gegarandeerd niet veranderen tijdens de scan (omdat de database bijvoorbeeld alleen-lezen is of omdat er een hint voor het vergrendelen van een tabel is opgegeven). Wanneer lees-niet-vastgelegde isolatie in gebruik is, kan de opslagengine echter nog steeds een scan op basis van toewijzing kiezen, zelfs als de onderliggende gegevens kunnen worden gewijzigd door gelijktijdige transacties.

In deze omstandigheden kan de op toewijzing geordende scan sommige vastgelegde gegevens volledig missen, of andere vastgelegde gegevens meer dan eens tegenkomen. De nadruk ligt daarbij op ontbrekende of dubbeltellingen toegewijd data (geen niet-vastgelegde data lezen) dus van "dirty reads" als zodanig is geen sprake. Deze ontwerpbeslissing (om scans op basis van toewijzing toe te staan ​​onder gelezen niet-gecommitteerde isolatie) wordt door sommige mensen als nogal controversieel beschouwd.

Als waarschuwing moet ik duidelijk zijn dat het meer algemene risico van het missen of dubbel tellen van vastgelegde rijen niet beperkt is tot het lezen van niet-vastgelegde isolatie. Het is zeker mogelijk om vergelijkbare effecten te zien bij locking read commits en repeatable reads (zoals we eerder zagen), maar dit gebeurt via een ander mechanisme. Gecommitteerde rijen ontbreken of komen ze meerdere keren tegen vanwege een toewijzing-geordende scan over veranderende gegevens is specifiek voor het gebruik van read uncommitted isolation.

Lezen van "corrupte" gegevens

Resultaten die de logica lijken te tarten (en zelfs beperkingen controleren!) zijn mogelijk onder locking read commit isolation (zie nogmaals dit artikel van Craig Freedman voor enkele voorbeelden). Om samen te vatten, het punt is dat locking read commits vastgelegde gegevens van verschillende tijdstippen kunnen zien - zelfs voor een enkele rij als bijvoorbeeld het queryplan technieken zoals index-intersectie gebruikt.

Deze resultaten kunnen onverwacht zijn, maar ze zijn volledig in lijn met de garantie om alleen vastgelegde gegevens te lezen. Er is gewoon geen ontkomen aan het feit dat hogere garanties voor gegevensconsistentie hogere isolatieniveaus vereisen.

Die voorbeelden kunnen zelfs behoorlijk schokkend zijn, als je ze nog niet eerder hebt gezien. Dezelfde resultaten zijn natuurlijk mogelijk onder niet-vastgelegde isolatie, maar het toestaan ​​van vuile leesbeurten voegt een extra dimensie toe:de resultaten kunnen vastgelegde en niet-vastgelegde gegevens van verschillende tijdstippen bevatten, zelfs voor dezelfde rij.

Verder gaand, is het zelfs mogelijk voor een gelezen niet-vastgelegde transactie om een ​​waarde van één kolom te lezen in een gemengde staat van vastgelegde en niet-vastgelegde gegevens. Dit kan gebeuren bij het lezen van een LOB-waarde (bijvoorbeeld xml of een van de 'max'-typen) als de waarde is opgeslagen op meerdere gegevenspagina's. Een niet-vastgelegde lezing kan vastgelegde of niet-vastgelegde gegevens van verschillende tijdstippen op verschillende pagina's tegenkomen, wat resulteert in een uiteindelijke waarde voor één kolom die een mengsel van waarden is!

Neem als voorbeeld een enkele varchar(max)-kolom die aanvankelijk 10.000 'x'-tekens bevat. Een gelijktijdige transactie werkt deze waarde bij tot 10.000 'y'-tekens. Een gelezen niet-vastgelegde transactie kan 'x'-tekens van de ene pagina van de LOB en 'y'-tekens van een andere lezen, wat resulteert in een uiteindelijke leeswaarde die een combinatie van 'x'- en 'y'-tekens bevat. Het is moeilijk te beweren dat dit niet staat voor het lezen van "corrupte" gegevens.

Demo

Maak een geclusterde tabel met een enkele rij LOB-gegevens:

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

Voer in een aparte sessie het volgende script uit om de LOB-waarde te lezen bij read uncommitted isolation:

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

Voer in de eerste sessie dit script uit om afwisselende waarden naar de LOB-kolom te schrijven:

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

Na een korte tijd wordt het script in sessie twee beëindigd, nadat het een gemengde status voor de LOB-waarde heeft gelezen, bijvoorbeeld:

Dit specifieke probleem is beperkt tot het lezen van LOB-kolomwaarden die over meerdere pagina's zijn verspreid, niet vanwege de garanties die worden geboden door het isolatieniveau, maar omdat SQL Server toevallig vergrendelingen op paginaniveau gebruikt om de fysieke integriteit te waarborgen. Een neveneffect van dit implementatiedetail is dat het dergelijke "corrupte" gegevenslezingen voorkomt als de gegevens voor een enkele leesbewerking zich op één enkele pagina bevinden.

Afhankelijk van de versie van SQL Server die u hebt, krijgt u, als "gemengde status"-gegevens worden gelezen voor een xml-kolom, ofwel een fout als gevolg van het mogelijk verkeerd gevormde xml-resultaat, helemaal geen fout, of de niet-toegewezen-specifieke fout 601 , "kan niet doorgaan met scannen met NOLOCK vanwege gegevensverplaatsing." Het lezen van mixed-state-gegevens voor andere LOB-typen resulteert over het algemeen niet in een foutmelding; de consumerende applicatie of query heeft geen manier om te weten dat het zojuist de ergste soort vuile lezing heeft ondergaan. Om de analyse te voltooien, wordt een niet-LOB-rij met gemengde toestanden die als resultaat van een indexkruising is gelezen, nooit als een fout gerapporteerd.

De boodschap hier is dat als je read uncommitted isolation gebruikt, je accepteert dat dirty reads de mogelijkheid bevatten om "corrupte" mixed-state LOB-waarden te lezen.

De NOLOCK-hint

Ik veronderstel dat geen enkele discussie over het gelezen niet-toegewezen isolatieniveau compleet zou zijn zonder op zijn minst deze (veel te veel gebruikte en verkeerd begrepen) tabelhint te noemen. De hint zelf is slechts een synoniem van de READUNCOMMITTED-tabelhint. Het vervult precies dezelfde functie:het object waarop het wordt toegepast, wordt benaderd met behulp van read uncommitted isolation semantics (hoewel er een uitzondering is).

Wat de naam "NOLOCK" betreft, het betekent gewoon dat geen gedeelde vergrendelingen worden genomen bij het lezen van gegevens . Andere vergrendelingen (schemastabiliteit, exclusieve vergrendelingen voor gegevenswijziging enzovoort) worden nog steeds als normaal beschouwd.

Over het algemeen zouden NOLOCK-hints ongeveer net zo gewoon moeten zijn als andere per-object isolatieniveautabelhints zoals SERIALIZABLE en READCOMMITTEDLOCK. Dat wil zeggen:helemaal niet gebruikelijk, en alleen gebruikt als er geen goed alternatief is, een duidelijk omschreven doel en een compleet begrip van de gevolgen.

Een voorbeeld van een legitiem gebruik van NOLOCK (of READUNCOMMITTED) is bij toegang tot DMV's of andere systeemweergaven, waarbij een hoger isolatieniveau ongewenste conflicten kan veroorzaken op niet-gebruikersgegevensstructuren. Een ander edge-case-voorbeeld kan zijn waar een query toegang moet hebben tot een aanzienlijk deel van een grote tabel, die gegarandeerd nooit gegevenswijzigingen zal ervaren terwijl de hinted-query wordt uitgevoerd. Er zou een goede reden moeten zijn om in plaats daarvan geen snapshot te gebruiken of gecommitteerde snapshot-isolatie te lezen, en de verwachte prestatieverbeteringen zouden moeten worden getest, gevalideerd en vergeleken met bijvoorbeeld het gebruik van een enkele hint voor een gedeelde tabelvergrendeling.

Het minst wenselijke gebruik van NOLOCK is helaas het meest voorkomende gebruik:het toepassen op elk object in een query als een soort snellere magische schakelaar. Met de beste wil van de wereld is er gewoon geen betere manier om SQL Server-code er beslist amateuristisch uit te laten zien. Als het legitiem is om niet-vastgelegde isolatie voor een query, codeblok of module te lezen, is het waarschijnlijk beter om het sessie-isolatieniveau op de juiste manier in te stellen en opmerkingen te geven om de actie te rechtvaardigen.

Laatste gedachten

Niet-vastgelegd lezen is een legitieme keuze voor het isolatieniveau van transacties, maar het moet wel een geïnformeerde keuze zijn. Ter herinnering, hier zijn enkele van de gelijktijdigheidsfenomenen die mogelijk zijn onder de standaardvergrendeling van SQL Server, lees vastgelegde isolatie:

  • Er ontbreken eerder vastgelegde rijen
  • Toegewezen rijen meerdere keren aangetroffen
  • Verschillende vastgelegde versies van dezelfde rij aangetroffen in een enkel instructie-/queryplan
  • Gecommitteerde gegevens van verschillende tijdstippen in dezelfde rij (maar verschillende kolommen)
  • Toegewezen gegevenslezingen die in tegenspraak lijken te zijn met ingeschakelde en gecontroleerde beperkingen

Afhankelijk van uw standpunt, kan dat een nogal schokkende lijst zijn van mogelijke inconsistenties voor het standaardisolatieniveau. Lees aan die lijst niet-gecommitteerde isolatie voegt toe:

  • Dirty reads (gegevens tegenkomen die nog niet zijn vastgelegd en misschien nooit zullen worden vastgelegd)
  • Rijen met een combinatie van vastgelegde en niet-vastgelegde gegevens
  • Gemiste/dubbele rijen vanwege scans op volgorde van toewijzing
  • Gemengde toestand ("corrupte") individuele LOB-waarden (één kolom)
  • Fout 601 – "kan niet doorgaan met scannen met NOLOCK vanwege gegevensverplaatsing" (voorbeeld).

Als uw primaire transactieproblemen betrekking hebben op de neveneffecten van het vergrendelen van read-committed isolation (blokkering, locking overhead, verminderde gelijktijdigheid als gevolg van lock-escalatie enzovoort), bent u wellicht beter gediend met een isolatieniveau voor rijversies, zoals read-committed snapshot-isolatie (RCSI) of snapshot-isolatie (SI). Deze zijn echter niet gratis, en vooral updates onder RCSI hebben contra-intuïtief gedrag.

Voor scenario's die de allerhoogste niveaus van consistentiegaranties vereisen, blijft serializable de enige veilige keuze. Voor prestatiekritieke bewerkingen op alleen-lezen gegevens (bijvoorbeeld grote databases die in feite alleen-lezen zijn tussen ETL-vensters), kan het expliciet instellen van de database op READ_ONLY ook een goede keuze zijn (gedeelde vergrendelingen worden niet gebruikt wanneer de database is alleen-lezen, en er is geen risico op inconsistentie).

Er zal ook een relatief klein aantal toepassingen zijn waarvoor read uncommitted isolation de juiste keuze is. Deze toepassingen moeten tevreden zijn met geschatte resultaten en de mogelijkheid van soms inconsistente, schijnbaar ongeldige (in termen van beperkingen) of "aantoonbaar corrupte" gegevens. Als gegevens relatief weinig veranderen, is het risico van deze inconsistenties ook dienovereenkomstig lager.

[ Zie de index voor de hele serie ]


  1. SQL Server AlwaysOn-beschikbaarheidsgroepen:installatie en configuratie. Deel 2

  2. Gedrag van SQL Server 2016-queryplan voor tijdelijke tabellen

  3. Generate_series in Postgres vanaf begin- en einddatum in een tabel

  4. Hoe te repareren "profielnaam is niet geldig" bij het bijwerken van een database-e-mailprofiel in SQL Server (T-SQL)