sql >> Databáze >  >> RDS >> Database

Přečtěte si Committed Snapshot Isolation

[ Viz rejstřík pro celou sérii ]

SQL Server poskytuje dvě fyzické implementace čtení potvrzeno úroveň izolace definovaná standardem SQL, zamykání izolace snímku potvrzeného čtení a potvrzení (RCSI ). Zatímco obě implementace splňují požadavky stanovené ve standardu SQL pro izolační chování při čtení, RCSI má zcela odlišné fyzické chování od implementace zamykání, na kterou jsme se podívali v předchozím příspěvku v této sérii.

Logické záruky

Standard SQL vyžaduje, aby transakce fungující na úrovni izolace potvrzeného čtení nezaznamenala žádné nečisté čtení. Dalším způsobem, jak vyjádřit tento požadavek, je říci, že transakce potvrzená čtením musí narazit pouze na potvrzená data .

Standard také říká, že číst potvrzené transakce mohou zažijte jevy souběžnosti známé jako neopakovatelné čtení a fantomy (ačkoli se to ve skutečnosti nevyžaduje). Jak se stává, u obou fyzických implementací izolace potvrzeného čtení na serveru SQL Server může docházet k neopakovatelným čtením a fiktivním řádkům, ačkoli přesné detaily jsou zcela odlišné.

Pohled na potvrzená data v určitém okamžiku

Pokud je volba databáze READ_COMMITTED_SNAPSHOT v ON SQL Server používá implementaci verzování řádků úrovně izolace potvrzené čtením. Když je toto povoleno, transakce požadující izolaci potvrzeného čtení automaticky používají implementaci RCSI; pro použití RCSI nejsou nutné žádné změny stávajícího kódu T-SQL. Pozorně si však všimněte, že to není to totéž že kód se bude chovat stejně pod RCSI jako při použití zamykací implementace čtení potvrzeno, ve skutečnosti to docela obecně není tento případ .

Ve standardu SQL není nic, co by vyžadovalo, aby data načtená transakcí potvrzenou čtením byla nejnovější potvrzená data. Implementace SQL Server RCSI tuto výhodu využívá k poskytování transakcí s časovým zobrazením potvrzených dat, kde tento okamžik je okamžikem zahájení aktuálního příkazu provedení (nikoli v okamžiku zahájení jakékoli obsahující transakce).

To je zcela odlišné od chování implementace zamykání SQL Serveru při čtení potvrzené, kde příkaz vidí nejnovější potvrzená data k okamžiku fyzického čtení každé položky . Uzamčením potvrzeného čtení uvolníte sdílené zámky tak rychle, jak je to možné, takže soubor dat, se kterými se setkáte, může pocházet z velmi odlišných časových okamžiků.

Abychom to shrnuli, uzamčení potvrzeného čtení vidí každý řádek tak, jak to bylo v té době, bylo krátce uzamčeno a fyzicky přečteno; RCSI vidí všechny řádky jak byli v době, kdy prohlášení začalo. U obou implementací je zaručeno, že nikdy neuvidí nepřijatá data, ale data, se kterými se setkají, se mohou velmi lišit.

Důsledky pohledu v určitém okamžiku

Zobrazení aktuálního pohledu na potvrzená data se může zdát evidentně lepší než složitější chování implementace zamykání. Je například jasné, že pohled v určitém okamžiku nemůže trpět problémy s chybějícími řádky nebo narazíte na stejný řádek vícekrát , které jsou obě možné pod uzamčením izolace potvrzené čtením.

Druhou důležitou výhodou RCSI je to, že nezíská sdílené zámky při čtení dat, protože data pocházejí z úložiště verzí řádků, nikoli přímo. Absence sdílených zámků může výrazně zlepšit souběžnost odstraněním konfliktů se souběžnými transakcemi, které chtějí získat nekompatibilní zámky. Tato výhoda se běžně shrnuje tím, že čtenáři neblokují autory pod RCSI a naopak. Dalším důsledkem omezení blokování kvůli nekompatibilním požadavkům na zámek je možnost zablokování je obvykle výrazně snížena, když běží pod RCSI.

Tyto výhody se však neobejdou bez nákladů a upozornění . Za prvé, údržba verzí potvrzených řádků spotřebovává systémové prostředky, takže je důležité, aby fyzické prostředí bylo nakonfigurováno tak, aby se s tím vyrovnalo, především z hlediska tempdb požadavky na výkon a paměť/místo na disku.

Druhé upozornění je o něco jemnější:RCSI poskytuje snímek potvrzených dat tak jak to bylo na začátku příkazu, ale nic nebrání změně skutečných dat (a potvrzení těchto změn) během provádění příkazu RCSI. Neexistují žádné sdílené zámky, pamatujte. Bezprostředním důsledkem tohoto druhého bodu je, že kód T-SQL běžící pod RCSI může rozhodovat se na základě zastaralých informací ve srovnání s aktuálním potvrzeným stavem databáze. Brzy si o tom promluvíme více.

Než pokročíme dále, je tu ještě jeden poslední postřeh (specifický pro implementaci), který bych chtěl o RCSI učinit. Skalární funkce a funkce s více příkazy spustit pomocí jiného vnitřního kontextu T-SQL než obsahuje příkaz. To znamená, že pohled k určitému okamžiku viděný uvnitř vyvolání skalární nebo vícepříkazové funkce může být pozdější než pohled k určitému okamžiku viděný zbytkem příkazu. To může vést k neočekávaným nesrovnalostem, protože různé části stejného prohlášení zobrazují data z různých časových okamžiků . Toto podivné a matoucí chování ne použít na funkce v řádku, které vidí stejný snímek jako příkaz, ve kterém se objevují.

Neopakovatelné čtení a fantomy

Vzhledem k tomu, že se na úrovni příkazu zobrazí bod v čase potvrzeného stavu databáze, nemusí být okamžitě zřejmé, jak může transakce potvrzení čtení pod RCSI zaznamenat neopakovatelné čtení nebo fenomén fiktivního řádku. Pokud totiž své myšlení omezíme na rozsah jediného prohlášení , žádný z těchto jevů není pod RCSI možný.

Čtení stejných dat vícekrát v rámci stejného výpisu pod RCSI vrátí vždy stejné hodnoty dat, žádná data mezi těmito čteními nezmizí a neobjeví se ani žádná nová data. Pokud vás zajímá, jaký typ příkazu může číst stejná data více než jednou, zamyslete se nad dotazy, které odkazují na stejnou tabulku více než jednou, třeba v poddotazu.

Konzistence čtení na úrovni příkazů je zřejmým důsledkem toho, že jsou čtení vydávána proti pevnému snímku dat. Důvod, proč RCSI ne zajistit ochranu před neopakovatelným čtením a fantomy je, že tyto standardní jevy SQL jsou definovány na úrovni transakce. Více příkazů v rámci transakce spuštěné na RCSI může zobrazit různá data, protože každý příkaz vidí časový okamžik k okamžiku toho konkrétního příkazu začalo.

Abych to shrnul, každé prohlášení v rámci transakce RCSI vidí sadu statických potvrzených dat, ale tato sada se může mezi příkazy v rámci stejné transakce měnit.

Zastaralá data

Možnost, že náš T-SQL kód učiní důležité rozhodnutí na základě zastaralých informací, je více než trochu znepokojující. Zvažte na chvíli, že snímek k určitému okamžiku používaný jedním příkazem spuštěným pod RCSI může být libovolně starý .

U příkazu, který běží po značnou dobu, se bude nadále zobrazovat potvrzený stav databáze jako při zahájení příkazu. Mezitím v příkazu chybí všechny potvrzené změny, ke kterým od té doby v databázi došlo.

To neznamená, že problémy spojené s přístupem k zastaralým datům v rámci RCSI jsou omezeny na dlouhotrvající prohlášení, ale problémy mohou být v takových případech jistě výraznější.

Otázka načasování

Tento problém s neaktuálními údaji se v zásadě týká všech příkazů RCSI, bez ohledu na to, jak rychle mohou být dokončeny. Jakkoli je časové okno malé, vždy existuje možnost, že souběžná operace může změnit soubor dat, se kterým pracujeme, aniž bychom si byli této změny vědomi. Podívejme se znovu na jeden z jednoduchých příkladů, které jsme použili dříve, když jsme zkoumali chování zamykání čtení potvrzeno:

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

Při spuštění pod RCSI tento příkaz nemůže zobrazit všechny potvrzené úpravy databáze, ke kterým dojde poté, co se příkaz spustí. I když se při implementaci zamykání nesetkáme s problémy zmeškaných nebo vícenásobných řádků, může souběžná transakce přidat platbu, která by měla abyste zabránili tomu, aby byl zákazníkovi zaslán strohý varovný dopis o prodlení s platbou poté, co se začne provádět výše uvedený výpis.

Pravděpodobně vás napadne mnoho dalších potenciálních problémů, které by mohly nastat v tomto scénáři nebo v jiných, které jsou koncepčně podobné. Čím déle příkaz běží, tím zastaralejší je jeho pohled na databázi a tím větší je prostor pro možná nezamýšlené důsledky.

V tomto konkrétním příkladu je samozřejmě spousta polehčujících faktorů. Chování může být považováno za naprosto přijatelné. Odeslání upomínkového dopisu, protože platba dorazila o několik sekund později, je totiž snadno obhajitelná akce. Princip však zůstává.

Selhání obchodních pravidel a rizika integrity

Závažnější problémy mohou nastat při použití zastaralých informací, než je zaslání varovného dopisu o několik sekund dříve. Dobrým příkladem této třídy slabosti je spouštěcí kód používá se k vynucení pravidla integrity, které je možná příliš složité na vynucení s deklarativními omezeními referenční integrity. Pro ilustraci zvažte následující kód, který používá spouštěč k vynucení variace omezení cizího klíče, ale takový, který vynucuje vztah pouze pro určité řádky podřízené tabulky:

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

Nyní zvažte transakci spuštěnou v jiné relaci (použijte k tomu jiné okno SSMS, pokud ji sledujete), která odstraní nadřazený řádek #1, ale ještě se nepotvrdí:

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

Zpět v naší původní relaci se snažíme vložit (zaškrtnutý) podřízený řádek, který odkazuje na tohoto rodiče:

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

Spouštěcí kód se spustí, ale protože RCSI vidí pouze committed data od okamžiku spuštění příkazu, stále vidí nadřazený řádek (nikoli nepotvrzené odstranění) a vložení je úspěšné !

Transakce, která odstranila nadřazený řádek, nyní může úspěšně potvrdit svou změnu, takže databáze zůstane nekonzistentní stav z hlediska naší spouštěcí logiky:

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

Toto je samozřejmě zjednodušený příklad, který lze snadno obejít pomocí vestavěných omezovacích zařízení. Mnohem složitější obchodní pravidla a omezení pseudointegrity lze napsat uvnitř i mimo spouštěče . Potenciál pro nesprávné chování v rámci RCSI by měl být zřejmý.

Chování při blokování a nejnovější přijatá data

Již dříve jsem zmínil, že u kódu T-SQL není zaručeno, že se bude chovat stejným způsobem pod RCSI read commited, jako se choval při použití implementace zamykání. Předchozí příklad spouštěcího kódu je toho dobrým příkladem, ale musím zdůraznit, že obecný problém se neomezuje na spouštěče .

RCSI obvykle není dobrou volbou pro jakýkoli kód T-SQL, jehož správnost závisí na blokování, pokud existuje souběžná nepotvrzená změna. RCSI také nemusí být správnou volbou, pokud kód závisí na čtení aktuálního potvrzená data, spíše než nejnovější potvrzená data v době zahájení příkazu. Tyto dvě úvahy spolu souvisí, ale nejsou totéž.

Zamykání čtení potvrzené pod RCSI

SQL Server poskytuje jeden způsob, jak požádat o uzamčení čtení potvrzeno, když je povoleno RCSI, pomocí nápovědy tabulky READCOMMITTEDLOCK . Můžeme upravit náš spouštěč, abychom se vyhnuli problémům uvedeným výše, přidáním této nápovědy do tabulky, která ke správnému fungování vyžaduje blokovací chování:

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;

S touto změnou se pokus o vložení potenciálně osamocených podřízených řádků blokuje, dokud se transakce odstranění nepotvrdí (nebo se nezruší). Pokud se odstranění potvrdí, spouštěcí kód detekuje narušení integrity a vyvolá očekávanou chybu.

Identifikace dotazů, které nemusí fungovat správně v rámci RCSI je netriviální úkol, který může vyžadovat rozsáhlé testování správně (a nezapomeňte, že tyto problémy jsou docela obecné a neomezují se na spouštěcí kód!) Také přidáním READCOMMITTEDLOCK nápověda ke každému stolu, který to potřebuje, může být zdlouhavý a náchylný k chybám. Dokud SQL Server neposkytne širší možnost požádat o implementaci zamykání tam, kde je to potřeba, zůstaneme u používání tabulkových rad.

Příště

Další příspěvek v této sérii pokračuje v našem zkoumání izolace snímku potvrzeného čtení s pohledem na překvapivé chování příkazů modifikace dat pod RCSI.

[ Viz rejstřík pro celou sérii ]


  1. Úvod do zabezpečení na úrovni řádků v SQL Server

  2. Výhody PostgreSQL

  3. Příklady DATEFROMPARTS() v SQL Server (T-SQL)

  4. Jak povolit vzdálený přístup k databázi PostgreSQL