Dr. Vermes Mátyás1
2000. június 1.
begin sequence tx:=tranBegin() ... 1-könyvelés ... 2-könyvelés ... tranCommit(tx) recover tranRollback(tx) end sequence
Ha a könyvelések során valamilyen hiba keletkezik (pl. nem lockolható egy rekord, nincs fedezet, nincs árfolyam, nem található egy számla, stb.), akkor csak egy break-et kell végrehajtani, és a félig sikerült könyveléseknek nyoma sincs az adatbázisban. Akkor is ez a helyzet, ha CTRL-C-t nyomnak, vagy, ha elszáll a program. A fenti tranzakció elvileg csak úgy vezethet inkonzisztens adatbázistartalomhoz, ha a lefagyás a könyvtári tranCommit végrehajtása alatt következik be.
Az összes módosítás a memóriában tárolódik mindaddig, amíg végül tranCommit ki nem írja az adatbázisba. Mivel commit előtt a módosítások nem íródnak a lemezre, azok csak a tranzakciót végző processz számára láthatók, idegen processzek számára nem. A processz a saját tranzakciójának részeredményeit látja, ezért a tranzakció többször is érintheti ugyanazt a rekordot, pl. az egymás utáni könyvelések összegei megfelelően fognak halmozódni a számlán.
tranBegin() egy számot ad, ami a tranzakció indexe a tranzakciókat tároló stackben (array). A továbbiakban tranBegin() és tranCommit() között:
tranRollback(tx) eldobja azokat a változásokat, amik a tx azonosítójú tranzakció kezdete óta történtek. Ha nem marad aktív tranzakció, akkor elengedi a lockokat, egyébként minden lockot megtart. Ha tx==NIL, akkor a legutolsó tranzakciót dobja el, ha tx==1, akkor minden tranzakciót eldob. A tranzakció után pontosan azok a lockok maradnak meg, amik a tranzakció előtt is megvoltak.
Tranzakció közben lemezre írja a table aktuális rekordját. Olyankor használjuk, amikor egy közös számláló tartalmát, pl. ugyszam(), vagy egy új egyedi kulcsot, pl. tabInsert(), láhatóvá akarunk tenni a többi program számára. Ha a lockflag értéke .t., akkor elengedi a szinkronizált rekord lockját (minden más lockot megtart). Az ügyszámot tartalmazó rekordot célszerű rögtön elengedni, máskülönben a tranzakció teljesen sorbaállítaná a konkurens programokat.
A tranzakcióban keletkezett változások tranCommit előtt még nincsenek kiírva az adatbázisba, ezért az új adatok csak a tranzakciót végző processz számára láthatók. Mivel az indexek sincsenek módosítva, azért a tranzakció közbeni navigálás, mindig a tábla tranzakció előtti állapotának megfelelő eredménnyel jár. Ennek következményeként bizonyos algoritmusok, amik a kiírt adat visszakeresésén alapulnak nem működnek tranzakcióban. A klibraryban lévő ugyszam() és a tabInsert() függvények azonnal láthatóvá teszik az új rekordot egy tranSynchronizeRecord() hívással.
A tranzakciós navigálás plusz feladata, hogy fel kell ismerni
a tranzakcióban módosított rekordokat, és a legfrissebb verzióval
helyettesíteni kell a lemezen található adatokat. Eközben
el kell igazodni az eredetileg is törölt állapotú,
a tranzakció közben törlődött és a kifilterezett rekordok között.
A navigálás algoritmusa tranzakcióban a következő:
A törlés megegyezik egy olyan módosítással, ami a deleted mezőt true-ra állítja, és (kompatibilitási okból) eggyel előrelép. A törölt rekord lockja is megmarad.
A táblaobjektum gotop, gobottom, seek, skip metódusai számára a tranzakcióban appendált (és ezért törölt) rekordok láthatatlanok. Az új rekordokra ezért csak a goto metódussal lehet visszapozícionálni.
A legtöbb bonyodalmat az append megvalósítása okozza,
néhány szó a részletekről: A tranzakcióban appendált rekordoknak
láthatatlannak kell lennie a többi processz számára, valamit viszont
mégis csak ki kell írni, hogy az új rekord recno-t kaphasson.
A láthatatlanság két módon érhető el:
A tranzakcióban appendált rekordokat a gotop, gobottom, seek, skip
utasítások nem látják, az ilyen rekordokra csak a goto-val lehet
rápozícionálni. Az egyes adatbáziskezelőkben a következő a helyzet.
Ha a tranzakción belül goto-t hajtunk végre az új rekordra,
akkor a goto megtalálja az új rekord memóriabeli változatát,
és azt behelyettesíti. A commit-ban ki fognak íródni az indexek,
ami után a rekord a normál navigáció számára is látható lesz.
A tranzakció közben a többi processz számára az új rekord teljesen
láthatatlan, azaz még goto-val sem tudnak rápozícionálni.
A recno sorrend szerinti navigálás számára azért lesz láthatatlan
a rekord, mert a lemezre törölt flaggel írjuk ki. Amikor goto-val
rápozícionálunk, akkor behelyettesítődik a memóriában tárolt
normál (nem törölt) állapotú rekord. A commit a nem törölt
állapotot fogja lemezre írni, a commit elmaradása,
vagy rollback esetén a rekord végleg törölt marad.
A recno kiírása miatt az idegen processzek számára az új rekord
nem teljesen láthatatlan, ui. goto-val be tudják olvasni a lemezen
törölt, kitöltetlen állapotban levő rekordot, ennek azonban
nincs nagy gyakorlati jelentősége.
A rendszer a tranzakció közbeni változásokat a memóriában gyűjti egészen a végső tranCommit-ig, ebből adódóan nem képes akármilyen nagy tranzakciót végrehajtani. Kerülendők az olyan tranzakciók, amik egy tábla minden rekordját módosítják, és gyakran OREF overflow2 hibához vezetnek. Ugyanezért a tranzakciókezelésből ki kellett zárni a tömeges módosítást tartalmazó metódusokat. Ha tranzakció közben próbáljuk meg végrehajtani az alábbi műveleteket, runtime errort kapunk:
A tranzakcióban módosult táblára tiltottak az alábbi műveletek:
A tranzakciók különösen megkönnyítik a debugolást. Állítsuk be a TRANCOMMIT=debug környezeti változót, ilyenkor a rendszer minden változást listáz, így azonnal látszik, hogy mit művel a program az adatbázisban.
Az itt közölt példaprogram megtalálható a ccctutor/tran directoryban. Fordítsuk le a próba projektet (m script), majd a ccctutor/tran/test directoryban indítsuk el s-et.
#include "table.ch" #include "_proba.ch" ***************************************************************************** function main() local dd PROBA:create PROBA:open(OPEN_EXCLUSIVE) PROBA:zap //teszt adatbázis app("a","Van, aki forrón szereti.") app("b","Van, aki forrón szereti.") app("c","Van, aki forrón szereti.") app("d","Van, aki forrón szereti.") view("INDULÓ") tranBegin() begin sequence PROBA:goto(2); PROBA_TEXT:="Próba szerencse." view("módos1") //break(NIL)
Navigáció közben látjuk a 2-es rekord változását, ha azonban itt kilépnénk (break), akkor az adatbázis eredeti (INDULÓ) állapota maradna meg.
app("bb","Van, aki forrón szereti.") view("app1") rec()
Betettünk egy új rekordot, ám az a navigáció közben nem látszik. Csak goto-val lehet rápozícionálni, ahogy az pl. a view() végén lévő PROBA:restore-ral történik. A rec() kiírás mutatja, hogy azért megvan a rekord.
PROBA:control("field") //korlátozás feloldva PROBA:goto(3); PROBA_TEXT:="Próba szerencse." PROBA:goto(4); PROBA:delete app("cc","Van, aki forrón szereti.") tranSynchronizeRecord(PROBA:table) app("dd","Van, aki forrón szereti.") dd:=PROBA:position view("módos2")
Mindenféle módosítás. A törölt rekord kikerül a navigáció látóköréből. A navigáció most sem mutatja az új rekordokat, kivéve, ha azt külön utasítással szinkronizáljuk. A dd kulcsú rekord pozícióját megjegyeztük későbbi használatra.
PROBA:seek("c") PROBA_FIELD:="aaa" view("seek")
A "c" kulcsot átírtuk "aaa"-ra. A navigáció az új kulcsot mutatja, de a régi kulcsnak megfelelő helyen, ui. először megtörténik a pozícionálás a még nem módosult indexekkel, majd az eredményre rátöltődnek a tranzakció közben készült (és a memóriában tárolt) változások.
PROBA:seek("c"); rec() PROBA:seek("aa"); rec() PROBA:gobottom(); rec()
Ezek eredménye szintén mutatja, hogy a navigálás az index tranzakció előtti állapotának megfelelő eredményt ad.
PROBA:goto(dd); PROBA_TEXT:=upper(PROBA_TEXT) //break()
A tranzakcióban létrejött rekordokra goto-val lehet visszapozícionálni és a tartalmukat újra felhasználni. Ha itt break-et csinálnánk, az induló állapothoz képest csak a külön szinkronizált cc rekord volna az eltérés.
tranCommit() recover tranRollback() end sequence view("VÉGSŐ")
Kiíródnak a módosítások, aktualizálódnak az indexek, így a navigáció mutatja az eddig rejtett rekordokat is.
return NIL ***************************************************************************** static function app(fld,txt) PROBA:append PROBA_TEXT:=txt PROBA_FIELD:=fld return NIL ***************************************************************************** static function rec() ? PROBA:position, PROBA_FIELD, PROBA_TEXT ? return NIL ***************************************************************************** static function view(txt) local state:=PROBA:save ? padr(txt,6),">", PROBA:position PROBA:gotop while( !PROBA:eof ) ? padr(txt,6),">", PROBA:position, PROBA_FIELD, PROBA_TEXT PROBA:skip end ? PROBA:restore:=state return NIL ***************************************************************************** static function view0(txt) local state:=PROBA:save, n ? padr(txt,6),">", PROBA:position for n:=1 to PROBA:lastrec PROBA:goto(n) ? padr(txt,6),">", PROBA:position, PROBA_FIELD, PROBA_TEXT next ? PROBA:restore:=state return NIL *****************************************************************************
Ez a tranzakciókezelő rendszer az általunk használt táblaobjektumokkal megvalósítható. Hangsúlyozom, hogy a program kifejezetten egyszerű (kevés, az eredeti kódtól jól elkülönülő kiegészítésről van csak szó), ezért bízni lehet abban, hogy sikerül stabilra megcsinálni. Ez az egyszerű tranzakciókezelés a máshol (pl. Oracle) elérhető funkcionalitásnak legalább 90%-át biztosítja, ugyanakkor a hatékonyságban szinte semmi csökkenést nem okoz.
1ComFirm BT.
2A CCC az OREF_SIZE környezeti változóban megadott darabszámú objektumot (stringet, arrayt, kódblokkot) tud tárolni, ha a program ezt túllépi, akkor keletkezik az ismert OREF overflow hiba. OREF_SIZE deafult értéke 8192, ami kis és közepes programokhoz elegendő, de általában legalább 20000-re van beállítva.