Problémy souběžnosti jsou těžké stejně jako vícevláknové programování. Pokud není použita serializovatelná izolace, může být obtížné kódovat transakce T-SQL, které budou vždy správně fungovat, když jiní uživatelé provádějí změny v databázi ve stejnou dobu.
Potenciální problémy mohou být netriviální, i když je dotyčná „transakce“ jednoduchá SELECT
prohlášení. U komplexních transakcí s více příkazy, které čtou a zapisují data, může být potenciál pro neočekávané výsledky a chyby při vysoké souběžnosti rychle ohromující. Pokusy o vyřešení jemných a obtížně reprodukovatelných problémů souběžnosti použitím náhodných zamykacích rad nebo jiných metod pokus-omyl mohou být extrémně frustrující.
V mnoha ohledech se úroveň izolace snímku jeví jako dokonalé řešení těchto problémů souběžnosti. Základní myšlenkou je, že každá transakce se snímkem se chová, jako by byla provedena proti své vlastní soukromé kopii potvrzeného stavu databáze, pořízené v okamžiku zahájení transakce. Poskytnutí celé transakce s neměnným pohledem na potvrzená data samozřejmě zaručuje konzistentní výsledky pro operace pouze pro čtení, ale co transakce, které mění data?
Snapshot isolation zpracovává změny dat optimisticky, implicitně předpokládá, že konflikty mezi souběžnými zapisovači budou relativně vzácné. Pokud dojde ke konfliktu zápisu, vyhraje první komisař a prohrané transakci budou změny vráceny zpět. Pro vrácenou transakci je to samozřejmě nešťastné, ale pokud je to dostatečně vzácný výskyt, výhody izolace snímků mohou snadno převážit náklady na příležitostné selhání a opakování.
Relativně jednoduchá a čistá sémantika izolace snímků (ve srovnání s alternativami) může být významnou výhodou zejména pro lidi, kteří nepracují výhradně ve světě databází, a proto neznají dobře různé úrovně izolace. I pro zkušené databázové profesionály může být relativně „intuitivní“ úroveň izolace vítanou úlevou.
Samozřejmě, věci jsou jen zřídka tak jednoduché, jak se na první pohled zdá, a izolace snímků není výjimkou. Oficiální dokumentace odvádí docela dobrou práci při popisu hlavních výhod a nevýhod izolace snímků, takže většina tohoto článku se soustředí na prozkoumání některých méně známých a překvapivých problémů, se kterými se můžete setkat. Nejprve však rychlý pohled na logické vlastnosti této úrovně izolace:
Vlastnosti kyselin a izolace snímku
Izolace snímků není jednou z úrovní izolace definovaných ve standardu SQL, ale stále je často porovnávána pomocí tam definovaných „fenomenů souběžnosti“. Například následující srovnávací tabulka je reprodukována z technického článku SQL Server, "SQL Server 2005 Row Versioning-Based Transaction Isolation" od Kimberly L. Tripp a Neal Graves:
Poskytnutím zobrazení v určitém okamžiku přijatých dat , izolace snímků poskytuje ochranu proti všem třem tam uvedeným jevům souběžnosti. Nepravdivým čtením je zabráněno, protože jsou viditelná pouze potvrzená data a statická povaha snímku brání jak neopakovatelným čtením, tak fiktivním zobrazením.
Toto srovnání (a zvláště zvýrazněná část) však pouze ukazuje, že úroveň snímku a serializovatelné izolace zabraňují stejným třem specifickým jevům. Neznamená to, že jsou rovnocenné ve všech ohledech. Důležité je, že standard SQL-92 nedefinuje serializovatelnou izolaci pouze z hlediska těchto tří jevů. Část 4.28 standardu uvádí úplnou definici:
Je zaručeno, že provádění souběžných transakcí SQL na úrovni izolace SERIALIZABLE bude serializovatelné. Serializovatelné provádění je definováno jako provádění operací souběžného provádění transakcí SQL, které má stejný účinek jako některé sériové provádění stejných transakcí SQL. Sériové spuštění je takové, při kterém se každá transakce SQL provede do dokončení před zahájením další transakce SQL.
Rozsah a význam implikovaných záruk zde často chybí. Řeknu to jednoduchým jazykem:
Jakákoli serializovatelná transakce, která se správně spustí, když je spuštěna samostatně, se bude nadále správně provádět s jakoukoli kombinací souběžných transakcí nebo bude vrácena zpět s chybovou zprávou (obvykle uváznutí v implementaci SQL Serveru).
Neserializovatelné úrovně izolace, včetně izolace snímků, neposkytují stejně silné záruky správnosti.
Zastaralá data
Izolace snímku se zdá téměř svůdně jednoduchá. Čtení vždy pocházejí z potvrzených dat k jedinému okamžiku a konflikty zápisu jsou automaticky detekovány a řešeny. Jak to, že to není dokonalé řešení pro všechny problémy související se souběžným používáním?
Jedním z potenciálních problémů je, že čtení snímků nemusí nutně odrážet aktuální potvrzený stav databáze. Transakce snímku zcela ignoruje všechny potvrzené změny provedené jinými souběžnými transakcemi po zahájení transakce snímku. Jiný způsob, jak to vyjádřit, je říci, že snímek transakce vidí zastaralá, neaktuální data. I když toto chování může být přesně to, co je potřeba ke generování přesné zprávy k určitému okamžiku, nemusí být tak vhodné za jiných okolností (například když se použije k vynucení pravidla ve spouštěči).
Zkreslení zápisu
Izolace snímků je také zranitelná vůči poněkud příbuznému jevu známému jako zkreslení zápisu. Čtení zastaralých dat v tom hraje roli, ale tento problém také pomáhá objasnit, co dělá a nedělá snímek „detekce konfliktu zápisu“.
Ke zkreslení zápisu dochází, když dvě souběžné transakce každá čte data, která druhá transakce upravuje. Nedochází ke konfliktu zápisu, protože dvě transakce upravují různé řádky. Žádná z transakcí nevidí změny provedené tou druhou, protože obě se čtou od okamžiku před provedením těchto změn.
Klasickým příkladem zkreslení zápisu je problém bílého a černého mramoru, ale zde chci ukázat další jednoduchý příklad:
-- 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;
Při izolaci snímku skončí obě tabulky v tomto skriptu jedním řádkem obsahujícím nulovou hodnotu. Toto je správný výsledek, ale nejde o serializovatelný výsledek:neodpovídá žádnému možnému příkazu k provedení sériové transakce. V každém skutečně sériovém rozvrhu musí být jedna transakce dokončena před zahájením druhé, takže druhá transakce by počítala řádek vložený první. Může to znít jako technická záležitost, ale pamatujte, že silné záruky serializovatelnosti platí pouze v případě, že transakce jsou skutečně serializovatelné.
Jemnost detekce konfliktů
Ke konfliktu zápisu snímku dochází vždy, když se transakce snímku pokusí upravit řádek, který byl změněn jinou transakcí, která byla potvrzena po zahájení transakce snímku. Jsou zde dvě jemnosti:
- Transakce se ve skutečnosti nemusí měnit jakékoli hodnoty dat; a
- Transakce nemusí upravovat žádné společné sloupce .
Následující skript ukazuje oba body:
-- 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;
Všimněte si následujícího:
- Každá transakce vyhledá stejný řádek pomocí jiného indexu
- Žádná aktualizace nevede ke změně již uložených dat
- Dvě transakce „aktualizují“ různé sloupce v řádku.
Přes to všechno, když se první transakce potvrdí, druhá transakce skončí s chybou konfliktu aktualizace:
Shrnutí:Detekce konfliktů vždy funguje na úrovni celého řádku a „aktualizace“ nemusí ve skutečnosti změnit žádná data. (Pokud by vás to zajímalo, změny v datech LOB nebo SLOB mimo řádek se také počítají jako změna řádku pro účely detekce konfliktů.
Problém cizího klíče
Detekce konfliktů se vztahuje také na nadřazený řádek ve vztahu cizího klíče. Při úpravě podřízeného řádku v rámci izolace snímku může změna nadřazeného řádku v jiné transakci vyvolat konflikt. Stejně jako dříve platí tato logika pro celý nadřazený řádek – nadřazená aktualizace nemusí ovlivnit samotný sloupec cizího klíče. Jakákoli operace v podřízené tabulce, která vyžaduje automatickou kontrolu cizího klíče v plánu provádění, může vést k neočekávanému konfliktu.
Chcete-li to demonstrovat, nejprve vytvořte následující tabulky a ukázková data:
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);
Nyní proveďte následující ze dvou samostatných připojení, jak je uvedeno v komentářích:
-- 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;
Čtení z fiktivní tabulky slouží k zajištění oficiálního zahájení transakce snímku. Vydání BEGIN TRANSACTION
k tomu nestačí; musíme provést nějaký druh přístupu k datům v uživatelské tabulce.
První aktualizace podřízené tabulky nezpůsobí konflikt, protože nastavení referenčního sloupce na NULL
nevyžaduje kontrolu nadřazené tabulky v prováděcím plánu (není co kontrolovat). Procesor dotazu se nedotýká nadřazeného řádku v prováděcím plánu, takže nedochází ke konfliktu.
Druhá aktualizace podřízené tabulky vyvolá konflikt, protože se automaticky provádí kontrola cizího klíče. Když procesor dotazu přistupuje k nadřazenému řádku, je také zkontrolován, zda nedošlo ke konfliktu aktualizací. V tomto případě je vyvolána chyba, protože odkazovaný nadřazený řádek po zahájení transakce snímku zaznamenal potvrzenou změnu. Všimněte si, že úprava nadřazené tabulky neovlivnila samotný sloupec cizího klíče.
Neočekávaný konflikt může také nastat, pokud změna v podřízené tabulce odkazuje na nadřazený řádek, který byl vytvořen souběžnou transakcí (a tato transakce potvrzená po zahájení transakce snímku).
Shrnutí:Plán dotazů, který zahrnuje automatickou kontrolu cizího klíče, může způsobit chybu konfliktu, pokud odkazovaný řádek od zahájení transakce snímku zaznamenal jakoukoli změnu (včetně vytvoření!).
Problém zkrátit tabulku
Transakce snímku selže s chybou, pokud byla jakákoli tabulka, ke které přistupuje, od začátku transakce zkrácena. To platí i v případě, že zkrácená tabulka neměla na začátku žádné řádky, jak ukazuje skript níže:
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;
Konečný SELECT se nezdaří s chybou:
Toto je další jemný vedlejší efekt, který je třeba zkontrolovat před povolením izolace snímků na existující databázi.
Příště
Další (a poslední) příspěvek v této sérii bude hovořit o úrovni izolace bez závazku čtení (laskavě známé jako "nolock").
[ Viz rejstřík pro celou sérii ]