Tranzacții în InterSystems IRIS globale

Tranzacții în InterSystems IRIS globaleInterSystems IRIS DBMS suportă structuri interesante pentru stocarea datelor - globale. În esență, acestea sunt chei cu mai multe niveluri cu diverse bunătăți suplimentare sub formă de tranzacții, funcții rapide pentru parcurgerea arborilor de date, încuietori și propriul limbaj ObjectScript.

Citiți mai multe despre globaluri în seria de articole „Globalurile sunt săbii de comori pentru stocarea datelor”:

Copaci. Partea 1
Copaci. Partea 2
Matrice rare. Partea 3

Am devenit interesat de modul în care tranzacțiile sunt implementate în global, ce caracteristici există. La urma urmei, aceasta este o structură complet diferită pentru stocarea datelor decât tabelele obișnuite. Nivel mult mai jos.

După cum se știe din teoria bazelor de date relaționale, o bună implementare a tranzacțiilor trebuie să satisfacă cerințele ACID:

A - Atomic (atomicitate). Toate modificările efectuate în tranzacție sau niciuna sunt înregistrate.

C - Consecvență. După finalizarea unei tranzacții, starea logică a bazei de date trebuie să fie consecventă intern. În multe privințe, această cerință se referă la programator, dar în cazul bazelor de date SQL se referă și la cheile străine.

I - Izolați. Tranzacțiile care rulează în paralel nu ar trebui să se afecteze reciproc.

D - Durabil. După finalizarea cu succes a unei tranzacții, problemele de la niveluri inferioare (penea de curent, de exemplu) nu ar trebui să afecteze datele modificate de tranzacție.

Globalurile sunt structuri de date non-relaționale. Au fost concepute pentru a rula foarte rapid pe hardware foarte limitat. Să ne uităm la implementarea tranzacțiilor în globals folosind imagine oficială IRIS docker.

Pentru a susține tranzacțiile în IRIS, sunt utilizate următoarele comenzi: Începeți, TCOMMIT, TROLLBACK.

1. Atomicitate

Cel mai simplu mod de a verifica este atomicitatea. Verificăm din consola bazei de date.

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

Apoi concluzionam:

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

Primim:

1 2 3

Totul e bine. Atomicitatea este menținută: toate modificările sunt înregistrate.

Să complicăm sarcina, să introducem o eroare și să vedem cum este salvată tranzacția, parțial sau deloc.

Să verificăm din nou atomicitatea:

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

Apoi vom opri cu forță containerul, îl vom lansa și vom vedea.

docker kill my-iris

Această comandă este aproape echivalentă cu o oprire forțată, deoarece trimite un semnal SIGKILL pentru a opri imediat procesul.

Poate tranzacția a fost salvată parțial?

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

- Nu, nu a supraviețuit.

Să încercăm comanda rollback:

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

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

Nimic nu a supraviețuit.

2. Consecvență

Deoarece în bazele de date bazate pe globale, cheile sunt făcute și pe globale (permiteți-mi să vă reamintesc că un global este o structură de nivel inferior pentru stocarea datelor decât un tabel relațional), pentru a îndeplini cerința de consistență, trebuie inclusă o modificare a cheii. în aceeași tranzacție ca o modificare a globalului.

De exemplu, avem o ^persoană globală, în care stocăm personalități și folosim TIN-ul ca cheie.

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

Pentru a avea o căutare rapidă după nume și prenume, am făcut tasta ^index.

^index(‘Kamenev’, ‘Sergey’, 1234567) = 1

Pentru ca baza de date să fie consecventă, trebuie să adăugăm personajul astfel:

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

În consecință, la ștergere trebuie să folosim și o tranzacție:

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

Cu alte cuvinte, îndeplinirea cerinței de consistență depinde în întregime de umerii programatorului. Dar când vine vorba de globale, acest lucru este normal, datorită naturii lor de nivel scăzut.

3. Izolarea

Aici încep sălbăticia. Mulți utilizatori lucrează simultan pe aceeași bază de date, schimbând aceleași date.

Situația este comparabilă cu cea în care mulți utilizatori lucrează simultan cu același depozit de cod și încearcă să comite simultan modificări la mai multe fișiere simultan.

Baza de date ar trebui să rezolve totul în timp real. Având în vedere că în companiile serioase există chiar și o persoană specială care este responsabilă de controlul versiunilor (pentru fuzionarea sucursalelor, rezolvarea conflictelor etc.), iar baza de date trebuie să facă toate acestea în timp real, complexitatea sarcinii și corectitudinea sarcinii. designul bazei de date și codul care o servește.

Baza de date nu poate înțelege semnificația acțiunilor efectuate de utilizatori pentru a evita conflictele dacă aceștia lucrează pe aceleași date. Poate anula doar o tranzacție care intră în conflict cu alta sau le poate executa secvenţial.

O altă problemă este că în timpul execuției unei tranzacții (înainte de un commit), starea bazei de date poate fi inconsecventă, deci este de dorit ca alte tranzacții să nu aibă acces la starea inconsecventă a bazei de date, ceea ce se realizează în bazele de date relaționale. în multe feluri: crearea de instantanee, rânduri cu versiuni multiple și etc.

Atunci când executăm tranzacții în paralel, este important pentru noi ca acestea să nu interfereze între ele. Aceasta este proprietatea izolării.

SQL definește 4 niveluri de izolare:

  • CITIȚI NECOMITAT
  • CITIȚI ANGAJATE
  • CITIRE REPETABILĂ
  • SERIALIZABIL

Să ne uităm la fiecare nivel separat. Costurile implementării fiecărui nivel cresc aproape exponențial.

CITIȚI NECOMITAT - acesta este cel mai scăzut nivel de izolare, dar în același timp și cel mai rapid. Tranzacțiile pot citi modificările făcute unul de celălalt.

CITIȚI ANGAJATE este următorul nivel de izolare, care este un compromis. Tranzacțiile nu pot citi modificările reciproce înainte de comitare, dar pot citi orice modificări făcute după comitare.

Dacă avem o tranzacție lungă T1, în timpul căreia au avut loc commit-uri în tranzacțiile T2, T3 ... Tn, care a funcționat cu aceleași date ca și T1, atunci când solicităm date în T1 vom obține un rezultat diferit de fiecare dată. Acest fenomen se numește citire nerepetabilă.

CITIRE REPETABILĂ — în acest nivel de izolare nu avem fenomenul de citire nerepetabilă, datorită faptului că pentru fiecare solicitare de citire a datelor se creează un instantaneu al datelor rezultat și când sunt reutilizate în aceeași tranzacție, datele din instantaneu. este folosit. Cu toate acestea, este posibil să citiți date fantomă la acest nivel de izolare. Aceasta se referă la citirea noilor rânduri care au fost adăugate prin tranzacții angajate în paralel.

SERIALIZABIL — cel mai înalt nivel de izolare. Se caracterizează prin faptul că datele utilizate în orice mod într-o tranzacție (citire sau modificare) devin disponibile altor tranzacții numai după finalizarea primei tranzacții.

În primul rând, să ne dăm seama dacă există o izolare a operațiunilor dintr-o tranzacție de firul principal. Să deschidem 2 ferestre de terminal.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Nu există izolare. Un fir vede ce face cel de-al doilea care a deschis tranzacția.

Să vedem dacă tranzacțiile cu fire diferite văd ce se întâmplă în interiorul lor.

Să deschidem 2 ferestre de terminal și să deschidem 2 tranzacții în paralel.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Tranzacțiile paralele văd reciproc datele. Așadar, am obținut cel mai simplu, dar și cel mai rapid nivel de izolare, CITEȘTE NEANGAJAT.

În principiu, acest lucru ar putea fi de așteptat pentru globale, pentru care performanța a fost întotdeauna o prioritate.

Ce se întâmplă dacă avem nevoie de un nivel mai ridicat de izolare în operațiunile pe global?

Aici trebuie să vă gândiți de ce sunt necesare niveluri de izolare și cum funcționează acestea.

Cel mai înalt nivel de izolare, SERIALIZE, înseamnă că rezultatul tranzacțiilor executate în paralel este echivalent cu executarea lor secvențială, ceea ce garantează absența coliziunilor.

Putem face acest lucru folosind blocări inteligente în ObjectScript, care au o mulțime de utilizări diferite: puteți face blocare regulată, incrementală, multiplă cu comanda LOCK.

Nivelurile mai mici de izolare sunt compromisuri concepute pentru a crește viteza bazei de date.

Să vedem cum putem atinge diferite niveluri de izolare folosind încuietori.

Acest operator vă permite să luați nu numai blocări exclusive necesare pentru modificarea datelor, ci și așa-numitele blocări partajate, care pot prelua mai multe fire în paralel atunci când au nevoie să citească date care nu ar trebui modificate de alte procese în timpul procesului de citire.

Mai multe informații despre metoda de blocare în două faze în rusă și engleză:

Blocare în două faze
Blocare în două faze

Dificultatea este că în timpul unei tranzacții starea bazei de date poate fi inconsecventă, dar aceste date inconsistente sunt vizibile altor procese. Cum să evitați acest lucru?

Folosind încuietori, vom crea ferestre de vizibilitate în care starea bazei de date va fi consistentă. Și orice acces la astfel de ferestre de vizibilitate a statului convenit va fi controlat de încuietori.

Blocările partajate pentru aceleași date sunt reutilizabile – mai multe procese le pot lua. Aceste blocări împiedică alte procese să modifice datele, de ex. sunt folosite pentru a forma ferestre cu starea bazei de date consistente.

Blocările exclusive sunt folosite pentru modificările datelor - doar un proces poate lua o astfel de blocare. O blocare exclusivă poate fi luată de:

  1. Orice proces dacă datele sunt gratuite
  2. Doar procesul care are o blocare partajată asupra acestor date și a fost primul care a solicitat o blocare exclusivă.

Tranzacții în InterSystems IRIS globale

Cu cât fereastra de vizibilitate este mai îngustă, cu atât celelalte procese trebuie să o aștepte mai mult, dar cu atât starea bazei de date din cadrul acesteia poate fi mai consistentă.

READ_COMMITTED — esența acestui nivel este că vedem numai date comise din alte fire. Dacă datele dintr-o altă tranzacție nu au fost încă comise, atunci vedem versiunea veche a acesteia.

Acest lucru ne permite să paralelizăm lucrarea în loc să așteptăm ca încuietoarea să fie eliberată.

Fără trucuri speciale, nu vom putea vedea versiunea veche a datelor în IRIS, așa că va trebui să ne descurcăm cu încuietori.

În consecință, va trebui să folosim blocări partajate pentru a permite citirea datelor numai în momente de consecvență.

Să presupunem că avem o bază de utilizatori ^persoană care își transferă bani unul altuia.

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 solicitării sumei de bani de 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ă afișați starea contului în contul dvs. personal, atunci puteți utiliza o blocare partajată sau nu o puteți folosi deloc:

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

Totuși, dacă presupunem că operațiunile cu bazele de date sunt efectuate aproape instantaneu (permiteți-mi să vă reamintesc că globalurile sunt o structură de nivel mult mai scăzut decât un tabel relațional), atunci necesitatea acestui nivel scade.

CITIRE REPETABILĂ - Acest nivel de izolare permite citiri multiple de date care pot fi modificate prin tranzacții concurente.

În consecință, va trebui să punem o blocare partajată pentru citirea datelor pe care le modificăm și blocări exclusive asupra datelor pe care le schimbăm.

Din fericire, operatorul LOCK vă permite să enumerați în detaliu toate încuietorile necesare, dintre care pot fi multe, într-o singură declarație.

LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)

alte operațiuni (în acest moment firele paralele încearcă să schimbe ^person(123, amount), dar nu pot)

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

Când se afișează încuietori separate prin virgule, acestea sunt luate secvențial, dar dacă procedați astfel:

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

apoi sunt luate atomic deodată.

SERIALIZAȚI — va trebui să setăm blocări astfel încât, în cele din urmă, toate tranzacțiile care au date comune să fie executate secvenţial. Pentru această abordare, cele mai multe încuietori ar trebui să fie exclusive și luate pe cele mai mici zone ale globale pentru performanță.

Dacă vorbim despre debitarea fondurilor la ^persoana globală, atunci doar nivelul de izolare SERIALIZE este acceptabil pentru aceasta, deoarece banii trebuie cheltuiți strict secvențial, altfel este posibil să cheltuiți aceeași sumă de mai multe ori.

4. Durabilitate

Am efectuat teste cu tăierea dură a recipientului folosind

docker kill my-iris

Baza le-a tolerat bine. Nu au fost identificate probleme.

Concluzie

Pentru global, InterSystems IRIS are suport pentru tranzacții. Sunt cu adevărat atomici și de încredere. Pentru a asigura consecvența unei baze de date bazate pe valori globale, sunt necesare eforturi ale programatorului și utilizarea tranzacțiilor, deoarece nu are structuri complexe încorporate, cum ar fi cheile externe.

Nivelul de izolare a globalurilor fără a utiliza blocări este READ UNCOMMITED, iar atunci când se utilizează blocaje poate fi asigurat până la nivelul SERIALIZE.

Corectitudinea și viteza tranzacțiilor pe global depind foarte mult de abilitățile programatorului: cu cât se folosesc încuietori mai larg partajate la citire, cu atât este mai mare nivelul de izolare și cu cât sunt luate încuietori mai strict exclusive, cu atât performanța este mai rapidă.

Sursa: www.habr.com

Adauga un comentariu