Vyberte stránku

V databázových systémech se obecně předpokládá implementace transakcí, které jsou nástrojem pro zajištění integrity dat a ochranou před softwarovými nebo hardwarovými chybami, jako je nedostatek místa na disku nebo výpadek proudu. Nicméně, ne všechny databáze musí být nutně transakční.

Kdyby neexistovala MySQL, tak by transakce patrně nebyly tak populárním tématem. I když ne všechny databáze byly a jsou transakční, nikdo (do nástupu MySQL) netvrdil, že transakce jsou něco „navíc“. Tento přístup se pak uchytil zejména mezi web designery, možná proto, že chyby v datech nejsou na první pohled vidět, zato rychlost, resp. pomalost webu je zřejmá.

V době nástupu MySQL (a PHP) přibližně před deseti lety, byl internet zvláštní experimentální zónou. S občasným výpadkem některých serverů si nikdo zvlášť hlavu nelámal, stejně tak se zabezpečením, ergonomií, rychlostí. Technologie, které se v té době používaly, byly neporovnatelné s těmi dnešními. Ať už jsou to staré verze PHP nebo o něco málo mladší verze ASP. Databáze se používaly pro uložení výsledků anket, uložení účtů a hesel (vesele s otevřenými hesly), pro ukládání logů. Ztráta dat, v té době, neznamenala větší komplikaci (u typických www aplikací do konce devadesátých let).

Pragmatický přístup MySQL AB (a nejen MySQL AB - „co neumíme, neumíme, protože to správný programátor nepotřebuje“ - a to ať už se jednalo o podporu transakcí, podporu referenční a doménové integrity, poddotazů, reálně nikoho neomezoval (datové schéma www aplikací bylo jednoduché, dat bylo relativně málo, stejně tak jako programátorů), a tak se diskuze o významu transakcí vedly hlavně v ideologické rovině. Tudíž naprosto bezvýsledné a nekonečné. Možná díky nim málokterý software vzbuzuje tolik emocí jako MySQL. MySQL má stovky oddaných stoupenců, a stejně tak stovky vášnivých odpůrců.

Diskuze ohledně transakcí se netýkaly jen stoupenců a odpůrců MySQL. Například na otázku, zda programátor používá transakce nad Microsoft SQL Serverem, s pousmáním odpověděl otázkou: „K čemu? Šance, že server spadne v okamžiku, kdy se něco děje, je minimální.“ Na otázku ohledně izolace uživatelů a řešení kolizí odpověděl, že servery jsou tak rychlé, že není šance, že by mohlo dojít ke kolizi.

Čtěte také: Význam kvality svarů při vyztužování betonu

Ovšem datová integrita může být porušena i z důvodů souběžné činnosti více uživatelů nad jednou databází. Problém nastává také v případě, že několik (dva a více) uživatelů provádí souběžně činnost v jedné databázi. Je třeba si uvědomit, že pod správu transakcí spadá i koordinace jednotlivých SQL dotazů spuštěných různými uživateli. Není možné, aby docházelo k chybám z důvodu paralelního přístupu k databázi.

Setkal jsem se s jedním e-shopem, kde se při předvánoční nákupní horečce náhodně objevovalo a ztrácelo zboží ve skladu (v tom virtuálním, nikoliv fyzickém). Jinak bylo všechno celý rok v pohodě. Správný výsledek je 27, kdežto hodnota uložená v databázi bude 26. Teoreticky, a v některých databázích i prakticky (PostgreSQL a Oracle mezi nimi není) nás před touto chybou ochrání správně nastavená (nejvyšší) úroveň izolace transakce. Souběh, race condition je zákeřná chyba - obtížně se simuluje, vyskytuje se minimálně, a to ještě v závislosti na zatížení databáze.

Uznávám, že je to trochu zrada - mám transakce, mám izolace transakcí, a stejně musím řešit souběh. V jedno uživatelské aplikaci se jedná o bezpečný kód. V to chvíli došlo tzv. race condition (souběhu).

Co je to transakce?

Pod transakcí si můžeme představit příkaz nebo skupinu příkazů, které převedou databázi (resp. data) z jednoho konzistentního stavu do druhého. Transakce je tedy sekvence příkazů vedoucích z jednoho konzistentního stavu databáze do druhého. Transakce začíná implicitně na počátku běhu programu a vždy po ukončení transakce předchozí. Celá transakce končí koncem programu nebo explicitním provedením příkazu commit. Transakce probíhá tak, že každá operace je protokolována v log souboru, který může být po provedení příkazu commit smazán. Končí-li transakce uváznutím nebo jinou uživatelskou kolizí, je (resp. může být) přerušena a data za použití log souboru budou obnovena do stavu před transakcí.

Pokud budu provádět UPDATE jednoho sloupce velké tabulky, tak konzistentní stav je před zahájením příkazu a po dokončení příkazu. V mezičase je obsah tabulky nekonzistentní - část je modifikovaná, část nikoliv. Pokud je příkaz spuštěn v rámci transakce, tak prostředky databáze je zajištěno, že uživatelé budou mít vždy přístup pouze ke konzistentním datům.

Čtěte také: Průvodce kročejovou izolací

Transakce zajišťují jednak bezpečnost - viz problém v případě výpadku po provedení prvního příkazu UPDATE (pozn. vypařila by se částka 100), a jednak konzistenci dat - data v okamžiku, kdy došlo k odečtu účtu, a ještě nedošlo k přičtení částky na druhý účet jsou nekonzistentní. Díky transakcím lze zjednodušit aplikační logiku více uživatelských aplikací. Transakce také zvyšují odolnost databáze vůči poškození.

V SQL pro transakce existují tři základní příkazy:

  • BEGIN - začátek transakce v aktuální relaci.
  • ROLLBACK - odvolání transakce. V průběhu transakce se mohou kdykoliv vrátit do stavu po posledním commitu vyvoláním procesu rollback.
  • COMMIT - potvrzení transakce. Commit je proces, který zajistí, že změny, které uživatel udělal během transakce, se stanou součástí fyzické databáze (to znamená, že se zapíší natvrdo do databáze). Než dojde ke commitu, existují vedle sebe obě varianty dat - stará i nová průběžně měněná.

ACID kritéria

Pro bezpečné zpracování dat je nezbytně nutné stanovit teoretické požadavky na chování systémů. V tomto případě hovoříme o kritériích ACID:

  • Atomicity (Atomicita transakce) - příkazy v transakci se vždy provedou všechny nebo žádný. Není tedy možné, aby se vykonala jen část transakce.
  • Consistency (Konzistence transakce) - před a po dokončení transakce jsou data konzistentní.
  • Isolation (Izolovanost transakce) - transakce jsou zpracovávány současně, okolí nic nezaznamená a výsledek transakce je stejný, jako by byla zpracovávána jedna transakce po druhé.
  • Durability (Trvalost transakce) - pokud již byla transakce potvrzena, potom jsou změny dat trvalé a nemohou být ztraceny - a to i v případě poruchy systému.

Problémy souběžných transakcí

V ANSI SQL (SQL92) jsou pojmenovány a popsány možné situace, které mohou nastat při souběžné činnosti více transakcí, a následně jsou definovány úrovně izolace transakcí, které těmto situacím předcházejí. Jedná se o tyto fenomény:

1. Špinavé čtení (Dirty Read)

  • Popis: K špinavému čtení dochází, když transakce čte data, která ještě nebyla potvrzena jinou transakcí.
  • Příklad: Transakce 1 aktualizuje řádek. Transakce 2 přečte aktualizovaný řádek předtím, než transakce 1 potvrdí aktualizaci. Poté klient A odvolá svou transakci.

2. Neopakovatelné čtení (Non-repeatable Read)

  • Popis: Neopakovatelné čtení nastane, když transakce čte stejný řádek dvakrát, ale pokaždé získá jiná data. Hodnoty v řádku se mezi čteními liší.
  • Příklad: Transakce 1 přečte řádek. Transakce 2 aktualizuje nebo odstraní tento řádek a potvrdí aktualizaci nebo odstranění. Klient A přečte data a prozatím neukončí transakci. Klient B změní nebo zruší tato data a ukončí svou transakci.

3. Přízraky (Phantom Read)

  • Popis: Přízrak je řádek, který odpovídá kritériím hledání, ale na začátku se nezobrazuje. Jde o fantomové řádky.
  • Příklad: Transakce 1 čte sadu řádků, které splňují určitá kritéria hledání. Transakce 2 vygeneruje nový řádek (prostřednictvím aktualizace nebo vložení), který odpovídá kritériím hledání pro transakci 1. Klient A položí dotaz, přečte odpověď na něj a prozatím neukončí transakci. Klient B vloží do databáze další řádky vyhovující podmínkám v dotazu klienta A a ukončí svou transakci.

Úrovně izolace transakcí

Čtyři úrovně izolace transakcí (jak je definováno SQL-92) jsou definovány z hlediska těchto jevů. Databázový systém nemusí implementovat všechny úrovně, stačí implementovat nejvyšší úroveň. Každá úroveň zastupuje všechny nižší úrovně. Tato definice je názorná, ale není úplná, co do popisu chování databázového systému. Jednotlivé implementace, které splňují standard, nemusí být 100% vzájemně kompatibilní. Typickou ukázkou je rozdíl v chování databází Oracle a DB2.

Čtěte také: IPA asfaltová izolace: Co potřebujete vědět

Nejvyšší úroveň izolace je SERIALIZABLE. To, že je definována více než jedna úroveň izolace transakcí, vychází ze skutečnosti, že úroveň SERIALIZABLE je „provozně“ náročná - znamená intenzivní zamykání a ve většině případů není nutná. Nižší úroveň znamená méně zámků (v klasické ne MVCC architektuře). Standard specifikuje výchozí úroveň izolace transakcí, a tou je úroveň SERIALIZABLE. Aplikační vývojář by tak měl úroveň ve své aplikaci spíše snižovat. Realita je přesně opačná. Plná implementace úrovně SERIALIZABLE je relativně náročná na zámky, takže výchozí úroveň je mnohem níže, a je na vývojářích vynucení vyšší úrovně izolace (výchozí READ COMMITTED je v MS SQL Server, PostgreSQL, Oracle, DB2).

Zde je přehled úrovní izolace:

Úroveň izolace Dirty Read Non-repeatable Read Phantom Read Popis
READ UNCOMMITTED Ano Ano Ano Transakce nejsou navzájem izolované. Pokud DBMS podporuje jiné úrovně izolace transakcí, ignoruje jakýkoli mechanismus, který používá k implementaci těchto úrovní.
READ COMMITTED Ne Ano Ano Transakce čeká na odemknutí řádků uzamčených jinými transakcemi; tím zabráníte čtení všech "nečistých" dat. Transakce obsahuje zámek pro čtení (pokud čte pouze řádek) nebo zámek zápisu (pokud aktualizuje nebo odstraní řádek) na aktuálním řádku, aby se zabránilo aktualizaci nebo odstranění jiných transakcí. Transakce uvolní zámky čtení, když se přesune mimo aktuální řádek.
REPEATABLE READ Ne Ne Ano Transakce čeká na odemknutí řádků uzamčených jinými transakcemi; tím zabráníte čtení všech "nečistých" dat. Transakce uchovává zámky čtení na všech řádcích, které se vrátí do aplikace, a zapisuje zámky na všech řádcích, které vloží, aktualizuje nebo odstraní. Vzhledem k tomu, že jiné transakce nemohou aktualizovat nebo odstranit tyto řádky, aktuální transakce se vyhne neopakovatelným čtením.
SERIALIZABLE Ne Ne Ne Transakce čeká na odemknutí řádků uzamčených jinými transakcemi; tím zabráníte čtení všech "nečistých" dat. Transakce uchovává zámek pro čtení (pokud čte pouze řádky) nebo zámek zápisu (pokud může aktualizovat nebo odstranit řádky) v oblasti řádků, které ovlivňuje. Vzhledem k tomu, že jiné transakce nemohou aktualizovat nebo odstranit řádky v daném rozsahu, aktuální transakce zamezuje jakýmkoli neopakovatelným čtením. Protože jiné transakce nemohou vložit žádné řádky v oblasti, aktuální transakce zabraňuje jakýmkoli fantomům.

Je důležité si uvědomit, že úroveň izolace transakce nemá vliv na schopnost transakce vidět své vlastní změny; transakce mohou vždy zobrazit všechny změny, které dělají. Například transakce se může skládat ze dvou příkazů UPDATE, z nichž první zvýší plat všech zaměstnanců o 10 procent a druhý nastaví mzdu tak, aby mzda zaměstnanců překračující určitou maximální částku byla nastavena na tuto maximální částku.

SERIALIZABLE

Nejvyšší úrovní je úroveň SERIALIZABLE. Tato úroveň nejlépe vzájemně izoluje uživatele databáze. Jak je z názvu patrné, transakce by se měly provádět za sebou, nikoliv souběžně. Triviální implementace této úrovně spočívá v omezení maximálního počtu přihlášených uživatelů na jednoho uživatele. Podobnou techniku používá např. SQLite. Výsledkem triviální implementace je totální destrukce výkonu databáze v případě přístupu více uživatelů. Vyjma embeded databází (což je např. SQLite) je proto tato technika nepoužitelná. I o něco sofistikovanější technika - provádění vždy pouze jedné write transakce - je neefektivní a prakticky se nepoužívá. Stupeň izolace SERIALIZABLE zaručuje, že souběžné transakce budou mít stejný efekt, jako by byly provedeny po sobě. Zároveň však snižuje schopnost serveru provádět akce souběžně, tudíž může snížit propustnost serveru, pokud více klientů pracuje se stejnými daty.

Implementace úrovní izolace v různých databázích

Vlastní implementace úrovní izolace je více-méně specifická pro každý databázový systém a předurčuje chování databáze. V době psaní standardu se vůbec nepočítalo s multigenerační architekturou. Implicitně se počítalo se zamykáním - problém je v tom, že to ve standardu nikde není zmíněno. Problém je, zda se upřednostní definice, která říká, že v SERIALIZABLE úrovni se nemá vyskytovat špinavé čtení, neopakovatelné čtení a fantómy, nebo sémantika, která mluví o řazení transakcí za sebou. Ta druhá varianta znamená zamykání celých oblastí tabulek (v lepším případě, v horším celých tabulek) - což může znamenat znatelné snížení výkonu.

Microsoft SQL Server

Databázový stroj SQL Serveru interně respektuje READ COMMITTED pouze úroveň izolace pro přístup k metadatům. Pokud transakce má úroveň izolace, která je například SERIALIZABLE a v rámci transakce, je proveden pokus o přístup k metadatům pomocí zobrazení katalogu nebo metadat generujících předdefinované funkce, tyto dotazy se spustí, dokud nebudou dokončeny jako READ COMMITTED. V rámci izolace snímků ale může přístup k metadatům selhat kvůli souběžných operacím DDL. Důvodem je to, že metadata nejsou ve verzi. Interně SQL server 602SQL implementuje pouze stupně izolace READ COMMITTED a SERIALIZABLE. Při nastavení READ UNCOMMITTED se vnitřně nastaví READ COMMITTED, při nastavení REPEATABLE READ se vnitřně nastaví SERIALIZABLE.

PostgreSQL

PostgreSQL implementuje pouze READ COMMITED (READ UNCOMMITED jako READ COMMITTED) a SERIALIZABLE (REPEATABLE READ jako SERIALIZABLE). Hlavní rozdíly v chování databází jsou v úrovni SERIALIZABLE. Za vzorové chování se považuje implementace v DB2, naopak diskutabilní je implementace v Oracle a PostgreSQL. Rollback segment v PostgreSQL úplně chybí. Verze záznamů zůstávají v jednom datovém souboru a nepřepisují se (dokud nejsou označeny jako explicitně mrtvé - viz příkaz VACUUM). V PostgreSQL je náročnější čtení (z disku se načítají jak potvrzené, tak nepotvrzené verze), rychlý COMMIT, ještě rychlejší ROLLBACK a nevýhodou nutnost spouštět VACUUM. V MVCC databázích (PostgreSQL) úroveň SERIALIZABLE neznamená vyšší režii nebo pomalejší aplikace - využívá se vlastností architektury a není důvod, proč se „omezovat“ nižší úrovní.

Oracle

Multigenerační architektura (MVCC nebo MGA) je implementována do Oracle. Hlavní rozdíly v chování databází jsou v úrovni SERIALIZABLE. Za vzorové chování se považuje implementace v DB2, naopak diskutabilní je implementace v Oracle a PostgreSQL. V MVCC databázích (Oracle) úroveň SERIALIZABLE neznamená vyšší režii nebo pomalejší aplikace - využívá se vlastností architektury a není důvod, proč se „omezovat“ nižší úrovní.

InnoDB (MySQL)

InnoDB je představitelem multigenerační architektury (MVCC nebo MGA) s rollback segmentem (je to podobná implementace jako u Oracle). Rollback segment se používá jednak pro operaci ROLLBACK, za druhé pro přístup ke starším verzím záznamů. Data v InnoDB se stěhují z datových souborů do rollback segmentu. Výhodou InnoDB je rychlé čtení potvrzených neaktualizovaných záznamů, naopak nevýhodou pomalejší UPDATE a významně pomalejší ROLLBACK. Zásadní rozdíl mezi InnoDB a PostgreSQL je použití primárního klíče jako cluster indexu. Tj. data jsou v InnoDB fyzicky uspořádána podle primárního klíče. Tudíž dotazy na primární klíč jsou velice rychlé a efektivní, za cenu o něco pomalejšího INSERTu. InnoDB nabízí všechny úrovně izolace transakcí - výchozí je REPEATABLE READ - ohledně chování odpovídá PostgreSQL implementaci SERIALIZABLE.

Firebird

Vývojáři Firebirdu raději nejasnosti ohledně SERIALIZABLE neřešili a přišli s vlastní úrovní SNAPSHOT a SNAPSHOT TABLE STABILITY. Z mého pohledu je engine Firebirdu docela podobný engine PostgreSQL. Nejmarkantnější rozdíl je neexistence transakčního logu ve Firebirdu. Dalším rozdílem je ukládání tzv. delta verzí. Neukládají se kompletní verze záznamů (jako v PostgreSQL), ale pouze jejich rozdíly (ve Firebirdu). Firebird odstraňuje staré verze záznamů průběžně při přístupu k řádku. Ohledně nabídky úrovní izolací transakcí používá Firebird vlastní terminologii - READ COMMITTED, SNAPSHOT, SNAPSHOT TABLE STABILITY. SNAPSHOT odpovídá REPEATABLE READu a SNAPSHOT TABLE STABILITY úrovni SERIALIZABLE. Pozor při úrovni SNAPSHOT TABLE STABILITY dochází k uzamčení pro zápis všech dotčených tabulek.

Zámky a Deadlocky

Implementace příkazů je specifická pro každý databázový server. Obecně je možné rozdělit implementaci do tří základních skupin. Do Oracle nebo PostgreSQL je implementována multigenerační architektura - principem je, že každý řádek může v jednom okamžiku existovat v několika verzích. Systém zámků je implementován do DB2. A Microsoft SQL Server používá kombinaci obou předchozích implementací.

Zámky se obecně rozdělují na exkluzívní (exklusive) a sdílené. Exkluzívní zámky kompletně uzamknou celou entitu. Zapisovat do entity i číst z entity může pouze vlastník zámku. Zámek může být přidělen jen jedné transakci. Sdílené zámky uzamknou entitu pro zápis. Čtení není omezeno a vlastník zámku má jistotu, že do entity nikdo nezapíše data. Tento typ zámku může být přidělen více transakcím. Pokud nastane situace, že transakce vyžaduje uzamčení, které je již přiděleno jiné transakci, je tato transakce pozastavena a čeká na přidělení zámku.

Zámky spravuje správce zámků (lock manager). V databázi existuje tabulka zámků a pro každý zámek se v této tabulce vede záznam o počtu transakcí, které sdílejí daný zámek, typ zámku, ukazatel do fronty zámků. Pokud chce transakce přistoupit k entitě a číst nebo aktualizovat data v entitě, je třeba, aby tato transakce obdržela sdílený nebo exkluzivní zámek k této entitě. Dále je implementováno pravidlo, že pokud již transakce nějaký zámek měla a tento zámek uvolnila, nemůže požadovat zámek další.

Deadlock (Uváznutí)

Při přidělování a uvolňování zámku, může dojít k problému, kdy jedna transakce žádá zámek druhé transakce. V tom by nebyl žádný problém, ale druhá transakce tento zámek uvolnit nemůže, protože opačně čeká na zámek první transakce. Tomuto problému se říká uváznutí (deadlock). Tuto situaci může zobecnit pro jakýkoliv počet transakcí. K uváznutí dochází málokdy, a řešení tohoto problému je jednoduché. Pokud transakce na zámek čeká moc dlouho, je pravděpodobné, že transakce uvázla, a transakce bude zrušena. Zrušená transakce se restartuje, a pokud znovu dojde k uváznutí, je upřednostněna.

V rámci prevence uváznutí jsou transakcím přidělovány priority - například čím starší transakce, tím má větší prioritu. V tomto případě, pokud dojde k uváznutí, správce zámku vybere transakci s vyšší prioritou a transakci s nižší prioritou zruší. Restartovaná transakce dostane stejnou prioritu, se kterou byla ukončena. Dnešní databáze deadlock dokáží identifikovat, a i řešit (výjimkou).

Při jakémkoliv použití zámků je nutné se zabývat otázkou deadlocku. Na úrovni tabulek je řešení poměrně jednoduché - stačí vždy zamykat ve stejném pořadí. Stejné pravidlo samozřejmě platí i pro záznamy, ale předpokládám, že by se dost obtížně implementovalo.

V MVCC databázích platí, že zapisující proces neblokuje čtecí proces - writers don't block readers and readers don't block writers (neplatí pro úroveň SERIALIZABLE). Pokud se ovšem dva zapisující procesy sejdou na jednom záznamu, pak druhý proces (který jako druhý provedl zápis) čeká na dokončení transakce prvního procesu.

Víceřádkové podmínky (Multi-row constraints)

V některých aplikacích se můžeme setkat s tzv. víceřádkovými podmínkami - (multi-row constraints). Např. součet všech objednávek od jednoho uživatele nesmí přesáhnout částku 10 000, nebo maximální měsíční počet objednávek může být 10. Tady klasický zámek nepomůže - nemůžete zamknout neexistující řádek. Řešením je uzamčení tabulky - nebo (v případě, že to databáze podporuje (pouze DB2), tzv. predikátový zámek (predicate lock). V Postgresu musí veškerou práci odřít vývojář a zamknout tabulku.

Optimistické a pesimistické zamykání

Existují dvě varianty zamykání, optimistická a pesimistická, v závislosti na nastavení READ_COMMITTED_SNAPSHOT (Default OFF). Pesimistické zamykání spočívá v přidělení exkluzívního zámku transakci. Ostatním uživatelům je tedy striktně zabráněno v manipulaci s databází. Toto řešení je ideální v případě, že časy jednotlivých zámků jsou krátké. Nevýhodou tohoto řešení je potencionálně dlouhá čekací doba jednotlivých uživatelů.

Druhou variantou je optimistické zamykání. Prvotní předpoklad je, že transakce spolu přijdou do konfliktu jen zřídka. Tento přístup je výhodný v tom, že umožňuje k databázi přístup více uživatelům najednou. SŘBD udržuje virtuální kopii databáze pro každého uživatele. Pokud chce uživatel aktualizovat záznam, porovná se fyzický a logický záznam v tabulkách, zjistí se rozdíly a pokud dojde k problémům, data se neaktualizují.

V závislosti na četnosti kolizí je výhodnější první nebo druhá strategie - pokud je riziko kolizí velké, pak je výhodnější zamykání. Pokud ke kolizím dochází zřídka, pak je výhodnější optimistický přístup v úrovni SERIALIZABLE. Pozor i pesimistický přístup v úrovni READ COMMITTED (s FOR UPDATE) může skončit výjimkou - dead lockem.

tags: #urovne #izolace #transakce #databáze

Oblíbené příspěvky: