SGBD InterSystems IRIS susține structuri interesante pentru stocarea datelor - globale. Practic, acestea sunt chei multilaterale cu diverse avantaje suplimentare, precum tranzacții, funcții rapide pentru navigarea în arborii de date, blocaje și un limbaj propriu, ObjectScript.
Mai multe despre globale în seria de articole „Globale - săbii cu două tăișuri pentru stocarea datelor”:
M-a interesat cum sunt implementate tranzacțiile în globale, ce caracteristici au. Este o structură complet diferită pentru stocarea datelor, comparativ cu tabelele obișnuite. Mult mai la un nivel de bază.
După cum se știe din teoria bazelor de date relaționale, o implementare bună a tranzacțiilor ar trebui să îndeplinească cerințele :
A - Atomic (atomaritate). Toate modificările efectuate în tranzacție trebuie înregistrate sau niciuna.
C - Consistency (consistență). După finalizarea tranzacției, starea logică a bazei de date trebuie să fie intern coerentă. În mare parte, această cerință se referă la programator, dar în cazul bazelor de date SQL se referă și la cheile externe.
I - Isolate (izolare). Tranzacțiile care se execută simultan nu trebuie să se influențeze reciproc.
D - Durable (durabilitate). După finalizarea cu succes a tranzacției, problemele de la nivelurile inferioare (de exemplu, o cădere a alimentării) nu trebuie să afecteze datele modificate de tranzacție.
Globalele sunt structuri de date nerelaționale. Au fost create pentru a funcționa extrem de rapid pe hardware foarte limitat. Să analizăm implementarea tranzacțiilor în globale folosind .
Pentru a susține tranzacțiile în IRIS, sunt folosite comenzile: , , .
1. Atomaritate
Cel mai ușor de verificat este atomaritatea. Verificăm din consola bazei de date.
Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMITApoi facem concluzia:
Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)Vom obține:
1 2 3Totul este în regulă. Atomaritatea a fost respectată: toate modificările au fost înregistrate.
Să complicăm puțin sarcina, introducând o eroare și să vedem cum se va păstra tranzacția, parțial sau deloc.
Verificăm din nou atomaritatea:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3După care vom opri forțat containerul, îl vom porni și vom verifica.
docker kill my-irisAceastă comandă este practic echivalentă cu oprirea forțată a alimentării, deoarece trimite semnalul de oprire imediată a procesului SIGKILL.
Poate că tranzacția a fost salvată parțial?
WRITE ^a(1), ^a(2), ^a(3)
^
^a(1)— Nu, nu s-a salvat.
Să testăm comanda de rollback:
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)Nici asta nu s-a salvat.
2. Consistența
Deoarece în bazele de date pe globale, cheile sunt de asemenea create pe globale (amintesc că globalul este o structură de nivel inferior pentru stocarea datelor, comparativ cu o tabelă relațională), pentru a îndeplini cerința de consistență, modificarea cheii trebuie inclusă în aceeași tranzacție cu modificarea globalului.
De exemplu, avem globalul ^person, în care stocăm persoanele și folosim ca și cheie CNP-ul.
^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
...Pentru a avea căutări rapide după nume și prenume, am creat cheia ^index.
^index('Kamenev', 'Sergey', 1234567) = 1Pentru ca baza de date să fie consistentă, trebuie să adăugăm persoana astfel:
TSTART
^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
^index('Kamenev', 'Sergey', 1234567) = 1
TCOMMITPrin urmare, la ștergere, trebuie de asemenea să folosim o tranzacție:
TSTART
Kill ^person(1234567)
ZKill ^index('Kamenev', 'Sergey', 1234567)
TCOMMITCu alte cuvinte, îndeplinirea cerinței de consistență este complet pe umerii programatorului. Dar când vine vorba de globale, este normal, din cauza naturii lor de nivel inferior.
3. Izolarea
Aici încep problemele. Mulți utilizatori lucrează simultan asupra aceleași baze de date, modificând aceleași date.
Situația este comparabilă cu aceea în care mulți utilizatori lucrează simultan cu același repository de cod și încearcă să comiteze modificări în multe fișiere în același timp.
Baza de date trebuie să gestioneze totul în timp real. Având în vedere că în companiile mari există chiar o persoană responsabilă de controlul versiunilor (pentru a gestiona fuziunile ramurilor, rezolvarea conflictelor etc.), iar baza de date trebuie să facă tot acest lucru în timp real, devine evidentă complexitatea sarcinii și corectitudinea proiectării bazei de date și a codului care o întreține.
O bază de date nu poate înțelege sensul acțiunilor efectuate de utilizatori pentru a preveni conflictele atunci când aceștia lucrează cu aceleași date. Aceasta poate anula o tranzacție care contrazice alta sau poate să le execute secvențial.
O altă problemă este că, în timpul execuției unei tranzacții (până la commit), starea bazei de date poate fi inconsistenta. Din acest motiv, este de dorit ca alte tranzacții să nu aibă acces la starea inconsistentă a bazei de date, ceea ce se realizează în bazele de date relaționale prin mai multe metode: crearea de snapshot-uri, gestionarea versiunilor multiple a rândurilor etc.
Când tranzacțiile sunt executate în paralel, este important să nu se interfereze reciproc. Acesta este ceea ce se numește proprietatea izolației.
SQL definește 4 niveluri de izolație:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
Să analizăm fiecare nivel în parte. Costurile de implementare ale fiecărui nivel cresc aproape exponențial.
READ UNCOMMITTED — acesta este cel mai scăzut nivel de izolație, dar și cel mai rapid. Tranzacțiile pot citi modificările efectuate de celelalte.
READ COMMITTED — acesta este următorul nivel de izolație, care reprezintă un compromis. Tranzacțiile nu pot citi modificările efectuate de celelalte până la commit, dar pot citi orice modificări efectuate după commit.
Dacă avem o tranzacție lungă T1, în timpul căreia au avut loc commit-uri în tranzacțiile T2, T3... Tn, care au lucrat cu aceleași date ca și T1, atunci la solicitarea datelor din T1, vom obține de fiecare dată un rezultat diferit. Acest fenomen se numește citire ne-repetabilă.
REPEATABLE READ — în acest nivel de izolație, nu avem fenomenul citirii ne-repetabile, datorită faptului că pentru fiecare solicitare de citire a datelor se creează un snapshot al rezultatelor, iar pentru reutilizarea în aceeași tranzacție se folosesc datele din snapshot. Totuși, în acest nivel de izolație este posibilă citirea datelor fantomă. Aceasta se referă la citirea unor rânduri noi, care au fost adăugate de tranzacții paralele confirmate.
SERIALIZABLE — cel mai înalt nivel de izolație. Acesta se caracterizează prin faptul că datele utilizate în moduri diferite în tranzacție (citire sau modificare) devin accesibile altor tranzacții doar după finalizarea primei tranzacții.
Pentru început, să ne asigurăm dacă există izolare a operațiunilor în tranzacție față de fluxul principal. Vom deschide 2 feronete de terminal.
Kill ^t
Write ^t(1)
2
TSTART
Set ^t(1)=2Nu există izolare. Un flux poate vedea ce face celălalt care a deschis tranzacția.
Să vedem dacă tranzacțiile din fluxuri diferite văd ceea ce se întâmplă în interiorul lor.
Vom deschide 2 feronete de terminal și vom deschide 2 tranzacții în paralel.
kill ^t
TSTART
Write ^t(1)
3
TSTART
Set ^t(1)=3
Tranzacțiile paralele văd datele unui alt. Așadar, am obținut cel mai simplu, dar și cel mai rapid nivel de izolare READ UNCOMMITED.
În principiu, acest lucru era de așteptat pentru globale, pentru care performanța a fost întotdeauna prioritară.
Ce facem dacă avem nevoie de un nivel mai înalt de izolare în operațiunile globale?
Aici trebuie să ne gândim de ce avem nevoie de niveluri de izolare și cum funcționează ele.
Cel mai înalt nivel de izolare SERIALIZE înseamnă că rezultatul tranzacțiilor executate în paralel este echivalent executării lor secvențial, ceea ce garantează absența coliziunilor.
Acest lucru putem realiza prin blocări inteligente în ObjectScript, care au o mulțime de metode diverse de aplicare: putemface o blocare obișnuită, incrementală sau multiplă cu comanda .
Nivelurile mai scăzute de izolare sunt compromituri menite să crească viteza de funcționare a bazei de date.
Să vedem cum putem obține diferite niveluri de izolare cu ajutorul blocărilor.
Acest operator permite obținerea nu doar a blocărilor exclusive, necesare pentru modificarea datelor, ci și a celor denumite shared, care pot fi obținute simultan de mai multe fluxuri atunci când trebuie să citească date care nu ar trebui să fie modificate de alte procese în timpul citirii.
Mai multe informații despre metoda de blocare în două faze în limbile română și engleză:
→
→
Dificultatea constă în faptul că, în timpul tranzacției, starea bazei poate fi nesolicitată, însă aceste date nesolicitate sunt vizibile altor procese. Cum putem evita asta?
Vom crea prin blocări feronete de vizibilitate în care starea bazei va fi conformă. Și toate apelurile la aceste feronete de vizibilitate a stării conforme vor fi controlate prin intermediul blocărilor.
Blocările partajate de date sunt reutilizabile — pot fi accesate de mai multe procese. Aceste blocări împiedică alte procese să modifice datele, adică sunt folosite pentru a forma feroneria unui stat coerent al bazei de date.
Blocările exclusive sunt utilizate pentru modificarea datelor — o astfel de blocare poate fi obținută doar de un singur proces. O blocare exclusivă poate fi obținută de:
- Orice proces, dacă datele sunt libere
- Numai procesul care are pe aceste date o blocare partajată și a cerut prima dată blocarea exclusivă.

Cu cât fereastra de vizibilitate este mai îngustă, cu atât trebuie să aștepte mai mult alte procese, dar cu atât mai coerent poate fi statul bazei de date în ea.
READ_COMMITED — esența acestui nivel este că vedem doar datele confirmate din alte fluxuri. Dacă datele dintr-o altă tranzacție nu sunt încă confirmate, atunci vedem versiunea lor veche.
Aceasta ne permite să desfășurăm lucrările în paralel în loc să așteptăm eliberarea blocării.
Fără trucuri speciale, nu vom putea vedea versiunea veche a datelor în IRIS, așa că va trebui să ne descurcăm cu blocările.
Prin urmare, va fi necesar să permitem citirea datelor doar în momentele de coerență cu blocările partajate.
Să presupunem că avem o bază de date a utilizatorilor ^person, care își transferă bani între ei.
Momentul transferului de la persoana 123 la persoana 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)Momentul cererii sumei de bani la persoana 123 înainte de debitare trebuie să fie însoțit de o blocare exclusivă (implicit):
LOCK +^person(123)
Write ^person(123)Și dacă trebuie să arătăm starea contului în spațiul personal, putem folosi o blocare partajată sau să nu o folosim deloc:
LOCK +^person(123)#”S”
Write ^person(123)Cu toate acestea, dacă presupunem că operațiile cu baza de date se efectuează practic instantaneu (să reamintim că globalele sunt o structură de nivel mai scăzut decât tabelul relațional), atunci necesitatea acestui nivel scade.
REPEATABLE READ — la acest nivel de izolare se permite faptul că pot exista mai multe citiri de date care pot fi modificate de tranzacții paralele.
Prin urmare, va trebui să punem o blocare partajată la citirea datelor pe care le modificăm și blocări exclusive pe datele pe care le schimbăm.
Operatorul LOCK permite enumerarea detaliată a tuturor blocărilor necesare printr-un singur operator, care poate fi foarte numeros.
LOCK +^person(123, amount)#”S”
lectură ^person(123, amount)alte operațiuni (în acest timp, firele paralele încearcă să modifice ^person(123, amount), dar nu pot)
LOCK +^person(123, amount)
modificare ^person(123, amount)
LOCK -^person(123, amount)
lectură ^person(123, amount)
LOCK -^person(123, amount)#”S”Când enumerăm blocările folosind virgulă, acestea sunt preluate secvențial, iar dacă se face astfel:
LOCK +(^person(123),^person(242))atunci acestea sunt preluate atomic toate deodată.
SERIALIZE — va trebui să stabilim blocările astfel încât, în final, toate tranzacțiile care au date comune să fie executate secvențial. Pentru această abordare, majoritatea blocărilor trebuie să fie exclusive și preluate pe cele mai mici zone globale pentru performanță.
Dacă vorbim despre retragerile de fonduri în globalul ^person, atunci pentru acesta este acceptabil doar nivelul de izolare SERIALIZE, deoarece banii trebuie cheltuiți strict secvențial, altfel este posibil să cheltuim aceeași sumă de mai multe ori.
4. Durabilitate
Am efectuat teste cu întreruperea bruscă a containerului prin
docker kill my-irisBaza s-a descurcat bine. Nu s-au identificat probleme.
Concluzie
Pentru globaluri în InterSystems IRIS există suport pentru tranzacții. Acestea sunt cu adevărat atomice, sigure. Pentru a asigura coerența BDR în globaluri, sunt necesare eforturile programatorului și utilizarea tranzacțiilor, deoarece nu există construcții complexe încorporate, cum ar fi cheile externe.
Nivelul de izolare al globalurilor fără utilizarea blocărilor este READ UNCOMMITED, iar prin utilizarea blocărilor se poate asigura inclusiv la nivelul SERIALIZE.
Corectitudinea și viteza de funcționare a tranzacțiilor pe globaluri depind foarte mult de priceperea programatorului: cu cât se folosesc mai mult blocările shared la citire, cu atât nivelul de izolare este mai mare, iar cu cât se iau mai puțin blocările exclusive, cu atât mai multă rapiditate.
Sursa: habr.com
