Databázový systém InterSystems IRIS podporuje zajímavé struktury pro ukládání dat — globály. V podstatě se jedná o vícerozměrné klíče s různými dodatečnými funkcemi, jako jsou transakce, rychlé funkce pro procházení datových stromů, zámky a vlastní jazyk ObjectScript.
Více o globálech v sérii článků „Globály — čarodějnické meče pro ukládání dat“:
Začalo mě zajímat, jak jsou realizovány transakce v globálech, jaké mají specifika. Vždyť se jedná o zcela jinou strukturu pro ukládání dat než nám obecně známé tabulky. Mnohem nízkoúrovňovější.
Jak je známo z teorie relačních databází, dobrá realizace transakcí by měla splňovat požadavky :
A — Atomicita. Zaznamenávají se všechny změny provedené v transakci nebo žádné.
C — Konzistence. Po dokončení transakce musí být logický stav databáze interně nekontradiktorní. Velmi často se tento požadavek týká programátorů, ale v případě SQL databází se to týká také cizích klíčů.
I — Izolace. Souběžně prováděné transakce by na sebe neměly mít vliv.
D — Trvanlivost. Po úspěšném dokončení transakce by problémy na nižších úrovních (například výpadek napájení) neměly ovlivnit data změněná transakcí.
Globály jsou nerelační datové struktury. Byly navrženy pro extrémně rychlou práci na velmi omezeném hardwaru. Pojďme se podívat na realizaci transakcí v globálech pomocí .
Pro podporu transakcí v IRIS se používají příkazy: , , .
1. Atomicita
Nejjednodušeji lze ověřit atomicitu. Ověříme z konzole databáze.
Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMITPak provedeme výstup:
Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)Dostaneme:
1 2 3Všechno je v pořádku. Atomicita byla dodržena: všechny změny byly zapsány.
Ztížíme úkol, zavedeme chybu a podíváme se, jak se transakce uchová, částečně nebo vůbec.
Ještě jednou ověříme atomicitu:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3Poté násilně zastavíme kontejner, spustíme ho a podíváme se.
docker kill my-irisTento příkaz je prakticky ekvivalentní násilnému vypnutí napájení, protože posílá signál okamžitého zastavení procesu SIGKILL.
Mohla částečně zůstat transakce?
WRITE ^a(1), ^a(2), ^a(3)
^
^a(1)— Ne, nezůstala.
Vyzkoušíme příkaz pro vrácení:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK
WRITE ^a(1), ^a(2), ^a(3)
^
^a(1)Také se nic nezachovalo.
2. Konzistence
Protože v globálních datech jsou klíče také tvořeny na globálech (připomínám, že global je struktura nižší úrovně pro ukládání dat než relační tabulka), pro splnění požadavku na konzistenci je třeba zahrnout změnu klíče do téže transakce, co i změnu globálu.
Například máme global ^person, v němž uchováváme osoby a jako klíč používáme IČO.
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...Abychom dosáhli rychlého hledání podle příjmení a jména, vytvořili jsme klíč ^index.
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1Aby databáze byla konzistentní, musíme přidávat osobu takto:
TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMITPokud jde o mazání, také musíme použít transakci:
TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMITJinými slovy, splnění požadavku na konzistenci leží plně na ramenou programátora. Ale když jde o globály, je to normální vzhledem k jejich nízké úrovni.
3. Izolace
Tady začíná chaos. Mnoho uživatelů současně pracuje na stejné databázi, mění stejné údaje.
Situace se dá přirovnat k tomu, když mnoho uživatelů současně pracuje se stejným repozitářem kódu a snaží se současně commitovat změny do mnoha souborů najednou.
Databáze to musí zvládat v reálném čase. Vzhledem k tomu, že ve vážných firmách existuje i speciální osoba, která se stará o kontrolu verzí (o sloučení větví, vyřešení konfliktů atd.), a databáze to všechno musí dělat v reálném čase, je obtížnost úkolu a správnost návrhu databáze a kódu, který ji spravuje, jasná.
Databáze nemůže pochopit smysl akcí prováděných uživateli, aby zabránila konfliktům, pokud pracují se stejnými daty. Může pouze zrušit jednu transakci, která je v rozporu s jinou, nebo je provést po sobě.
Dalším problémem je, že během provádění transakce (před potvrzením) může být stav databáze nekonzistentní, a proto je žádoucí, aby měli ostatní transakce omezený přístup k nekonzistentnímu stavu databáze, což se v relačních DB dosahuje mnoha způsoby: vytvářením snímků, více verzemi řádků atd.
Při paralelním provádění transakcí je důležité, aby si navzájem nepřekážely. To je vlastnost izolace.
SQL definuje 4 úrovně izolace:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
Pojďme se podívat na každou úroveň jednotlivě. Náklady na implementaci každé úrovně rostou téměř exponenciálně.
READ UNCOMMITTED — to je nejnižší úroveň izolace, ale zároveň nejrychlejší. Transakce mohou číst změny provedené navzájem.
READ COMMITTED — to je další úroveň izolace, která představuje kompromis. Transakce nemohou číst změny provedené navzájem, dokud nejsou potvrzeny, ale mohou číst jakékoli změny provedené po potvrzení.
Pokud máme dlouhou transakci T1, během které došlo k potvrzením v transakcích T2, T3 … Tn, které pracovaly se stejnými daty jako T1, pak při dotazu na data v T1 pokaždé dostaneme jiný výsledek. Tento jev se nazývá nenapravitelné čtení.
REPEATABLE READ — v této úrovni izolace nemáme jev nenapravitelného čtení, čímž vznikají snímky výsledných dat pro každý požadavek na čtení dat, které se při opětovném použití v této samé transakci používají z tohoto snímku. V této úrovni izolace je však možné číst fantomová data, což znamená čtení nových řádků, které byly přidány paralelními potvrzenými transakcemi.
SERIALIZABLE — nejvyšší úroveň izolace. Je charakterizována tím, že data jakýmkoli způsobem používaná v transakci (čtení nebo změna) se stávají dostupnými jiným transakcím až po dokončení první transakce.
Nejprve se podívejme, zda je v transakci izolace operací od hlavního vlákna. Otevřeme 2 terminálová okna.
Kill ^t
Write ^t(1)
2
TSTART
Set ^t(1)=2Izolace není přítomna. Jedno vlákno vidí, co dělá druhé otevřené transakce.
Podívejme se, zda transakce různých vláken vidí, co se v nich děje.
Otevřeme 2 terminálová okna a otevřeme 2 transakce současně.
kill ^t
TSTART
Write ^t(1)
3
TSTART
Set ^t(1)=3
Současné transakce vidí data navzájem. Takže jsme dostali nejjednodušší, ale také nejrychlejší úroveň izolace READ UNCOMMITTED.
V zásadě to bylo lze očekávat u globálních, pro které je výkon vždy prioritou.
Co však dělat, když budeme potřebovat vyšší úroveň izolace v operacích na globálních?
Je třeba přemýšlet o tom, proč vůbec úrovně izolace potřebujeme a jak fungují.
Nejvyšší úroveň izolace SERIALIZE znamená, že výsledek paralelně prováděných transakcí je ekvivalentní jejich sekvenčnímu provedení, což zaručuje absenci kolizí.
Toho můžeme dosáhnout pomocí správných zámků v ObjectScript, které mají množství různých způsobů použití: lze provádět běžné, inkrementální, vícenásobné zámky pomocí příkazu .
Nižší úrovně izolace jsou kompromise, které mají za cíl zvýšit rychlost databáze.
Podívejme se, jak můžeme dosáhnout různých úrovní izolace pomocí zámků.
Tento příkaz umožňuje brát nejen exkluzivní zámky potřebné pro změnu dat, ale také tzv. sdílené, které mohou být současně získány několika vlákny, když potřebují číst data, která by neměla být během čtení měněna jinými procesy.
Více o dvoufázové metodě blokování v ruštině a angličtině:
→
→
Obtížnost spočívá v tom, že během transakce může být stav databáze nekonzistentní, avšak tyto nekonzistentní údaje jsou viditelné jiným procesům. Jak tomu předejít?
Pomocí zámků vytvoříme taková okna viditelnosti, ve kterých bude stav databáze konzistentní. A všechny přístupy k těmto oknům viditelnosti konzistentního stavu budou řízeny zámky.
Sdílené zámky pro stejné data umožňují více procesům je používat. Tyto zámky zakazují jiným procesům měnit data, tj. používají se k vytváření oken konzistentního stavu databáze.
Exkluzivní zámky se používají pro změnu dat — tento zámek může mít pouze jeden proces. Exkluzivní zámek může mít:
- Jakýkoli proces, pokud jsou data volná
- Pouze ten proces, který má pro tato data sdílený zámek a jako první požádal o exkluzivní zámek.

Čím užší je okno viditelnosti, tím déle musí ostatní procesy čekat, ale tím konzistentnější může být stav databáze v něm.
READ_COMMITTED — podstata této úrovně spočívá v tom, že vidíme pouze potvrzená data z jiných toků. Pokud data v jiné transakci ještě nebyla potvrzena, vidíme jejich starou verzi.
To nám umožňuje paralelizovat práci místo čekání na uvolnění zámku.
Bez speciálních triků nebudeme moci vidět starou verzi dat v IRIS, takže se budeme muset obejít zámky.
Tudíž musíme pomocí sdílených zámků povolit čtení dat pouze v obdobích konzistence.
Představme si, že máme databázi uživatelů ^person, kteří si navzájem převádějí peníze.
Okamžik převodu od osoby 123 k osobě 242:
LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)Okamžik dotazu na množství peněz od osoby 123 před odečtením by měl být doprovázen exkluzivním zámkem (výchozí):
LOCK +^person(123)
Write ^person(123)A pokud je potřeba zobrazit stav účtu v osobním účtu, lze použít sdílený zámek nebo ho vůbec nepoužít:
LOCK +^person(123)#”S”
Write ^person(123)Nicméně, pokud předpokládáme, že operace práce s databází probíhají téměř okamžitě (připomínám, že globály jsou mnohem nízkoúrovňovější struktura než relační tabulka), pak potřeba této úrovně klesá.
REPEATABLE READ — na této úrovni izolace se umožňuje, že může být několik čtení dat, která mohou být změněna paralelními transakcemi.
Tudíž musíme umístit sdílený zámek na čtení dat, která měníme, a exkluzivní zámky na data, která měníme.
Operátor LOCK umožňuje podrobně vypsat všechny potřebné zámky v jednom operátoru, kterých může být velmi mnoho.
LOCK +^person(123, amount)#”S”
čtení ^person(123, amount)další operace (v tuto chvíli se paralelní vlákna pokoušejí změnit ^person(123, amount), ale nemohou)
LOCK +^person(123, amount)
změna ^person(123, amount)
LOCK -^person(123, amount)
čtení ^person(123, amount)
LOCK -^person(123, amount)#”S”Při vyjmenování zámků čárkou se berou postupně, a pokud se to udělá takto:
LOCK +(^person(123),^person(242))tak se berou atomárně najednou.
SERIALIZE — budeme muset nastavit zámky tak, aby nakonec všechny transakce, které mají společná data byly prováděny sekvenčně. Pro tento přístup by většina zámků měla být exkluzivní a měly by se brát na co nejmenších oblastech globálu pro výkon.
Pokud hovoříme o výběrech prostředků v globálu ^person, pak je pro něj přijatelná pouze úroveň izolace SERIALIZE, protože peníze by měly být utraceny striktne sekvenčně, jinak je možné utratit stejnou částku několikrát.
4. Dlouhověkost
Testoval jsem náhlé vypnutí kontejneru pomocí
docker kill my-irisDatabáze to přežila dobře. Nebyly zjištěny žádné problémy.
Závěr
Pro globály v InterSystems IRIS existuje podpora transakcí. Jsou skutečně atomární, spolehlivé. Pro zajištění konzistence DB v globálech jsou třeba úsilí programátora a použití transakcí, protože v ní neexistují složité vestavěné konstrukce jako cizí klíče.
Úroveň izolace u globálů bez použití zámků je READ UNCOMMITTED, a při použití zámků ji lze zajistit až do úrovně SERIALIZE.
Správnost a rychlost fungování transakcí na globálech velmi silně závisí na umění programátora: čím více se používají sdílené zámky při čtení, tím vyšší je úroveň izolace, a čím užší jsou exkluzivní zámky, tím větší je výkon.
Zdroj: habr.com
