Transakcije u globalima InterSystems IRIS

Transakcije u globalima InterSystems IRISSistem Управления Базами Данных InterSystems IRIS поддерживает интересные структуры для хранения данных — глобалы. По сути, это многоуровневые ключи с различными дополнительными функциями, такими как транзакции, быстрые функции для обхода деревьев данных, блокировки и свой язык ObjectScript.

Подробнее о глобалах в цикле статей «Глобалы — мечи-кладенцы для хранения данных»:

Деревья. Часть 1
Деревья. Часть 2
Разреженные массивы. Часть 3

Мне стало интересно, как реализованы транзакции в глобалах, какие там есть особенности. Ведь это совершенно иная структура для хранения данных, чем всем привычные таблицы. Она гораздо более низкоуровневая.

Как известно из теории реляционных баз данных, хорошая реализация транзакций должна удовлетворять требованиям ACID:

A — Atomic (атомарность). Записываются все изменения, сделанные в транзакции, или никаких.

С — Consistency (согласованность). После завершения транзакции логическое состояние БД должно быть внутренне непротиворечивым. Во многом это требование касается программиста, но в случае SQL-баз данных оно касается также внешних ключей.

I — Isolate (изолированность). Параллельно выполняющиеся транзакции не должны оказывать влияние друг на друга.

D — Durable (долговечность). После успешного завершения транзакции проблемы на нижних уровнях (сбой по питанию, например) не должны оказывать влияние на данные, изменённые транзакцией.

Глобалы — это нереляционные структуры данных. Они создавались для сверхбыстрой работы на очень ограниченном железе. Давайте разберёмся в реализации транзакций в глобалах с помощью официального docker-образа IRIS.

Для поддержки транзакций в IRIS используются команды: TSTART, TCOMMIT, TROLLBACK.

1. Атомарность

Легче всего проверить атомарность. Проверяем из консоли базы данных.

Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMIT

Потом делаем вывод:

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Получим:

1 2 3

Всё в порядке. Атомарность соблюдена: все изменения записались.

Усложним задачу, введём ошибку и посмотрим, как сохранится транзакция, частично или вообще никак.

Ещё раз проверим атомарность:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3

После чего принудительно остановим контейнер, запустим и посмотрим.

docker kill my-iris

Ta ekipa je praktično ekvivalentna nasilnemu izklopu napajanja, saj pošilja signal za takojšnjo zaustavitev procesa SIGKILL.

Ali je mogoče, da je transakcija delno shranjena?

WRITE ^a(1), ^a(2), ^a(3)
^
 ^a(1)

— Ne, ni shranjena.

Preizkusimo ukaz za povrnitev:

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)

Tudi nič ni bilo shranjeno.

2. Konsistentnost

Ker so ključi v globalnih zbirkah oblikovani tudi na globalih (opomnim, da je global struktura z nižjo ravnijo za shranjevanje podatkov kot relacijska tabela), je za izpolnitev zahteve po konsistentnosti treba vključiti spremembo ključa v isto transakcijo, kot tudi spremembo globala.

Na primer, imamo global ^person, kjer shranjujemo osebne informacije in kot ključ uporabljamo davčno številko.

^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
...

Da bi imeli hiter iskalnik po priimkih in imenih, smo ustvarili ključ ^index.

^index('Kamenev', 'Sergey', 1234567) = 1

Da bi bila baza konsistentna, moramo dodati osebo tako:

TSTART
^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
^index('Kamenev', 'Sergey', 1234567) = 1
TCOMMIT

Ustrezno, pri brisanju moramo prav tako uporabiti transakcijo:

TSTART
Kill ^person(1234567)
ZKill ^index('Kamenev', 'Sergey', 1234567)
TCOMMIT

Z drugimi besedami, izpolnjevanje zahteve po konsistentnosti leži v celoti na ramenih programerja. A ko gre za globale — je to normalno, zaradi njihove nizke ravni narave.

3. Izolacija

Tukaj se začnejo zapleti. Veliko uporabnikov hkrati dela na isti zbirki podatkov, spreminja iste podatke.

Situacija je primerljiva s tem, ko veliko uporabnikov hkrati deluje z enim in istim repozitorijem kode in poskuša hkrati napredovati s spremembami v več datotekah naenkrat.

Baza podatkov mora to vse razreševati v realnem času. Glede na to, da imajo v resnih podjetjih celo posebne osebe, ki skrbijo za nadzor različic (za združevanje vej, reševanje konfliktov itd.), in da mora vse to baza podatkov početi v realnem času, postane očitna kompleksnost naloge in pravilnost načrtovanja zbirke podatkov ter kode, ki jo vzdržuje.

Baza podatkov ne more razumeti pomena dejanj, ki jih izvajajo uporabniki, da bi preprečila konflikte, kadar delajo z istimi podatki. Lahko samo prekliče eno transakcijo, ki nasprotuje drugi, ali pa ju izvede zaporedno.

Še en problem je, da je stanje baze med izvajanjem transakcije (pred potrditvijo) lahko neusklajeno, zato je najbolje, da druge transakcije nimajo dostopa do neusklajenega stanja baze podatkov. To se v relacijskih bazah doseže na več načinov: s sistemom zajemanja trenutnih podatkov, večrazličnimi različicami vrstic itd.

Pri sočasnem izvajanju transakcij je pomembno, da si ne morejo preprečevati. To je lastnost izolacije.

SQL določa 4 ravni izolacije:

  • NEPREBERENO
  • PREBERENO POTRJENO
  • PONOVLJENO PREBERENO
  • SERIALIZABILNO

Preglejmo vsako raven posebej. Stroški izvajanja vsake ravni se nekoliko povišujejo eksponentno.

NEPREBERENO — to je najnižja raven izolacije, vendar je tudi najhitrejša. Transakcije lahko berejo spremembe, ki jih je naredil drug.

PREBERENO POTRJENO — to je naslednja raven izolacije, ki predstavlja kompromis. Transakcije ne morejo brati sprememb, ki jih je naredil drug, do potrditve, lahko pa berejo vse spremembe, ki so nastale po potrditvi.

Če imamo dolgo transakcijo T1, med katero so bile izvedene potrditve v transakcijah T2, T3 … Tn, ki so delovale s tistimi podatki, s katerimi je delovala T1, bomo ob zahtevi po podatkih v T1 vsakič dobili drugačen rezultat. Ta pojav se imenuje neponovljivo branje.

PONOVLJENO PREBERENO — pri tej ravni izolacije nimamo pojava neponovljivega branja, saj se za vsako zahtevo po branju podatkov ustvari zajem podatkov, in pri ponovnem uporabi v tej isti transakciji se uporabijo podatki iz zajema. Vendar pa pri tej ravni izolacije lahko pride do branja fantomskih podatkov. To pomeni branje novih vrstic, ki so jih dodale sočasno potrjene transakcije.

SERIALIZABILNO — najvišja raven izolacije. Značilna je po tem, da so podatki, na kakršen koli način uporabljeni v transakciji (branje ali spreminjanje), drugim transakcijam dostopni šele po zaključku prve transakcije.

Najprej ugotovimo, ali obstaja izolacija operacij v transakciji od glavnega toka. Odprimo 2 terminalna okna.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Izolacija ne obstaja. En tok vidi, kaj drugi dela z odprto transakcijo.

Poglejmo, ali različni tori transakcij vidijo, kaj se dogaja v njih.

Odprimo 2 terminalna okna in odprite 2 transakciji hkrati.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Paralelne transakcije vidijo podatke druga druge. Tako smo dobili najpreprostejšo, a tudi najhitrejšo raven izolacije READ UNCOMMITTED.

To je bilo v načelu mogoče pričakovati za globalne spremenljivke, za katere je bila vedno prioriteta hitrost.

Kaj pa, če potrebujemo višjo raven izolacije pri operacijah z globalnimi spremenljivkami?

Tukaj se moramo vprašati, zakaj sploh potrebujemo ravni izolacije in kako delujejo.

Najvišja raven izolacije SERIALIZE pomeni, da je rezultat paralelno izvršenih transakcij enakovreden njihovemu zaporednemu izvrševanju, kar zagotavlja odsotnost kolizij.

To lahko dosežemo z ustreznimi zaklepanji v ObjectScript, ki ponujajo veliko različnih načinov uporabe: lahko izvajamo običajna, inkrementalna, večkratna zaklepanja s pomočjo ukaza LOCK.

Nizke ravni izolacije so kompromisi, namenjeni povečanju hitrosti delovanja baze podatkov.

Poglejmo, kako lahko dosežemo različne ravni izolacije s pomočjo zaklepanj.

Ta ukaz omogoča pridobivanje ne le ekskluzivnih zaklepanj, potrebnih za spremembo podatkov, temveč tudi tako imenovanih deljenih zaklepanj, ki jih lahko hkrati pridobi več tokov, ko morajo brati podatke, ki jih drugi procesi ne smejo spreminjati med branjem.

Več informacij o dvofazni metodi zaklepanja v ruščini in angleščini:

→ Dvofazno zaklepanje
→ Two-phase locking

Težava je v tem, da je med transakcijo stanje baze lahko neusklajeno, vendar so ti neusklajeni podatki vidni drugim procesom. Kako se temu izogniti?

Z zaklepanji bomo ustvarili taka okna vidnosti, v katerih bo stanje baze usklajeno. Vse zadeve do takih oken vidnosti usklajenega stanja bodo pod nadzorom zaklepanj.

Delična zaklepanja istih podatkov so večkratna — lahko jih prevzamejo več procesov. Ta zaklepanja drugim procesom prepovedujejo spremembo podatkov, torej se uporabljajo za oblikovanje okna usklajenega stanja Baze podatkov.

Izključna zaklepanja se uporabljajo za spremembe podatkov — to zaklepanje lahko prevzame samo en proces. Izključna zaklepanja lahko prevzame:

  1. Katerikoli proces, če so podatki prosti.
  2. Samo tisti proces, ki ima za te podatke deljeno zaklepanje in je prvi zaprosil za izključno zaklepanje.

Transakcije u globalima InterSystems IRIS

Tem bolje je okno vidnosti, tem dlje morajo drugi procesi čakati, toda tem bolj usklajeno lahko je stanje Baze podatkov v njem.

READ_COMMITED — bistvo te ravni je, da vidimo samo potrjene podatke iz drugih tokov. Če podatki v drugi transakciji še niso potrjeni, vidimo njihovo prejšnjo različico.

To nam omogoča, da razporedimo delo namesto čakanja na sprostitev zaklepanja.

Brez posebnih zank ne bomo mogli videti prejšnje različice podatkov v IRIS-u, zato se bomo morali zadovoljiti z zaklepanji.

Zadostiti bomo morali z deljenimi zaklepanji, da dovolite branje podatkov le v trenutkih usklajenosti.

Recimo, da imamo bazo uporabnikov ^person, ki si med seboj prenašajo denar.

Trenutek prenosa od osebe 123 k osebi 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)

Trenutek zahteve za količino denarja pri osebi 123 pred odbitkom mora spremljati izključno zaklepanje (privzeto):

LOCK +^person(123)
Write ^person(123)

Če pa želimo prikazati stanje računa v osebnem računu, lahko uporabimo deljeno zaklepanje ali pa je sploh ne uporabimo:

LOCK +^person(123)#”S”
Write ^person(123)

Vendar pa, če predpostavimo, da se operacije z Bazo podatkov izvajajo praktično takoj (spomnim vas, da so globalne spremenljivke veliko bolj nizko nivojske strukture kot relacijska tabela), potreba po tej ravni pade.

PONOVLJENO PREBERENO — na tej ravni izolacije je dovoljeno, da obstajajo več branj podatkov, ki jih lahko spreminjajo vzporedne transakcije.

Zato bomo morali postaviti deljeno zaklepanje za branje podatkov, ki jih spreminjamo, in izključna zaklepanja za podatke, ki jih spreminjamo.

Operator LOCK allows you to list in detail all necessary locks within a single operator, which can be quite numerous.

LOCK +^person(123, amount)#”S”
reading ^person(123, amount)

other operations (at this time, parallel threads are trying to change ^person(123, amount), but cannot)

LOCK +^person(123, amount)
modifying ^person(123, amount)
LOCK -^person(123, amount)

reading ^person(123, amount)
LOCK -^person(123, amount)#”S”

When listing locks separated by commas, they are taken sequentially, but if done like this:

LOCK +(^person(123),^person(242))

they are taken atomically all at once.

SERIALIZE — we will need to set locks in such a way that, ultimately, all transactions that have common data are executed sequentially. For this approach, most locks must be exclusive and taken over the smallest areas of the global for performance.

However, when it comes to withdrawals in the global ^person, only the SERIALIZE isolation level is acceptable, as money must be spent strictly sequentially, otherwise it is possible to spend the same amount multiple times.

4. Longevity

I conducted tests with hard container shutdown using

docker kill my-iris

The database handled this well. No problems were identified.

Zaključek

For globals in InterSystems IRIS, transaction support is available. They are truly atomic, reliable. However, to ensure database consistency in globals, programmer effort and the use of transactions are necessary, as there are no complex built-in constructs like foreign keys.

The isolation level for globals without using locks is READ UNCOMMITTED, and with locks, it can be ensured up to the SERIALIZE level.

The correctness and speed of transactions on globals heavily depend on the programmer's skill: the more widely shared locks are used for reading, the higher the isolation level, and the narrower exclusive locks are taken, the greater the performance.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster