5.1 Spørgsmålet om samtidighed

Lad os starte med en lidt fjern teori.

Ethvert informationssystem (eller blot en applikation), som programmører opretter, består af flere typiske blokke, som hver især giver en del af den nødvendige funktionalitet. For eksempel bruges cachen til at huske resultatet af en ressourcekrævende operation for at sikre hurtigere læsning af data af klienten, strømbehandlingsværktøjer giver dig mulighed for at sende beskeder til andre komponenter til asynkron behandling, og batchbehandlingsværktøjer bruges til at " rake" de akkumulerede mængder af data med en vis periodicitet. .

Og i næsten alle applikationer er databaser (DB'er) involveret på den ene eller anden måde, som normalt udfører to funktioner: gemme data, når de modtages fra dig, og senere give dem til dig efter anmodning. Der er sjældent nogen, der tænker på at lave deres egen database, for der findes allerede mange færdige løsninger. Men hvordan vælger du den rigtige til din ansøgning?

Så lad os forestille os, at du har skrevet en applikation med en mobil grænseflade, der giver dig mulighed for at indlæse en tidligere gemt liste med opgaver rundt omkring i huset - altså læse fra databasen, og supplere den med nye opgaver, samt prioritere hver specifik opgave - fra 1 (højest) til 3 (laveste). Lad os sige, at din mobilapplikation kun bruges af én person ad gangen. Men nu turde du fortælle din mor om din kreation, og nu er hun blevet den anden faste bruger. Hvad sker der, hvis du på samme tid, lige i det samme millisekund, beslutter dig for at sætte en opgave - "vask vinduerne" - til en anden grad af prioritet?

Rent fagligt kan din og mors databaseforespørgsler betragtes som 2 processer, der lavede en forespørgsel til databasen. En proces er en enhed i et computerprogram, der kan køre på en eller flere tråde. Typisk har en proces et maskinkodebillede, hukommelse, kontekst og andre ressourcer. Processen kan med andre ord karakteriseres som udførelse af programinstruktioner på processoren. Når din ansøgning sender en anmodning til databasen, taler vi om, at din database behandler anmodningen modtaget over netværket fra én proces. Hvis der er to brugere, der sidder i applikationen på samme tid, kan der være to processer på et hvilket som helst tidspunkt.

Når en proces sender en anmodning til databasen, finder den den i en bestemt tilstand. Et stateful system er et system, der husker tidligere hændelser og gemmer nogle oplysninger, som kaldes "tilstand". En variabel erklæret som integerkan have en tilstand på 0, 1, 2 eller f.eks. 42. Mutex (gensidig udelukkelse) har to tilstande: låst eller ulåst , ligesom en binær semafor ("påkrævet" vs. "frigivet") og generelt binær (binære) datatyper og variabler, der kun kan have to tilstande - 1 eller 0.

Baseret på tilstandsbegrebet er flere matematiske og tekniske strukturer baseret, såsom en endelig automat - en model, der har én indgang og én udgang og er i en af ​​et endeligt sæt af tilstande på hvert tidspunkt - og "tilstanden ” designmønster, hvor et objekt ændrer adfærd afhængigt af den interne tilstand (f.eks. afhængig af hvilken værdi der tildeles en eller anden variabel).

Så de fleste objekter i maskinverdenen har en tilstand, der kan ændre sig over tid: vores pipeline, som behandler en stor datapakke, kaster en fejl og bliver mislykket , eller Wallet-objektegenskaben, som gemmer mængden af ​​penge, der er tilbage i brugerens konto, ændringer efter lønkvitteringer.

En overgang ("overgang") fra en tilstand til en anden - for eksempel fra igangværende til mislykket - kaldes en operation. Sandsynligvis kender alle CRUD- operationerne - create, read, update, deleteeller lignende HTTP- metoder - POST, GET, PUT, DELETE. Men programmører giver ofte andre navne til operationer i deres kode, fordi operationen kan være mere kompleks end blot at læse en bestemt værdi fra databasen – den kan også tjekke dataene, og så vores operation, som har taget form af en funktion, vil for eksempel hedde Og validate()hvem udfører disse operationer-funktioner? allerede beskrevet processer.

Lidt mere, og du vil forstå, hvorfor jeg beskriver vilkårene så detaljeret!

Enhver handling - det være sig en funktion eller, i distribuerede systemer, at sende en anmodning til en anden server - har 2 egenskaber: påkaldelsestidspunktet og færdiggørelsestiden (gennemførelsestiden) , som vil være strengt større end påkaldelsestiden (forskere fra Jepsen gå ud fra de teoretiske antagelser om, at begge disse tidsstempler vil få imaginære, fuldt synkroniserede, globalt tilgængelige ure).

Lad os forestille os vores opgaveliste-applikation. Du laver en forespørgsel til databasen via den mobile grænseflade i 14:00:00.014, og din mor har 13:59:59.678(det vil sige 336 millisekunder før) opdateret opgavelisten via den samme grænseflade og tilføjet opvask til den. Under hensyntagen til netværksforsinkelsen og den mulige kø af opgaver til din database, hvis udover dig og din mor alle din mors venner også bruger din applikation, kan databasen udføre mors anmodning, efter at den har behandlet din. Der er med andre ord en chance for, at to af dine anmodninger, samt anmodninger fra din mors veninder, bliver sendt til samme data på samme tid (samtidigt).

Så vi er kommet til det vigtigste udtryk inden for databaser og distribuerede applikationer - samtidighed. Hvad præcist kan samtidighed af to operationer betyde? Hvis en operation T1 og en operation T2 er givet, så:

  • T1 kan startes før starttidspunktet for udførelse T2 og afsluttes mellem start- og sluttidspunktet for T2
  • T2 kan startes før starttidspunktet for T1, og afsluttes mellem starten og slutningen af ​​T1
  • T1 kan startes og afsluttes mellem start- og sluttidspunktet for T1-udførelse
  • og ethvert andet scenarie, hvor T1 og T2 har en vis fælles udførelsestid

Det er klart, at vi inden for rammerne af dette foredrag primært taler om forespørgsler, der kommer ind i databasen, og hvordan databasestyringssystemet opfatter disse forespørgsler, men begrebet samtidighed er vigtigt, for eksempel i forbindelse med operativsystemer. Jeg vil ikke afvige alt for langt fra emnet for denne artikel, men jeg synes, det er vigtigt at nævne, at den samtidighed, vi taler om her, ikke er relateret til dilemmaet med samtidighed og samtidighed og deres forskel, som diskuteres i sammenhængen. af operativsystemer og højtydende.computing. Parallelisme er en måde at opnå samtidighed i et miljø med flere kerner, processorer eller computere. Vi taler om samtidighed i betydningen samtidig adgang af forskellige processer til fælles data.

Og hvad kan egentlig gå galt rent teoretisk?

Når man arbejder med delte data, kan der opstå adskillige problemer relateret til samtidighed, også kaldet "raceforhold". Det første problem opstår, når en proces modtager data, som den ikke burde have modtaget: ufuldstændige, midlertidige, annullerede eller på anden måde "forkerte" data. Det andet problem er, når processen modtager forældede data, det vil sige data, der ikke svarer til den sidst gemte tilstand af databasen. Lad os sige, at en applikation har trukket penge fra en brugers konto med en saldo på nul, fordi databasen returnerede kontostatus til applikationen uden at tage højde for den sidste hævning af penge fra den, som skete for blot et par millisekunder siden. Situationen er halvdårlig, ikke?

5.2 Transaktioner kom for at redde os

For at løse sådanne problemer dukkede begrebet en transaktion op - en bestemt gruppe af sekventielle operationer (tilstandsændringer) med en database, som er en logisk enkelt operation. Jeg vil give et eksempel med en bank igen - og ikke tilfældigt, for begrebet en transaktion opstod tilsyneladende netop i forbindelse med arbejdet med penge. Det klassiske eksempel på en transaktion er overførsel af penge fra en bankkonto til en anden: du skal først hæve beløbet fra kildekontoen og derefter indsætte det på målkontoen.

For at denne transaktion kan udføres, skal applikationen udføre flere handlinger i databasen: kontrollere afsenderens saldo, spærre beløbet på afsenderens konto, tilføje beløbet til modtagerens konto og trække beløbet fra afsenderen. Der vil være flere krav til en sådan transaktion. Eksempelvis kan ansøgningen ikke modtage forældede eller forkerte oplysninger om saldoen - for eksempel hvis samtidig en parallel transaktion endte med en fejl halvvejs, og midlerne ikke blev trukket fra kontoen - og vores ansøgning allerede har modtaget oplysninger at midlerne blev afskrevet.

For at løse dette problem blev en sådan egenskab ved en transaktion som "isolation" kaldt: vores transaktion udføres, som om der ikke var andre transaktioner, der blev udført på samme tidspunkt. Vores database udfører samtidige operationer, som om den udfører dem en efter en, sekventielt - faktisk kaldes det højeste isolationsniveau Strict Serializable . Ja, det højeste, hvilket betyder, at der er flere niveauer.

"Stop," siger du. Hold dine heste, sir.

Lad os huske, hvordan jeg beskrev, at hver operation har en opkaldstid og en udførelsestid. For nemheds skyld kan du overveje at kalde og udføre som 2 handlinger. Derefter kan den sorterede liste over alle opkald og udførelseshandlinger kaldes databasens historie. Så er transaktionsisolationsniveauet et sæt historier. Vi bruger isolationsniveauer til at afgøre, hvilke historier der er "gode". Når vi siger, at en historie "bryder serialiserbarheden" eller "kan ikke serialiseres", mener vi, at historien ikke er i sættet af serialiserbare historier.

For at gøre det klart, hvilken slags historier vi taler om, vil jeg give eksempler. For eksempel er der sådan en slags historie - mellemlæsning . Det sker, når transaktion A får lov til at læse data fra en række, der er blevet ændret af en anden kørende transaktion B og endnu ikke er blevet committet ("ikke committet") - det vil sige, at ændringerne endnu ikke er endeligt committet af transaktion B, og den kan til enhver tid annullere dem. Og for eksempel er afbrudt læsning blot vores eksempel med en annulleret hævningstransaktion

Der er flere mulige anomalier. Det vil sige, at anomalier er en form for uønsket datatilstand, der kan opstå under konkurrenceadgang til databasen. Og for at undgå visse uønskede tilstande, bruger databaser forskellige niveauer af isolation - det vil sige forskellige niveauer af databeskyttelse fra uønskede tilstande. Disse niveauer (4 stykker) blev opført i ANSI SQL-92 standarden.

Beskrivelsen af ​​disse niveauer forekommer vag for nogle forskere, og de tilbyder deres egne, mere detaljerede klassifikationer. Jeg råder dig til at være opmærksom på den allerede nævnte Jepsen, samt Eremitage-projektet, som har til formål at afklare præcis hvilke isolationsniveauer, der tilbydes af specifikke DBMS, såsom MySQL eller PostgreSQL. Hvis du åbner filerne fra dette lager, kan du se, hvilken rækkefølge af SQL-kommandoer de bruger til at teste databasen for visse anomalier, og du kan gøre noget lignende for de databaser, du er interesseret i). Her er et eksempel fra lageret for at holde dig interesseret:

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

Det er vigtigt at forstå, at for den samme database kan du som regel vælge en af ​​flere typer isolation. Hvorfor ikke vælge den stærkeste isolering? For ligesom alt inden for datalogi bør det valgte isolationsniveau svare til en afvejning, som vi er klar til at foretage - i dette tilfælde en afvejning i eksekveringshastigheden: Jo stærkere isolationsniveauet er, jo langsommere vil anmodningerne være behandlet. For at forstå hvilket niveau af isolation du har brug for, skal du forstå kravene til din applikation, og for at forstå om den database du har valgt tilbyder dette niveau, bliver du nødt til at kigge i dokumentationen - for de fleste applikationer vil dette være nok, men hvis du har nogle særligt stramme krav, er det bedre at arrangere en test som det, fyrene fra Eremitageprojektet laver.

5.3 "I" og andre bogstaver i ACID

Isolation er dybest set, hvad folk mener, når de taler om ACID generelt. Og det er af denne grund, at jeg begyndte analysen af ​​dette akronym med isolation, og gik ikke i orden, som de, der forsøger at forklare dette koncept, normalt gør. Lad os nu se på de resterende tre bogstaver.

Husk igen vores eksempel med en bankoverførsel. En transaktion for at overføre penge fra én konto til en anden inkluderer en hævningsoperation fra den første konto og en genopfyldningsoperation på den anden. Hvis genopfyldningsoperationen af ​​den anden konto mislykkedes, ønsker du sandsynligvis ikke, at udbetalingen fra den første konto skal finde sted. Med andre ord, enten lykkes transaktionen fuldstændigt, eller også sker den slet ikke, men den kan ikke kun foretages for en del. Denne egenskab kaldes "atomicitet", og det er et "A" i ACID.

Når vores transaktion udføres, overfører den, ligesom enhver operation, databasen fra en gyldig tilstand til en anden. Nogle databaser tilbyder såkaldte constraints - det vil sige regler, der gælder for de lagrede data, for eksempel vedrørende primære eller sekundære nøgler, indekser, standardværdier, kolonnetyper mv. Så når vi foretager en transaktion, skal vi være sikre på, at alle disse begrænsninger vil blive opfyldt.

Denne garanti kaldes "konsistens" og et bogstav Ci ACID (ikke at forveksle med konsistens fra verden af ​​distribuerede applikationer, som vi vil tale om senere). Jeg vil give et klart eksempel på konsistens i betydningen ACID: en applikation til en onlinebutik ønsker at tilføje ordersen række til tabellen, og ID'et fra tabellen product_idvil blive angivet i kolonnen - typisk .productsforeign key

Hvis produktet f.eks. blev fjernet fra sortimentet og følgelig fra databasen, skulle rækkeindsættelsesoperationen ikke ske, og vi får en fejl. Denne garanti, sammenlignet med andre, er efter min mening en smule langt ude - om ikke andet fordi den aktive brug af begrænsninger fra databasen betyder forskydning af ansvaret for dataene (samt delvis forskydning af forretningslogik, hvis vi taler om sådan en begrænsning som CHECK ) fra applikationen til databasen, som, som de siger nu, netop er sådan.

Og endelig forbliver det D- "modstand" (holdbarhed). En systemfejl eller anden fejl bør ikke føre til tab af transaktionsresultater eller databaseindhold. Det vil sige, at hvis databasen svarede, at transaktionen var vellykket, betyder det, at dataene blev optaget i ikke-flygtig hukommelse - for eksempel på en harddisk. Dette betyder i øvrigt ikke, at du umiddelbart vil se dataene ved næste læseanmodning.

Forleden arbejdede jeg med DynamoDB fra AWS (Amazon Web Services), og sendte nogle data til lagring, og efter at have modtaget et svar ( HTTP 200OK) eller sådan noget besluttede jeg at tjekke det - og så ikke dette data i databasen for de næste 10 sekunder. Det vil sige, at DynamoDB forpligtede mine data, men ikke alle noder blev øjeblikkeligt synkroniseret for at få den seneste kopi af dataene (selvom det kan have været i cachen). Her klatrede vi igen ind på konsistensens territorium i forbindelse med distribuerede systemer, men tiden til at tale om det er stadig ikke kommet.

Så nu ved vi, hvad SYRE-garantier er. Og vi ved endda, hvorfor de er nyttige. Men har vi virkelig brug for dem i enhver ansøgning? Og hvis ikke, hvornår præcist? Tilbyder alle DB'er disse garantier, og hvis ikke, hvad tilbyder de i stedet?