5.1 Az egyidejűség kérdése

Kezdjük egy kicsit távoli elmélettel.

Bármely információs rendszer (vagy egyszerűen egy alkalmazás), amelyet a programozók hoznak létre, több tipikus blokkból áll, amelyek mindegyike biztosítja a szükséges funkciók egy részét. Például a gyorsítótárat egy erőforrás-igényes művelet eredményének emlékezésére használják, hogy az ügyfél gyorsabban olvassa el az adatokat, az adatfolyam-feldolgozó eszközök lehetővé teszik az üzenetek küldését más összetevőknek az aszinkron feldolgozáshoz, a kötegelt feldolgozó eszközöket pedig a " rake" bizonyos periodikusan felhalmozott adatmennyiséget.

És szinte minden alkalmazásban részt vesznek így vagy úgy adatbázisok (DB-k), amelyek általában két funkciót látnak el: tárolják az adatokat, amikor Öntől kapják, és később kérésre átadják azokat. Ritkán jut eszébe valakinek saját adatbázis létrehozása, mert már sok kész megoldás létezik. De hogyan válassza ki a megfelelőt az alkalmazásához?

Tehát képzeljük el, hogy írt egy mobil felülettel rendelkező alkalmazást, amely lehetővé teszi, hogy egy korábban elmentett feladatlistát töltsön be a ház körül - azaz kiolvassa az adatbázisból, és kiegészítse új feladatokkal, valamint az egyes konkrét feladatokat rangsorolja. feladat - 1-től (legmagasabb) 3-ig (legalacsonyabb). Tegyük fel, hogy mobilalkalmazását egyszerre csak egy személy használja. De most mertél mesélni édesanyádnak az alkotásodról, és most ő lett a második rendszeres felhasználó. Mi történik, ha egy időben, pontosan ugyanabban az ezredmásodpercben úgy dönt, hogy valamilyen feladatot – „ablakok mosását” – más prioritási fokra állítja?

Szakmai értelemben az Ön és anyja adatbázis-lekérdezései 2 olyan folyamatnak tekinthetők, amelyek lekérdezést végeztek az adatbázisban. A folyamat egy számítógépes program entitása, amely egy vagy több szálon futhat. Általában egy folyamat gépi kód képpel, memóriával, kontextussal és egyéb erőforrásokkal rendelkezik. Más szavakkal, a folyamat úgy jellemezhető, mint a programutasítások végrehajtása a processzoron. Amikor az alkalmazás kérelmet intéz az adatbázishoz, akkor arról beszélünk, hogy az adatbázis egy folyamatból a hálózaton keresztül kapott kérést dolgozza fel. Ha egyszerre két felhasználó ül az alkalmazásban, akkor egy adott pillanatban két folyamat lehet.

Amikor egy folyamat kérést intéz az adatbázishoz, azt egy bizonyos állapotban találja meg. Az állapottartó rendszer egy olyan rendszer, amely megjegyzi a korábbi eseményeket és tárol bizonyos információkat, amelyeket "állapotnak" neveznek. A változóként deklarált változó integerállapota 0, 1, 2 vagy mondjuk 42 lehet. A Mutexnek (kölcsönös kizárás) két állapota van: zárva vagy feloldva , akárcsak egy bináris szemafor ("kötelező" vs. "felszabadítva") és általában bináris. (bináris) adattípusok és változók, amelyeknek csak két állapota lehet - 1 vagy 0.

Az állapot fogalma alapján számos matematikai és mérnöki struktúra alapul, mint például a véges automata - egy olyan modell, amelynek egy bemenete és egy kimenete van, és minden időpillanatban véges állapothalmaz egyikében van - és az „állapot ” tervezési minta, amelyben egy objektum viselkedését a belső állapottól függően változtatja (például attól függően, hogy egy vagy másik változóhoz milyen érték van hozzárendelve).

Tehát a gépek világában a legtöbb objektumnak van valamilyen állapota, amely idővel változhat: a nagy adatcsomagot feldolgozó csővezetékünk hibát dob, és meghibásodik , vagy a Wallet objektum tulajdonság, amely a felhasználó fájljában maradt pénzösszeget tárolja. számla, bérszámfejtés utáni változások.

Az egyik állapotból a másikba való átmenetet („átmenetet”) – mondjuk a folyamatban lévőből a sikertelenbe műveletnek nevezzük. Valószínűleg mindenki ismeri a CRUD műveleteket - create, read, update, deletevagy hasonló HTTP metódusokat - POST, GET, PUT, DELETE. Ám a programozók sokszor más nevet is adnak a műveleteknek a kódjukban, mert a művelet bonyolultabb is lehet, mint egy bizonyos érték kiolvasása az adatbázisból - az adatokat is le tudja ellenőrizni, majd a függvény formát öltött műveletünk, hívják majd például És validate()ki végzi ezeket a műveleteket-funkciókat? már leírt folyamatokat.

Még egy kicsit, és megérted, miért írom le ilyen részletesen a kifejezéseket!

Bármilyen műveletnek - legyen az függvény, vagy elosztott rendszerekben egy kérés küldése egy másik szerverre - 2 tulajdonsága van: a hívási idő és a befejezési idő (befejezési idő) , ami szigorúan nagyobb lesz, mint a hívási idő (a Jepsen kutatói abból az elméleti feltevésből induljunk ki, hogy mindkét időbélyeg képzeletbeli, teljesen szinkronizált, globálisan elérhető órát kap).

Képzeljük el a teendőlista alkalmazásunkat. Ön a mobil felületen keresztül kéri az adatbázist ben 14:00:00.014, és édesanyja ben 13:59:59.678(azaz 336 ezredmásodperccel korábban) frissítette a teendők listáját ugyanazon a felületen keresztül, hozzáadva a mosogatáshoz. Figyelembe véve a hálózati késleltetést és az adatbázis lehetséges feladatsorát, ha Ön és édesanyja mellett édesanyád összes barátja is használja az alkalmazásodat, az adatbázis az anya kérését az Ön feldolgozása után tudja végrehajtani. Vagyis van esély arra, hogy két kérésed, valamint édesanyád barátnőinek kérései egyszerre (egyidejűleg) ugyanarra az adatra kerüljenek.

Elérkeztünk tehát az adatbázisok és az elosztott alkalmazások területén a legfontosabb kifejezéshez - a párhuzamossághoz. Mit jelent pontosan két művelet egyidejűsége? Ha néhány T1 és néhány T2 művelet adott, akkor:

  • A T1 a T2 végrehajtás kezdő időpontja előtt indítható, és a T2 kezdési és befejezési időpontja között fejezhető be
  • A T2 a T1 kezdési időpontja előtt kezdhető, és a T1 kezdete és vége között fejezhető be
  • A T1 a T1 végrehajtásának kezdési és befejezési ideje között indítható és fejezhető be
  • és minden más forgatókönyv, ahol a T1 és T2 valamilyen közös végrehajtási idővel rendelkezik

Jól látható, hogy az előadás keretein belül elsősorban az adatbázisba belépő lekérdezésekről van szó, illetve arról, hogy az adatbázis-kezelő rendszer hogyan érzékeli ezeket a lekérdezéseket, de az egyidejűség kifejezés fontos például az operációs rendszerek kapcsán. Nem térek el túlságosan a cikk témájától, de fontosnak tartom megemlíteni, hogy az az egyidejűség, amelyről itt beszélünk, nem kapcsolódik a kontextusban tárgyalt párhuzamosság és párhuzamosság dilemmájához, valamint ezek különbségéhez. az operációs rendszerek és a nagy teljesítményű számítástechnika. A párhuzamosság az egyidejűség elérésének egyik módja több magot, processzort vagy számítógépet tartalmazó környezetben. Egyidejűségről beszélünk abban az értelemben, hogy a különböző folyamatok egyidejűleg hozzáférnek a közös adatokhoz.

És valójában mi ronthat el, pusztán elméletileg?

A megosztott adatokon végzett munka során számos, az egyidejűséggel kapcsolatos probléma, más néven "versenyfeltételek" fordulhat elő. Az első probléma akkor jelentkezik, amikor egy folyamat olyan adatokat kap, amelyeket nem kellett volna: hiányos, ideiglenes, törölt vagy más módon "helytelen" adatokat. A második probléma az, amikor a folyamat elavult adatokat kap, vagyis olyan adatokat, amelyek nem felelnek meg az adatbázis utolsó mentett állapotának. Tegyük fel, hogy valamelyik alkalmazás nulla egyenleggel vett fel pénzt egy felhasználó számlájáról, mert az adatbázis visszaadta az alkalmazásnak a számla állapotát, nem vette figyelembe a legutóbbi pénzfelvételt, ami alig pár milliszekundummal ezelőtt történt. A helyzet ilyen-olyan, nem?

5.2 A tranzakciók megmentettek minket

Az ilyen problémák megoldása érdekében megjelent a tranzakció fogalma - az adatbázissal végzett szekvenciális műveletek (állapotváltozások) bizonyos csoportja, amely logikailag egyetlen művelet. Ismét egy banki példát mondok - és nem véletlenül, mert a tranzakció fogalma láthatóan pontosan a pénzzel való munka összefüggésében jelent meg. A tranzakció klasszikus példája a pénz átutalása egyik bankszámláról a másikra: először ki kell venni az összeget a forrásszámláról, majd be kell utalni a célszámlára.

A tranzakció végrehajtásához az alkalmazásnak több műveletet kell végrehajtania az adatbázisban: ellenőriznie kell a feladó egyenlegét, zárolnia kell az összeget a küldő számláján, hozzáadnia kell az összeget a címzett számlájához, és le kell vonnia az összeget a feladótól. Egy ilyen tranzakciónak több követelménye is lesz. Például az alkalmazás nem kaphat elavult vagy téves információkat az egyenlegről - például ha ezzel egyidejűleg egy párhuzamos tranzakció félidőben hibával végződött, és a pénz nem került levonásra a számláról - és az alkalmazásunk már kapott információt hogy a pénzeszközöket leírták.

Ennek a problémának a megoldására a tranzakció olyan tulajdonságát vették igénybe, mint az „isolation”: a tranzakciónk úgy kerül végrehajtásra, mintha más tranzakciók nem is történnének ugyanabban a pillanatban. Adatbázisunk úgy hajtja végre a párhuzamos műveleteket, mintha egymás után hajtaná végre azokat, szekvenciálisan – valójában a legmagasabb elkülönítési szintet nevezik Strict Serializable . Igen, a legmagasabb, ami azt jelenti, hogy több szint is létezik.

– Állj – mondod. Tartsa a lovait, uram.

Emlékezzünk arra, hogyan írtam le, hogy minden műveletnek van hívási ideje és végrehajtási ideje. A kényelem kedvéért megfontolhatja a hívást és a végrehajtást 2 műveletként. Ekkor az összes hívási és végrehajtási művelet rendezett listája az adatbázis előzményeinek nevezhető. Ekkor a tranzakciós elkülönítési szint előzmények halmaza. Izolációs szinteket használunk annak meghatározására, hogy melyik történet a „jó”. Amikor azt mondjuk, hogy egy történet "megszakítja a szerializálhatóságot" vagy "nem szerializálható", akkor azt értjük, hogy a sztori nincs a sorozatozható történetek halmazában.

Hogy egyértelmű legyen, milyen történetekről van szó, hozok példákat. Például van egy ilyen történelem - köztes olvasmány . Ez akkor fordul elő, ha az A tranzakció egy másik futó B tranzakció által módosított és még nem véglegesített ("nem véglegesített") sorból származó adatokat olvashat ki - vagyis a változtatásokat még nem véglegesítette B tranzakciót, és bármikor törölheti azokat. És például a megszakított olvasás csak a mi példánk egy visszavont pénzfelvételi tranzakcióra

Számos anomália lehetséges. Vagyis az anomáliák valamilyen nemkívánatos adatállapot, amely az adatbázishoz való kompetitív hozzáférés során fordulhat elő. És bizonyos nem kívánt állapotok elkerülése érdekében az adatbázisok különböző szintű elszigetelést alkalmaznak - vagyis különböző szintű adatvédelmet a nem kívánt állapotokkal szemben. Ezeket a szinteket (4 db) az ANSI SQL-92 szabvány tartalmazza.

E szintek leírása néhány kutató számára homályosnak tűnik, és saját, részletesebb osztályozást kínálnak. Azt tanácsolom, hogy figyeljen a már említett Jepsenre, valamint a Hermitage projektre, amelynek célja annak tisztázása, hogy az egyes DBMS-ek, például a MySQL vagy a PostgreSQL pontosan milyen izolációs szinteket kínálnak. Ha megnyitja a fájlokat ebből a tárolóból, láthatja, hogy milyen SQL-parancsok sorozatával tesztelik az adatbázist bizonyos rendellenességekre, és hasonlót tehet az Önt érdeklő adatbázisok esetében is). Íme egy példa az adattárból, hogy felkeltse az érdeklődését:

-- 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

Fontos megérteni, hogy ugyanahhoz az adatbázishoz általában választhat egyet a többféle elkülönítés közül. Miért ne válassza a legerősebb szigetelést? Mert, mint minden a számítástechnikában, a választott elkülönítési szintnek meg kell felelnie egy olyan kompromisszumnak, amelyre készek vagyunk - ebben az esetben a végrehajtási sebesség kompromisszumának: minél erősebb az elkülönítési szint, annál lassabbak lesznek a kérések. feldolgozott. Ahhoz, hogy megértse, milyen szintű elszigeteltségre van szüksége, meg kell értenie az alkalmazás követelményeit, és ahhoz, hogy megértse, hogy az Ön által választott adatbázis rendelkezik-e ezzel a szinttel, át kell tekintenie a dokumentációt - a legtöbb alkalmazás számára ez elegendő, de Ha vannak különösen szigorú követelmények, akkor jobb, ha megszervez egy tesztet, mint amit a Hermitage projekt srácai csinálnak.

5.3 "I" és egyéb betűk az ACID-ben

Az emberek alapvetően az elszigeteltségre gondolnak, amikor a SAV-ról általában beszélnek. És ez az oka annak, hogy ennek a betűszónak az elemzését elszigetelten kezdtem, és nem mentem sorra, mint általában azok, akik megpróbálják megmagyarázni ezt a fogalmat. Most nézzük a maradék három betűt.

Emlékezzünk vissza a banki átutalással kapcsolatos példánkra. A pénzeszközök egyik számláról a másikra történő átutalására irányuló tranzakció magában foglalja az első számláról történő kifizetési műveletet, a másodikon pedig egy feltöltési műveletet. Ha a második számla feltöltési művelete meghiúsult, valószínűleg nem szeretné, hogy az első számláról történjen a kifizetési művelet. Vagyis vagy teljes mértékben sikerül a tranzakció, vagy egyáltalán nem jön létre, de nem lehet csak részben megkötni. Ezt a tulajdonságot "atomitásnak" nevezik, és ez "A" az ACID-ben.

Amikor a tranzakciónk végrehajtódik, akkor, mint minden művelet, átviszi az adatbázist egyik érvényes állapotból a másikba. Egyes adatbázisok úgynevezett megszorításokat kínálnak – vagyis olyan szabályokat, amelyek a tárolt adatokra vonatkoznak, például az elsődleges vagy másodlagos kulcsokra, indexekre, alapértelmezett értékekre, oszloptípusokra stb. Egy tranzakció során tehát biztosnak kell lennünk abban, hogy mindezen korlátok teljesülnek.

Ezt a garanciát "konzisztenciának" nevezik, és CACID-ben egy betű (nem tévesztendő össze az elosztott alkalmazások világából származó konzisztenciával, amelyről később fogunk beszélni). Világos példát adok az ACID értelmében vett konzisztenciára: egy online áruházhoz tartozó alkalmazás ordersegy sort szeretne hozzáadni a táblázathoz, és a táblázat azonosítója az oszlopban product_idlesz feltüntetve - tipikus .productsforeign key

Ha a terméket mondjuk eltávolítottuk a választékból, és ennek megfelelően az adatbázisból, akkor a sorbeszúrási művelet nem történhet meg, és hibaüzenetet kapunk. Ez a garancia, a többihez képest, szerintem kicsit túlzás - már csak azért is, mert az adatbázisból származó kényszerek aktív használata az adatokért való felelősség áthárítását jelenti (valamint az üzleti logika részleges eltolódását, ha már kb. egy olyan megszorítást, mint a CHECK ) az alkalmazásból az adatbázisba, ami, ahogy most mondják, pontosan így van.

És végül marad D- "ellenállás" (tartósság). A rendszerhiba vagy bármely más hiba nem vezethet a tranzakciós eredmények vagy az adatbázistartalom elvesztéséhez. Vagyis ha az adatbázis azt válaszolta, hogy a tranzakció sikeres volt, akkor ez azt jelenti, hogy az adatokat a nem felejtő memóriában rögzítették - például egy merevlemezen. Ez egyébként nem jelenti azt, hogy a következő olvasási kérésnél azonnal látni fogja az adatokat.

Épp a napokban dolgoztam a DynamoDB-vel az AWS-től (Amazon Web Services), és elküldtem néhány adatot mentésre, és miután megkaptam a választ HTTP 200(OK), vagy valami hasonló, úgy döntöttem, hogy megnézem - és ezt nem láttam. adatok az adatbázisban a következő 10 másodpercre. Vagyis a DynamoDB átvette az adataimat, de nem minden csomópont szinkronizált azonnal, hogy megkapja az adatok legújabb példányát (bár lehet, hogy a gyorsítótárban volt). Itt ismét a konzisztencia területére másztunk az elosztott rendszerek kontextusában, de még mindig nem jött el az idő, hogy beszéljünk róla.

Tehát most már tudjuk, mi a ACID garancia. És még azt is tudjuk, miért hasznosak. De valóban szükségünk van rájuk minden alkalmazásban? És ha nem, akkor pontosan mikor? Minden DB kínálja ezeket a garanciákat, és ha nem, mit kínál helyette?