Transaktioner i globala InterSystems IRIS

Transaktioner i globala InterSystems IRISDatabashanteraren InterSystems IRIS stödjer intressanta strukturer för datalagring – globala variabler. I grunden Ă€r detta flernivĂ„nycklar med olika ytterligare fördelar som transaktioner, snabba funktioner för att navigera i datatrĂ€d, lĂ„sningar och sitt eget sprĂ„k ObjectScript.

Mer om globala variabler i en serie artiklar "Globala variabler – svĂ€rd och skatter för datalagring":

TrÀd. Del 1
TrÀd. Del 2
Sparsamma arrayer. Del 3

Jag blev nyfiken pÄ hur transaktioner Àr implementerade i globala variabler, vilka sÀrdrag de har. Det Àr ju en helt annan struktur för datalagring Àn de vanliga tabellerna. Mycket mer lÄg nivÄ.

Som kÀnt frÄn teorin om relationsdatabaser ska en bra implementation av transaktioner uppfylla kraven ACID:

A – Atomic (atomĂ€r). Alla förĂ€ndringar som görs i en transaktion ska antingen sparas eller inte sparas alls.

C – Consistency (konsekvens). Efter avslutandet av en transaktion ska den logiska tillstĂ„ndet i databasen vara internt konsekvent. Mycket av detta krav berör programmeraren, men i fallet med SQL-databaser handlar det Ă€ven om frĂ€mmande nycklar.

I – Isolate (isolering). Parallellt körande transaktioner ska inte pĂ„verka varandra.

D – Durable (hĂ„llbarhet). Efter att transaktionen framgĂ„ngsrikt avslutats ska problem pĂ„ lĂ€gre nivĂ„er (som strömavbrott, till exempel) inte pĂ„verka de data som förĂ€ndrats av transaktionen.

Globala variabler Àr icke-relationella datastrukturer. De skapades för super snabb drift pÄ mycket begrÀnsad hÄrdvara. LÄt oss titta pÄ implementeringen av transaktioner i globala variabler med hjÀlp av den officiella Docker-bilden för IRIS.

För att stödja transaktioner i IRIS anvÀnds kommandon: TSTART, TCOMMIT, TROLLBACK.

1. AtomÀritet

Det Àr enklast att kontrollera atomÀritet. Vi testar frÄn databasens konsol.

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

Sedan gör vi en utskrift:

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

Vi fÄr:

1 2 3

Allt Àr i sin ordning. AtomÀritet Àr upprÀtthÄllen: alla förÀndringar har sparats.

LÄt oss komplicera uppgiften, introducera ett fel och titta pÄ hur transaktionen sparas, delvis eller inte alls.

Vi kontrollerar atomÀritet Ànnu en gÄng:

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

DĂ€refter tvingar vi container att stanna, startar den igen och ser efter.

docker kill my-iris

Det hÀr kommandot Àr praktiskt taget likvÀrdigt med att tvinga avstÀngning av strömmen, eftersom det skickar en SIGKILL-avbrottssignal.

Kan transaktionen ha sparats delvis?

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

— Nej, den har inte sparats.

LÄt oss prova ÄterstÀllningskommandot:

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)

Inte heller har nÄgot sparats.

2. Konsistens

Eftersom nycklar i globala databasstrukturer ocksÄ görs pÄ globala nivÄer (kom ihÄg att en global Àr en mer lÄg nivÄ struktur för att lagra data Àn en relationsdatabas), behöver en nyckelförÀndring ingÄ i samma transaktion som förÀndringen av den globala, för att uppfylla kravet pÄ konsistens.

Till exempel har vi en global ^person dÀr vi lagrar personuppgifter och anvÀnder Skatteidentifikationsnummer som nyckel.

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

För att ha en snabb sökning efter efternamn och förnamn har vi skapat nyckeln ^index.

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

För att databasen ska vara konsekvent mÄste vi lÀgga till personuppgifterna sÄ hÀr:

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

DÀrför mÄste vi vid borttagning ocksÄ anvÀnda en transaktion:

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

Med andra ord ligger ansvaret för att uppfylla kravet pĂ„ konsistens helt och hĂ„llet pĂ„ programmerarens axlar. Men nĂ€r det gĂ€ller globala strukturer — Ă€r det normalt, pĂ„ grund av deras lĂ„gnivĂ„natur.

3. Isolering

HÀr börjar komplexiteten. MÄnga anvÀndare arbetar samtidigt med samma databas och Àndrar samma data.

Situationen Àr jÀmförbar med att mÄnga anvÀndare arbetar samtidigt med samma kodförvar och försöker commitera förÀndringar i mÄnga filer samtidigt.

Databasen mÄste hantera allt detta i realtid. Med tanke pÄ att seriösa företag till och med har en speciell person som ansvarar för versionkontroll (för sammanslagning av grenar, konfliktlösning etc.), och att databaser mÄste göra allt detta i realtid, blir uppenbar komplexiteten i uppgiften och vikten av korrekt databasdesign samt koden som stöder den.

En databas kan inte förstÄ innebörden av de ÄtgÀrder som anvÀndarna utför för att förhindra konflikter, om de arbetar med samma data. Den kan bara Äterkalla en transaktion som strider mot en annan eller utföra dem i sekvens.

Ytterligare ett problem Àr att under tiden en transaktion utförs (fram till commit), kan databasens tillstÄnd vara inkonsistent, sÄ det Àr önskvÀrt att andra transaktioner inte har tillgÄng till ett inkonsistent tillstÄnd i databasen, vilket uppnÄs i relationella databaser pÄ mÄnga sÀtt: genom att skapa snapshots, genom multiversionering av rader osv.

Vid parallell körning av transaktioner Àr det viktigt att de inte stör varandra. Detta Àr egenskapen av isolation.

SQL definierar 4 nivÄer av isolation:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

LÄt oss övervÀga varje nivÄ för sig. Kostnaderna för implementering av varje nivÄ ökar nÀstan exponentiellt.

READ UNCOMMITTED — detta Ă€r den lĂ€gsta nivĂ„n av isolation, men samtidigt den snabbaste. Transaktioner kan lĂ€sa förĂ€ndringar som har gjorts av varandra.

READ COMMITTED — detta Ă€r nĂ€sta nivĂ„ av isolation, som Ă€r en kompromiss. Transaktioner kan inte lĂ€sa förĂ€ndringar som har gjorts av varandra fram till commit, men kan lĂ€sa alla förĂ€ndringar som gjorts efter commit.

Om vi har en lÄng transaktion T1, under vilken commits har gjorts i transaktioner T2, T3 
 Tn, som har arbetat med samma data som T1, dÄ kommer vi varje gÄng vi frÄgar efter data i T1 att fÄ ett annat resultat. Detta fenomen kallas icke-repetitiv lÀsning.

REPEATABLE READ — pĂ„ denna isolationsnivĂ„ har vi inget fenomen av icke-repetitiv lĂ€sning, pĂ„ grund av att för varje begĂ€ran om att lĂ€sa data skapas en snapshot av resultatdata och nĂ€r de anvĂ€nds igen i samma transaktion anvĂ€nds data frĂ„n snapshot. Men pĂ„ denna isolationsnivĂ„ Ă€r det möjligt att lĂ€sa fantomdata. Det avser att lĂ€sa nya rader som har lagts till av parallella, fixerade transaktioner.

SERIALIZABLE — den högsta nivĂ„n av isolation. Den kĂ€nnetecknas av att data som pĂ„ nĂ„got sĂ€tt anvĂ€nds i transaktionen (lĂ€sa eller Ă€ndra) blir tillgĂ€ngliga för andra transaktioner först efter att den första transaktionen har avslutats.

LÄt oss börja med att förstÄ om det finns nÄgon isolering av operationerna i transaktionen frÄn huvudflödet. Vi öppnar 2 terminalfönster.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Det finns ingen isolering. En trÄd ser vad den andra som öppnat transaktionen gör.

LÄt oss se om transaktionerna i olika trÄdar ser vad som hÀnder inuti dem.

Vi öppnar 2 terminalfönster och startar 2 transaktioner parallellt.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Parallella transaktioner ser varandras data. SÄ vi har fÄtt den enklaste, men ocksÄ den snabbaste nivÄn av isolation READ UNCOMMITTED.

Detta kunde i princip förvÀntas för globala variabler, dÀr prestanda alltid har satts i första rummet.

Vad gör vi om vi behöver en högre nivÄ av isolering i operationer pÄ globala variabler?

HÀr behöver vi reflektera över varför nivÄer av isolering alls Àr nödvÀndiga och hur de fungerar.

Den högsta nivÄn av isolering SERIALIZE innebÀr att resultatet av parallellt körda transaktioner Àr ekvivalent med deras sekventiella utförande, vilket garanterar att det inte uppstÄr kollisioner.

Detta kan vi göra med hjÀlp av intelligenta lÄs i ObjectScript, som har mÄnga olika anvÀndningsomrÄden: man kan göra vanliga, inkrementella eller flerlÄsningskommandon med LOCK.

LÀgre nivÄer av isolering Àr kompromisser som syftar till att öka databasens prestanda.

LÄt oss se hur vi kan uppnÄ olika nivÄer av isolering med hjÀlp av lÄs.

Denna operator tillÄter att ta inte bara exklusiva lÄs, som behövs för att Àndra data, utan ocksÄ sÄ kallade delade lÄs, som kan tas av flera trÄdar parallellt nÀr de behöver lÀsa data som inte fÄr förÀndras av andra processer under lÀsningen.

Mer om tvÄfasmetoden för lÄsning pÄ ryska och engelska:

→ TvĂ„faslĂ„sning
→ Two-phase locking

SvÄrigheten ligger i att under transaktionen kan databasens tillstÄnd vara inkonsistent, men dessa inkonsistenta data Àr synliga för andra processer. Hur undviker vi detta?

Vi kommer att skapa sÄdana synfönster med lÄs dÀr databasens tillstÄnd kommer att vara konsistent. Och alla Ätkomster till sÄdana synfönster med konsistent tillstÄnd kommer att kontrolleras av lÄs.

Delade lÄsningar av samma data Àr ÄteranvÀndbara - de kan tas av flera processer. Dessa lÄsningar förhindrar andra processer frÄn att Àndra data, dvs. de anvÀnds för att skapa fönster för en överenskommen databasstatus.

Exklusiva lÄsningar anvÀnds för att Àndra data - en sÄdan lÄsning kan endast tas av en process. En exklusiv lÄsning kan tas av:

  1. Vilken som helst process, om data Àr fria
  2. Endast den process som har en shared-lÄsning pÄ dessa data och som först begÀrde den exklusiva lÄsningen.

Transaktioner i globala InterSystems IRIS

Ju mer begrÀnsad synfÀltet Àr, desto lÀngre mÄste andra processer vÀnta, men desto mer överensstÀmmande kan databasens tillstÄnd vara i det.

READ_COMMITED — kĂ€rnan i denna nivĂ„ Ă€r att vi bara ser de begĂ„ngna data frĂ„n andra strömmar. Om data i en annan transaktion Ă€nnu inte Ă€r begĂ„ngna ser vi deras gamla version.

Detta gör att vi kan parallellisera arbetet istÀllet för att vÀnta pÄ att lÄsningen ska slÀppas.

Utan speciella knep kommer vi inte att kunna se den gamla versionen av data i IRIS, sÄ vi mÄste klara oss med lÄsningar.

DÀrför mÄste vi med hjÀlp av shared-lÄsningar tillÄta lÀsning av data endast vid punkter av överensstÀmmelse.

Anta att vi har en anvÀndardatabas ^person, dÀr de överför pengar till varandra.

Överföringsögonblicket frĂ„n person 123 till person 242:

LÅS +^person(123), +^person(242)
StÀll in ^person(123, amount) = ^person(123, amount) - amount
StÀll in ^person(242, amount) = ^person(242, amount) + amount
LÅS -^person(123), -^person(242)

Ögonblicket för att begĂ€ra antalet pengar frĂ„n person 123 innan debitering mĂ„ste Ă„tföljas av en exklusiv lĂ„sning (som standard):

LÅS +^person(123)
Skriv ^person(123)

Om vi behöver visa kontots status i personligt konto kan vi anvÀnda en shared-lÄsning eller helt skippa den:

LÅS +^person(123)#”S”
Skriv ^person(123)

Men om vi antar att operationer med databasen utförs nÀstan omedelbart (jag noterar att globala Àr en mycket mer lÄg nivÄstruktur Àn en relationsdatabas), minskar behovet av denna nivÄ.

REPEATABLE READ — pĂ„ denna isoleringsnivĂ„ tillĂ„ts att det kan finnas flera lĂ€sningar av data som kan Ă€ndras av parallella transaktioner.

DÀrför mÄste vi sÀtta en shared-lÄsning för att lÀsa data som vi Àndrar och exklusiva lÄsningar för de data vi förÀndrar.

Med hjÀlp av LOCK-operatören kan man noggrant ange alla nödvÀndiga lÄs inom en enda operator, eftersom det kan vara vÀldigt mÄnga.

LOCK +^person(123, amount)#”S”
lÀsning ^person(123, amount)

andra operationer (under tiden försöker parallella trÄdar att Àndra ^person(123, amount), men kan inte)

LOCK +^person(123, amount)
Ă€ndra ^person(123, amount)
LOCK -^person(123, amount)

lÀsning ^person(123, amount)
LOCK -^person(123, amount)#”S”

NÀr man listar lÄs med kommatecken tas de i sekvens, men om man gör sÄ hÀr:

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

tilldelas de atomÀrt pÄ en gÄng.

SERIALIZE — vi mĂ„ste stĂ€lla in lĂ„sen sĂ„ att slutligen alla transaktioner som har gemensamma data utförs sekventiellt. För denna metod mĂ„ste de flesta lĂ„s vara exklusiva och tas pĂ„ de minsta omrĂ„dena i globalen för prestanda.

Om vi talar om uttag av medel i globalen ^person, sÄ Àr endast SERIALIZE-isoleringsnivÄn acceptabel, eftersom pengar mÄste spenderas strikt sekventiellt, annars kan samma belopp spenderas flera gÄnger.

4. LÄngvarighet

Jag har genomfört tester med hÄrd avstÀngning av containern genom

docker kill my-iris

Databasen hanterade det bra. Inga problem upptÀcktes.

Slutsats

För globalerna i InterSystems IRIS finns stöd för transaktioner. De Àr verkligen atomÀra, pÄlitliga. För att sÀkerstÀlla databasens konsekvens i globalerna krÀvs det insatser frÄn programmerarens sida och anvÀndning av transaktioner, eftersom det inte finns nÄgra komplexa inbyggda strukturer som externa nycklar.

IsoleringsnivÄn för globalerna utan att anvÀnda lÄs Àr READ UNCOMMITTED, och med anvÀndning av lÄs kan den uppnÄs Ànda upp till SERIALIZE-nivÄn.

Korrekthet och hastighet för transaktioner pÄ globalerna beror vÀldigt mycket pÄ programmerarens skicklighet: ju mer som delas av shared-lÄs vid lÀsning, desto högre isoleringsnivÄ, och ju mer restriktivt exklusiva lÄs tas, desto bÀttre prestanda.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster