Transaktioner i InterSystems IRIS globals

Transaktioner i InterSystems IRIS globalsInterSystems IRIS DBMS stöder intressanta strukturer för att lagra data - globala. I huvudsak handlar det om flernivånycklar med diverse extra godsaker i form av transaktioner, snabba funktioner för att korsa dataträd, lås och ett eget ObjectScript-språk.

Läs mer om globaler i artikelserien "Globals är skattsvärd för att lagra data":

Träd. Del 1
Träd. Del 2
Glesa arrayer. Del 3

Jag blev intresserad av hur transaktioner implementeras i globala, vilka funktioner som finns. Detta är trots allt en helt annan struktur för att lagra data än de vanliga tabellerna. Mycket lägre nivå.

Som bekant från teorin om relationsdatabaser måste en bra implementering av transaktioner uppfylla kraven SYRA:

A - Atomicitet (atomicitet). Alla ändringar som görs i transaktionen eller inga alls registreras.

C - Konsistens. När en transaktion har slutförts måste databasens logiska tillstånd vara internt konsekvent. På många sätt berör detta krav programmeraren, men i fallet med SQL-databaser gäller det även främmande nycklar.

Jag - Isolera. Transaktioner som löper parallellt bör inte påverka varandra.

D - Hållbar. Efter framgångsrikt slutförande av en transaktion bör problem på lägre nivåer (till exempel strömavbrott) inte påverka data som ändras av transaktionen.

Globaler är icke-relationella datastrukturer. De designades för att köra supersnabbt på mycket begränsad hårdvara. Låt oss titta på implementeringen av transaktioner i globala med hjälp av officiella IRIS docker-bild.

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

1. Atomicitet

Det enklaste sättet att kontrollera är atomicitet. Vi kollar från databaskonsolen.

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

Sedan avslutar vi:

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

Vi får:

1 2 3

Allt är bra. Atomiciteten bibehålls: alla förändringar registreras.

Låt oss komplicera uppgiften, introducera ett fel och se hur transaktionen sparas, delvis eller inte alls.

Låt oss kontrollera atomiciteten igen:

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

Sedan kommer vi att tvångsstoppa containern, lansera den och se.

docker kill my-iris

Detta kommando motsvarar nästan en forcerad avstängning, eftersom det skickar en SIGKILL-signal för att stoppa processen omedelbart.

Kanske var transaktionen delvis sparad?

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

– Nej, det har inte överlevt.

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)
^
<UNDEFINED> ^a(1)

Inget har överlevt heller.

2. Konsekvens

Eftersom i databaser baserade på globaler görs nycklar även på globaler (låt mig påminna om att en global är en struktur på lägre nivå för att lagra data än en relationstabell), för att uppfylla konsistenskravet måste en förändring av nyckeln inkluderas i samma transaktion som en förändring i den globala.

Till exempel har vi en global ^person, där vi lagrar personligheter och vi använder TIN som nyckel.

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

För att göra en snabb sökning på efternamn och förnamn gjorde vi ^index-nyckeln.

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

För att databasen ska vara konsekvent måste vi lägga till personan så här:

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

Följaktligen, när vi raderar måste vi också använda en transaktion:

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

Att uppfylla konsistenskravet vilar med andra ord helt på programmerarens axlar. Men när det kommer till globala, är detta normalt, på grund av deras låga natur.

3. Isolering

Det är här vildmarken börjar. Många användare arbetar samtidigt på samma databas och ändrar samma data.

Situationen är jämförbar med när många användare samtidigt arbetar med samma kodlager och försöker göra ändringar i många filer samtidigt.

Databasen ska reda ut allt i realtid. Med tanke på att det i seriösa företag till och med finns en speciell person som är ansvarig för versionskontroll (för att slå samman filialer, lösa konflikter etc.), och databasen måste göra allt detta i realtid, uppgiftens komplexitet och riktigheten av databasdesign och kod som tjänar den.

Databasen kan inte förstå innebörden av de åtgärder som utförs av användare för att undvika konflikter om de arbetar med samma data. Det kan bara ångra en transaktion som är i konflikt med en annan, eller utföra dem i följd.

Ett annat problem är att under utförandet av en transaktion (före en commit) kan databasens tillstånd vara inkonsekvent, så det är önskvärt att andra transaktioner inte har tillgång till databasens inkonsekventa tillstånd, vilket uppnås i relationsdatabaser på många sätt: skapa ögonblicksbilder, multiversionsrader och etc.

När man utför transaktioner parallellt är det viktigt för oss att de inte stör varandra. Detta är egenskapen för isolering.

SQL definierar 4 isoleringsnivåer:

  • LÄS OBEGRÄNSAD
  • LÄS ÅTAGANDE
  • REPETERBAR LÄSNING
  • SERIALISERBAR

Låt oss titta på varje nivå separat. Kostnaderna för att implementera varje nivå växer nästan exponentiellt.

LÄS OBEGRÄNSAD - detta är den lägsta nivån av isolering, men samtidigt den snabbaste. Transaktioner kan läsa ändringar som gjorts av varandra.

LÄS ÅTAGANDE är nästa nivå av isolering, vilket är en kompromiss. Transaktioner kan inte läsa varandras ändringar före commit, men de kan läsa eventuella ändringar som gjorts efter commit.

Om vi ​​har en lång transaktion T1, under vilken commits ägde rum i transaktionerna T2, T3 ... Tn, som fungerade med samma data som T1, så får vi ett annat resultat varje gång när vi begär data i T1. Detta fenomen kallas icke-repeterbar läsning.

REPETERBAR LÄSNING — i denna isoleringsnivå har vi inte fenomenet icke-repeterbar läsning, på grund av det faktum att för varje begäran att läsa data skapas en ögonblicksbild av resultatdata och när de återanvänds i samma transaktion, data från ögonblicksbilden är använd. Det är dock möjligt att läsa fantomdata på denna isoleringsnivå. Detta syftar på att läsa nya rader som har lagts till av parallella genomförda transaktioner.

SERIALISERBAR — högsta isoleringsnivån. Det kännetecknas av det faktum att data som används på något sätt i en transaktion (läsning eller ändring) blir tillgänglig för andra transaktioner först efter att den första transaktionen har slutförts.

Låt oss först ta reda på om det finns isolering av operationer i en transaktion från huvudtråden. Låt oss öppna 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 öppnade transaktionen gör.

Låt oss se om transaktioner av olika trådar ser vad som händer inuti dem.

Låt oss öppna 2 terminalfönster och öppna 2 transaktioner parallellt.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Parallella transaktioner ser varandras data. Så, vi fick den enklaste, men också den snabbaste isoleringsnivån, LÄS OCMITED.

I princip kan detta förväntas för globala, där prestation alltid har varit en prioritet.

Vad händer om vi behöver en högre grad av isolering i operationer på globala enheter?

Här behöver du fundera på varför isoleringsnivåer överhuvudtaget behövs och hur de fungerar.

Den högsta isoleringsnivån, SERIALIZE, innebär att resultatet av transaktioner som utförs parallellt är likvärdigt med deras sekventiella exekvering, vilket garanterar frånvaron av kollisioner.

Vi kan göra detta med smarta lås i ObjectScript, som har många olika användningsområden: du kan göra regelbunden, inkrementell, multipla låsning med kommandot LÅS.

Lägre isoleringsnivåer är avvägningar utformade för att öka databashastigheten.

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

Denna operatör låter dig ta inte bara exklusiva lås som behövs för att ändra data, utan så kallade delade lås, som kan ta flera trådar parallellt när de behöver läsa data som inte ska ändras av andra processer under läsningsprocessen.

Mer information om tvåfasblockeringsmetoden på ryska och engelska:

Tvåfas blockering
Tvåfaslåsning

Svårigheten är att under en transaktion kan tillståndet för databasen vara inkonsekvent, men dessa inkonsekventa data är synliga för andra processer. Hur undviker man detta?

Med hjälp av lås kommer vi att skapa synlighetsfönster där tillståndet för databasen kommer att vara konsekvent. Och all tillgång till sådana synlighetsfönster för den överenskomna staten kommer att kontrolleras av lås.

Delade lås på samma data är återanvändbara – flera processer kan ta dem. Dessa lås hindrar andra processer från att ändra data, d.v.s. de används för att bilda fönster med konsekvent databastillstånd.

Exklusiva lås används för dataändringar - endast en process kan ta ett sådant lås. Ett exklusivt lås kan tas av:

  1. Alla processer om uppgifterna är gratis
  2. Endast processen som har ett delat lås på denna data och var den första att begära ett exklusivt lås.

Transaktioner i InterSystems IRIS globals

Ju smalare synlighetsfönstret är, desto längre tid måste andra processer vänta på det, men desto mer konsekvent kan tillståndet för databasen i den vara.

READ_COMMITTED — Kärnan i denna nivå är att vi bara ser engagerad data från andra trådar. Om uppgifterna i en annan transaktion ännu inte har begåtts, ser vi dess gamla version.

Detta gör att vi kan parallellisera arbetet istället för att vänta på att låset ska släppas.

Utan speciella knep kommer vi inte att kunna se den gamla versionen av datan i IRIS, så vi får nöja oss med lås.

Följaktligen kommer vi att behöva använda delade lås för att tillåta att data endast kan läsas i ögonblick av konsistens.

Låt oss säga att vi har en användarbas ^person som överför pengar till varandra.

Överföringsögonblick från person 123 till person 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)

Ögonblicket för att begära summan pengar från person 123 innan debitering måste åtföljas av en exklusiv spärr (som standard):

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

Och om du behöver visa kontostatusen i ditt personliga konto kan du använda ett delat lås eller inte använda det alls:

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

Men om vi antar att databasoperationer utförs nästan omedelbart (låt mig påminna dig om att globaler är en struktur på mycket lägre nivå än en relationstabell), så minskar behovet av denna nivå.

REPETERBAR LÄSNING - Denna isoleringsnivå möjliggör flera läsningar av data som kan modifieras av samtidiga transaktioner.

Följaktligen måste vi sätta ett delat lås för att läsa data som vi ändrar och exklusiva lås på data som vi ändrar.

Lyckligtvis låter LOCK-operatören dig i detalj lista alla nödvändiga lås, som det kan finnas många av, i ett uttalande.

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

andra operationer (för närvarande försöker parallella trådar ändra ^person(123, mängd), men kan inte)

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

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

När du listar lås separerade med kommatecken tas de sekventiellt, men om du gör så här:

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

sedan tas de atomärt på en gång.

GE SOM SERIE — vi måste sätta lås så att alla transaktioner som har gemensamma data i slutändan exekveras sekventiellt. För detta tillvägagångssätt bör de flesta lås vara exklusiva och användas på de minsta områdena i världen för prestanda.

Om vi ​​pratar om att debitera medel i den globala ^personen, är endast SERIALISE-isoleringsnivån acceptabel för det, eftersom pengar måste spenderas strikt sekventiellt, annars är det möjligt att spendera samma summa flera gånger.

4. Hållbarhet

Jag genomförde tester med hård skärning av behållaren med hjälp av

docker kill my-iris

Basen tolererade dem väl. Inga problem identifierades.

Slutsats

För globala företag har InterSystems IRIS transaktionsstöd. De är verkligen atomära och pålitliga. För att säkerställa konsistens i en databas baserad på globaler krävs programmeringsinsatser och användning av transaktioner, eftersom den inte har komplexa inbyggda konstruktioner såsom främmande nycklar.

Isoleringsnivån för globala enheter utan att använda lås är LÄS OCMITED, och när du använder lås kan det säkerställas upp till SERIALISERA-nivån.

Korrektheten och hastigheten för transaktioner på globala enheter beror mycket på programmerarens skicklighet: ju mer delade lås används vid läsning, desto högre isoleringsnivå och ju mer snävt exklusiva lås tas, desto snabbare prestanda.

Källa: will.com

Lägg en kommentar