Uvod u implementaciju transakcija u MySQL-u - Programiranje zasnovano na veri
U relacionalnim bazama podataka značaj transakcija je nesporan; svako ko imalo poznaje baze podataka zna da transakcije imaju četiri osnovna ACID svojstva. Ono što možda ne znamo jeste na koji način baza podataka implementira ta četiri svojstva. U ovom članku ćemo analizirati implementaciju transakcija i pokušati da razumemo kako baza podržava transakcije; naravno, u članku ćemo kratko predstaviti i kako MySQL implementira ACID.

Transakcija je zapravo osnovna jedinica kontrole konkurencije; verujem da svi znamo da je transakcija niz operacija kod kojih se ili sve operacije izvrše, ili se ne izvrši nijedna, i da predstavlja nedeljivu radnu jedinicu. Četiri glavne ACID karakteristike transakcija u bazi su temelj transakcija; kada razumemo kako je ACID implementiran, razumećemo i implementaciju transakcija. U nastavku ćemo redom objasniti kako baza implementira ova četiri svojstva.
Atomarnost
Prilikom učenja o transakcijama često će vam reći da je transakcija niz operacija koje se ili sve izvrše, ili se ne izvrši nijedna — to je upravo opis atomarnosti transakcija. Iako transakcije poseduju atomarnost, atomarnost nije svojstvo vezano isključivo za transakcije; ona se pojavljuje na mnogim mestima.

Pošto pojedinačne operacije nisu atomske i mogu se razložiti na više koraka, kada neka od tih operacija naiđe na grešku ili baci izuzetak, cela operacija možda neće nastaviti izvršavanje, a nuspojave već izvedenih operacija mogu dovesti do gubitka ili pogrešnosti ažuriranih podataka.
Transakcija se suštinski ne razlikuje previše od jedne obične operacije; ona je skup operacija nad bazom podataka (možemo ih shvatiti kao SQL naredbe). Ako transakcija ne bi bila atomska, ne bismo mogli da garantujemo da su sve operacije iste transakcije ili sve izvršene ili sve neizvršene, pa bi ceo sistem baze podataka bio i neupotrebljiv i nepouzdan.
Dnevnik poništavanja
Da bismo garantovali atomarnost transakcije, potrebno je da pri nastanku greške poništimo (rollback) već izvedene operacije. U MySQL-u se mehanizam oporavka ostvaruje preko dnevnika poništavanja (undo log); sve izmene koje transakcija napravi prvo se beleže u ovaj dnevnik poništavanja, a zatim se upisuju odgovarajući redovi u bazi.

Ovaj proces je zapravo vrlo lako razumeti: da bismo pri nastanku greške mogli da poništimo sve prethodne operacije, očigledno moramo beležiti sve prethodne operacije, tako da pri grešci možemo izvršiti poništavanje.
Pored toga što pri grešci ili kada korisnik izvrši ROLLBACK obezbeđuje podatke potrebne za poništavanje, dnevnik poništavanja omogućava i sledeće: kada ceo sistem doživi pad, kada proces baze bude direktno ubijen, a korisnik ponovo pokrene proces baze, odmah se pregledom dnevnika poništavanja mogu poništiti prethodno nezavršene transakcije. Zato dnevnik poništavanja mora biti persistovan na disk pre samih podataka — to je glavni razlog zašto prvo pišemo dnevnik, a zatim bazu.
Dnevnik poništavanja ne može fizički vratiti bazu u stanje pre izvršenja naredbe ili transakcije; on je logički dnevnik. Kada se dnevnik poništavanja koristi, on samo logički poništava izmene u bazi prema zapisima u dnevniku. Možemo razumeti to tako da svaki INSERT koji koristimo u transakciji odgovara jednom DELETE, a svaki UPDATE odgovara suprotnom UPDATE iskazu.

Ovde nećemo obraditi format dnevnika poništavanja ni način na koji se njime upravlja; ovaj članak se fokusira na to kakav je to zapravo objekat, koji problem rešava i kako ga rešava. Za čitaoce koji žele detalje implementacije, na mreži sigurno postoji mnogo članaka o dnevniku poništavanja.
Stanja transakcije
Pošto transakcija poseduje atomarnost, iz daljine transakcija deluje kao nedeljiva celina, pa transakcija ima samo tri stanja: Active, Commited i Failed — transakcija je ili u izvršavanju, ili u stanju uspeha, ili u stanju neuspeha:

Međutim, ako pogledamo izbliza, uvidećemo da transakcija više nije atomska, već sadrži više međustanja, kao što je delimično potvrđivanje; dijagram stanja transakcije postaje sve složeniji.

Dijagram stanja transakcije i opisi stanja preuzeti su iz 14. poglavlja knjige Database System Concepts.
- Active: početno stanje transakcije, označava da je transakcija u izvršavanju;
- Partially Commited: nakon izvršenja poslednje naredbe;
- Failed: nakon što se utvrdi da transakcija ne može normalno da se izvrši;
- Aborted: nakon što je transakcija poništena i baza vraćena u stanje pre transakcije;
- Commited: nakon uspešnog izvršenja cele transakcije;
Iako pri grešci stanje cele baze može biti obnovljeno, ako u transakciji izvršimo radnje poput: ispisa logova na standardni izlaz, slanja imejla spolja, izmene sadržaja na disku van baze, ili čak izvršenja novčanog transfera tokom transakcije — te radnje, kao vidljivi spoljni izlazi, ne mogu biti poništene. Ove probleme rešava i za njih odgovara programer aplikacije; u većini slučajeva takve neponištive radnje treba okidati tek nakon što je cela transakcija potvrđena.

Na primer, kod rezervacije karata: čak i ako prema trećoj strani pošaljemo zahtev tek nakon što je cela transakcija završena, pošto slanje zahteva trećoj strani i dobijanje rezultata traje duže, ako baza ili server padnu odmah nakon potvrde transakcije, vrlo lako možemo izgubiti taj proces slanja zahteva, što dovodi do veoma ozbiljnog problema. To baza podataka ne može da garantuje; programer mora u odgovarajućem trenutku proveriti da li je zahtev poslat i da li je rezultat uspešan ili neuspešan.
Atomarnost paralelnih transakcija
Do sada su se sve transakcije izvršavale serijski; pitanje paralelnog izvršavanja nije razmatrano. Međutim, u praksi su paralelne transakcije sasvim uobičajene, a pod paralelnim izvršavanjem mogu se javiti veoma složeni problemi:

Kada Transaction1 tokom izvršavanja vrši čitanje i pisanje nad korisnikom id = 1, a da pritom nije potvrdila ili poništila izmene, Transaction2 u tom trenutku vrši čitanje istih podataka i potvrđuje svoju transakciju; drugim rečima, Transaction2 zavisi od Transaction1. Kada Transaction1 zbog nekih grešaka mora da se poništi, da bi se očuvala atomarnost transakcije, potrebno je poništiti i Transaction2; međutim, pošto smo već potvrdili Transaction2, više nemamo način da je poništimo. Time nastaje problem; u knjizi Database System Concepts ova pojave se naziva neraspored koji se ne može oporaviti (Nonrecoverable Schedule). A šta je onda raspored koji se može oporaviti?
A recoverable schedule is one where, for each pair of transactions Ti and Tj such that Tj reads a data item previously written by Ti , the commit operation of Ti appears before the commit operation of Tj .
Jednostavno rečeno, ako Transaction2 zavisi od Transaction1, onda Transaction1 mora završiti operaciju potvrde pre nego što Transaction2 bude potvrđena:

Međutim, to nije sve; kako se broj transakcija povećava, ceo proces oporavka postaje sve složeniji, pa oporavak od grešaka u transakcijama nije tako lak zadatak.

U događaju prikazanom na gornjoj slici, Transaction2 zavisi od Transaction1, a Transaction3 takođe zavisi od Transaction1. Kada Transaction1 zbog greške u izvršavanju mora da se poništi, da bi se očuvala atomarnost transakcije, poništiće se i sav rad u Transaction2 i Transaction3. Ova situacija se naziva kaskadno poništavanje (Cascading Rollback). Kaskadno poništavanje zahteva poništavanje velike količine rada, što je teško prihvatiti; ipak, ako želimo apsolutnu atomarnost, moramo se suočiti s tim problemom. Kasnije u članku ćemo detaljno objasniti kako se rešava atomarnost paralelnih transakcija.
Trajnost
Pošto je u pitanju baza podataka, postoji veoma jaka potreba za trajnim skladištenjem podataka; ako su podaci upisani u bazu, onda oni moraju biti bezbedno sačuvani na disku. Trajnost transakcije ogleda se upravo u tome da, jednom kada je transakcija potvrđena, podaci sigurno bivaju upisani u bazu i trajno sačuvani.

Kada je transakcija jednom potvrđena, više je nije moguće poništiti; jedini način da se povuče potvrđena transakcija jeste kreiranje suprotne transakcije koja originalnu operaciju „kompenzuje“ — i to je jedan od izraza trajnosti transakcije.
Dnevnik ponavljanja
Kao i atomarnost, i trajnost transakcije se ostvaruje preko dnevnika. MySQL koristi dnevnik ponavljanja (redo log) za implementaciju trajnosti transakcije. Dnevnik ponavljanja sastoji se od dva dela: prvi je bafer dnevnika ponavljanja u memoriji; pošto je u memoriji, on je isparljiv. Drugi deo je fajl dnevnika ponavljanja na disku, koji je trajan.

Kada u okviru transakcije pokušamo da izmenimo podatke, sistem prvo učita podatke sa diska u memoriju, ažurira keširane podatke u memoriji, zatim generiše jedan zapis dnevnika ponavljanja i upisuje ga u bafer dnevnika ponavljanja. Kada transakcija zaista bude potvrđena, MySQL će sadržaj bafera dnevnika ponavljanja isprazniti u fajl dnevnika ponavljanja, a zatim ažurirati podatke iz memorije na disk. Koraci 4 i 5 na slici izvršavaju se upravo pri potvrdi transakcije.
U InnoDB-u se dnevnik ponavljanja čuva u blokovima od 512 bajtova; pošto je veličina bloka jednaka veličini sektora na disku, upis dnevnika ponavljanja može garantovati atomarnost i neće, usled nestanka struje, ostati upisan samo pola dnevnika i time ostaviti prljave podatke.
Pored toga što sve izmene nad bazom generišu dnevnik ponavljanja, pošto i dnevnik poništavanja mora biti trajno sačuvan, i za njega se kreira odgovarajući dnevnik ponavljanja. Nakon greške, pri ponovnom pokretanju baze, sistem iz dnevnika ponavljanja pronalazi zapise koji nisu ažurirani na disk baze i ponovo ih izvršava kako bi zadovoljio trajnost transakcije.
Dnevnik poništavanja i dnevnik ponavljanja
Do sada smo upoznali dve vrste dnevnika u MySQL-u: dnevnik poništavanja (undo log) i dnevnik ponavljanja (redo log). U sistemu baze podataka atomarnost i trajnost transakcija garantuje dnevnik transakcija (transaction log), a u implementaciji to su upravo gore pomenuta dva dnevnika: prvi služi za poništavanje efekata transakcije, a drugi za ponavljanje već potvrđenih transakcija pri obradi grešaka. Oni garantuju dve stvari:
- Transakcija kod koje dođe do greške ili koju je potrebno poništiti može uspešno biti poništena (atomarnost);
- Kada sistem padne nakon potvrde transakcije, a pre nego što podaci stignu da se upišu na disk, pri sledećem pokretanju podaci mogu uspešno biti obnovljeni (trajnost);
U bazi podataka ova dva dnevnika često rade zajedno; možemo ih shvatiti kao jedan dnevnik transakcija koji sadrži ID transakcije, izmenjene elemente redova i vrednosti pre i posle izmene.

Jedan zapis dnevnika transakcija istovremeno sadrži i vrednost pre i vrednost posle izmene, pa se njime vrlo jednostavno mogu obaviti i poništavanje i ponavljanje. Ovde nećemo detaljnije razrađivati dnevnik ponavljanja i poništavanja; možda ćemo o korišćenju ova dva dnevnika govoriti u nekom kasnijem članku o mehanizmu oporavka sistema baze podataka.
Izolacija
Autor je već u ranijem članku „Uvod u MySQL i InnoDB“ obradio izolaciju transakcija baze, ali radi nezavisnosti i potpunosti članka, ovde ćemo ponovo obraditi izolaciju transakcija; sadržaj može biti nešto drugačiji.
Izolacija transakcija jedna je od nekoliko osnova obrade podataka u bazi. Ako između transakcija baze ne bi postojala izolacija, došlo bi do problema poput kaskadnog poništavanja opisanih u odeljku Atomarnost paralelnih transakcija, što bi izazvalo ogromne gubitke u performansama. Ako bi redosled izvršavanja svih transakcija bio linearni, upravljanje transakcijama bi bilo mnogo lakše, ali dozvoljeno paralelno izvršavanje transakcija može povećati propusnost i iskorišćenost resursa i skratiti vreme čekanja svake transakcije.

Kada više transakcija istovremeno konkurentno izvršava, izolacija transakcija može biti narušena; iako pojedinačna transakcija možda nema nikakvih grešaka, posmatrano u celini doći će do problema sa konzistencijom baze. Serijsko izvršavanje, s druge strane, omogućava programeru da zanemari efekte paralelnosti i dobro održava konzistenciju baze, ali utiče na performanse izvršavanja transakcija.
Nivoi izolacije transakcija
Dakle, izolacija i konzistencija baze zapravo su pitanje koje programer mora da odmeri; to koji nivo izolacije se obezbeđuje određuje i performanse baze i konzistenciju koja se može postići. U SQL standardu definisana su četiri nivoa izolacije transakcija: READ UNCOMMITED, READ COMMITED, REPEATABLE READ i SERIALIZABLE; svaki nivo izolacije rešava jedan problem više u odnosu na prethodni:
RAED UNCOMMITED: upit ne postavlja brave, može se pročitati i nepotvrđeni red (Dirty Read);READ COMMITED: postavlja se samo brava na zapis, bez zazora između zapisa, tako da je ubacivanje novih zapisa u blizini zaključanih dozvoljeno, pa pri više uzastopnih upita mogući različiti rezultati (Non-Repeatable Read);REPEATABLE READ: višestruko čitanje istog opsega vratilo bi snapshot prvog upita, ne vraća različite redove podataka, ali može doći do fantomskog čitanja (Phantom Read);SERIALIZABLE: InnoDB implicitno dodaje deljenu bravu svim upitima i rešava problem fantomskog čitanja;
Nijedan od gore navedenih nivoa izolacije ne dozvoljava prljav upis (Dirty Write), odnosno situaciju u kojoj tekuća transakcija ažurira podatak koji je druga transakcija već ažurirala, ali još nije potvrdila. Većina baza koristi READ COMMITED kao podrazumevani nivo izolacije, ali MySQL koristi REPEATABLE READ kao podrazumevanu konfiguraciju. Od RAED UNCOMMITED ka SERIALIZABLE, kako nivo izolacije postaje sve stroži, performanse baze pri konkurentnom izvršavanju transakcija postepeno opadaju.

Za korisnika baze, teorijski gledano, nije potrebno da zna kako su nivoi izolacije implementirani; dovoljno je da zna koji problem taj nivo rešava. Međutim, detalji implementacije različitih nivoa izolacije u različitim bazama nas u mnogim slučajevima mogu dovesti do neočekivanih zamki.
Ako čitalac ne zna šta tačno predstavljaju prljavo čitanje, neponovljivo čitanje i fantomsko čitanje, može pročitati prethodni članak „Uvod u MySQL i InnoDB“; ovde ćemo samo prikazati jednu sliku koja pokazuje kako pojedini nivoi izolacije rešavaju ove probleme.

Implementacija nivoa izolacije
Implementacija nivoa izolacije u bazi podataka sastoji se u korišćenju mehanizama kontrole konkurencije za upravljanje transakcijama koje se izvršavaju u isto vreme i ograničavanje pristupa i ažuriranja istog resursa od strane različitih transakcija. U nastavku ćemo kratko objasniti rad tri najvažnija mehanizma kontrole konkurencije.
Brave
Brave su najčešći mehanizam kontrole konkurencije. U okviru jedne transakcije ne zaključavamo celu bazu, već samo one stavke podataka kojima pristupamo. I u MySQL-u i u ostalim uobičajenim bazama brave se dele u dve vrste: deljena brava (Shared) i isključiva brava (Exclusive); prva se još zove brava za čitanje, a druga brava za pisanje.

Brava za čitanje obezbeđuje da operacije čitanja mogu konkurentno da se izvršavaju bez međusobnog ometanja, dok brava za pisanje garantuje da prilikom ažuriranja podataka u bazi nijedna druga transakcija neće pristupati ili menjati isti zapis, čime se izbegavaju nepredvidivi problemi.
Vremenske oznake
Pored brava, drugi način implementacije izolacije transakcija je preko vremenskih oznaka (timestamp). Baze koje koriste ovaj pristup, na primer PostgreSQL, za svaki zapis čuvaju dva polja; vremenska oznaka čitanja sadrži najveću vremensku oznaku među svim transakcijama koje su pristupile tom zapisu, dok vremenska oznaka pisanja zapisa čuva vremensku oznaku transakcije koja je zapis promenila na trenutnu vrednost.

Prilikom implementacije izolacije transakcija pomoću vremenskih oznaka, često se koristi optimistička brava: prvo se izvrši izmena podataka, a prilikom upisa nazad proveri se da li se trenutna vrednost, odnosno vremenska oznaka, promenila. Ako nije, vrši se upis; inače se generiše nova vremenska oznaka i podatak se ponovo ažurira. Optimistička brava zapravo nije pravi mehanizam brava, već samo jedna ideja, pa je ovde nećemo dalje razrađivati.
Više verzija i izolacija snapshotom
Održavanjem više verzija podataka, baza može dozvoliti transakcijama da čitaju stare verzije podataka dok ih druge transakcije ažuriraju; mnoge baze su implementirale ovaj mehanizam. Pošto operacije čitanja više ne moraju da čekaju oslobađanje brave za pisanje, značajno se poboljšavaju performanse čitanja. I MySQL i PostgreSQL su implementirali svoje verzije ovog mehanizma, poznate kao MVCC. Iako im se implementacije razlikuju, MySQL je MVCC ostvario preko dnevnika poništavanja pomenutog u članku, čime se obezbeđuje da transakcije pri paralelnom izvršavanju mogu preuzeti podatke bez čekanja na oslobađanje isključive brave.
Izolacija i atomarnost
Ovde treba kratko pomenuti probleme poput kaskadnog poništavanja na koje smo naišli u odeljku o atomarnosti. Ako jedna transakcija upisuje podatak, pri tome dobija isključivu bravu, pa ostale transakcije koje žele da dobiju bravu za čitanje tog reda moraju sačekati oslobađanje brave za pisanje — prirodno se time izbegavaju kaskadna poništavanja i slični problemi.

Međutim, u većini baza, uključujući MySQL, koriste se osobine poput MVCC-a, što znači da normalno čitanje ne zahteva brašćenje. Kada želimo da ažuriramo pročitani podatak, potrebno je koristiti SELECT ... FOR UPDATE kako bi se pokušalo dobiti isključiva brava za odgovarajući red, radi obezbeđivanja normalnog rada različitih transakcija.
Konzistencija
Autor smatra da je konzistencija baze podataka veoma zbunjujuć pojam, jer oblast baza podataka zapravo sadrži dve konzistencije: jedna je konzistencija u ACID-u, a druga konzistencija iz CAP definicije.

Ove dve konzistencije baze podataka ni uopšte nisu ista stvar; mnogo, mnogo ljudi ima veoma duboko pogrešno shvatanje ova dva pojma. Kada razgovaramo o konzistenciji baze podataka, obavezno moramo razumeti kontekst i nastojati jasno da pojasnimo o kojoj konzistenciji govorimo — o konzistenciji u ACID-u ili o konzistenciji u CAP-u.
ACID
Definicija konzistencije u ACID-u koju daje baza podataka je sledeća: ako se transakcija atomski i nezavisno izvrši u konzistentnoj bazi, nakon njenog izvršenja stanje baze sigurno će biti konzistentno. Prvo značenje ovog pojma odnosi se na ograničenja integriteta podataka, uključujući ograničenje primarnog ključa, referencijalna ograničenja i razne provere ograničenja; pre, za vreme i nakon izvršenja transakcije ne smeju biti prekršena ograničenja integriteta podataka. Svi upisi u bazu treba da budu dozvoljeni i ne smeju dovesti do nedozvoljenog stanja podataka.
A transaction must preserve database consistency - if a transaction is run atomically in isolation starting from a consistent database, the database must again be consistent at the end of the transaction.
Možemo transakciju shvatiti kao funkciju koja prima spoljni SQL ulaz i konzistentnu bazu, a nužno vraća konzistentnu bazu.

Drugo značenje zapravo se odnosi na logički zahtev prema programeru: u kodu moramo napisati ispravnu logiku transakcije. Na primer, kod novčanog transfera, logika transakcije ne može samo da odbije novac sa računa ili samo da doda novac — to je zahtev na nivou aplikacije za konzistenciju baze podataka.
Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - Database System Concepts
Konzistencija u ACID-u postavlja prema transakciji zahteve koji ne obuhvataju samo provere integriteta i dozvoljenosti podataka, već i ispravnost logike na nivou aplikacije.
Konzistencija podataka u CAP teoremi zapravo govori o tome da različiti čvorovi u distribuiranom sistemu imaju istu vrednost za iste kopije podataka; dok konzistencija u ACID-u označava pravila baze — ako šema određuje da vrednost mora biti jedinstvena, onda konzistentan sistem mora osigurati da ta vrednost u svim operacijama ostane jedinstvena. Prema tome, definicije konzistencije u CAP-u i ACID-u su fundamentalno različite.
Rezime
Četiri osnovne ACID karakteristike transakcija temelj su na kojem se oslanja rad baze podataka. Međutim, potpuno garancija ACID-a, posebno izolacije, znatno utiče na performanse; u praksi ćemo prema poslovnim potrebama prilagođavati nivo izolacije. Pored izolacije, atomarnost i trajnost transakcija verovatno su lakše razumljive karakteristike: prva garantuje da se transakcija u bazi ili izvrši u celosti ili uopšte ne izvrši, dok druga garantuje da su upisi u bazu trajno sačuvani i neposredni. Konzistencija, sa druge strane, nije samo zahtev baze prema integritetu sopstvenih podataka, već postavlja i zahteve prema programeru — da piše logički ispravne i razumne transakcije.
Konačno, i najvažnije: kada neko govori o konzistenciji, obavezno razjasnite kontekst. Ako imate pitanja o sadržaju članka, možete ostaviti komentar.
Reference
- Database System Concepts
- Transakcija baze podataka
- How does MVCC (Multi-Version Concurrency Control) work
- How does a relational database work
- Implementing Transaction Processing using Redo Logs
- Implementing Transaction Processing using Undo Logs
- Undo/Redo Logging Rules
- MySQL dešifrovano: šetnja kroz dnevnik ponavljanja InnoDB skladišnog pogona
- Razlika između C u ACID-u i C u CAP teoremi
- Disambiguating ACID and CAP
Referentni link: https://draveness.me/mysql-transaction/, obradio: Chenmo Wang Er
