You are on page 1of 20

Bazy

Danych

Zbudujmy sobie bazę danych


Paweł Marciniak
– LhimkDB. Część 1
J
est bardo dużo różnych baz danych. Są bar-
dzo dobre bazy danych dostępne za darmo ������� ������������������

z całym kodem źródłowym, bez żadnych ogra- ����������


��������� �����������
niczeń licencyjnych (np. PostgreSQL). Po co zatem
budować jeszcze jedną? Jest kilka powodów. �������
��������������

Po pierwsze, będziemy budować bazę danych do �������


���������
�������

wbudowywania do programów (ang. embedded da-


tabase), o architekturze klucz-wartość (ang. key-va- ���

lue). Jest oczywiście dużo takich baz danych dostęp- �������


��������������
��������� ����������

nych na licencjach open source, ale bardzo nielicz- �����


ne można używać bez ograniczeń w programach ko-
mercyjnych. Najpopularniejsza jest Berkeley DB (http:// Rysunek 1. Architektura LhimkDB
www.sleepycat.com/), która wymaga udostępniania ko-
du źródłowego programu za darmo. Jest bardzo cieka- musiała mieć swoją własną partycję na dysku. Nikt przy
wa baza danych PBL (http://mission.base.com/peter/ zdrowych zmysłach nie używał standardowego syste-
source/) na licencji LGPL, ale niestety ma ograniczo- mu plików. Potem zaczęły się pojawiać bazy, które sa-
ną długość klucza do 255 bajtów. Warta zobacze- me potrafiły powiększać swoje pliki (nie wymagały ad-
nia jest baza danych MetaKit (http://www.equi4.com/ ministratora!). Dziś wydaje się to oczywiste, ale kiedyś
metakit.html), dostępna na licencji BSD, ale nie jest to takie nie było. Może czas na kolejne zmiany.
baza danych klucz-wartość. MetaKit jest bazą danych Oczywiście nikt przystępując do pisania kolejnej
opartą na kolumnach (ang. column based). Jest jesz- aplikacji nie będzie pisał własnej bazy danych (no
cze dużo innych projektów, ale, albo mają restrykcyj- może prawie nikt). Nawet jeśli istniejące rozwiąza-
ną licencję, albo nie mają transakcji, albo jeszcze cze- nia mają złe licencje, lub są przestarzałe. Lepiej jed-
goś innego im brak. W sumie, nie znalazłem dobrej ba- nak wziąć sprawdzone, dobrze przetestowane roz-
zy danych klucz-wartość do wbudowywania do progra- wiązanie, w końcu baza danych musi być w 100 %
mów. To może warto taką bazę zbudować? niezawodna. Z drugiej strony, warto przynajmniej
Najprościej by było (tak się wydaje), wyjąć kawałek zobaczyć jak taka baza jest zbudowana, a najlepiej
PostgreSQL-a, który odpowiada za samo przechowy- choćby spróbować coś samemu napisać. Zrozumie-
wanie danych w indeksach (czyli w postaci klucz-war- my wtedy lepiej jak baza danych działa, a wszystkie
tość). PostgreSQL korzysta z biblioteki Gist, więc jest to one działają podobnie, i będziemy mogli dużo lepiej
dobrze wydzielony fragment kodu. Jednak PostgreSQL wykorzystywać gotowe rozwiązania. Pojawi się też
jest bazą danych budowaną na algorytmach z lat 70. w naszym życiu więcej humoru, jeśli czytamy rekla-
i 80., oczywiście odnawianych, ale wciąż podstawą jego my Poważnych Wielkich Producentów Baz Danych.
działania są standardowe algorytmy. Skoro budujemy Niniejszy artykuł jest pierwszą częścią cztero-
coś nowego, to czemu nie zrobić tego inaczej? Podsta- częściowej serii, w której zbudujemy od podstaw no-
wy budowy baz danych, praktyczne i teoretyczne, były woczesną bazę danych do wbudowywania. Ta część
formułowane w latach, kiedy komputer miał kilka kilobaj- jest ogólnym przeglądem całego projektu, który ma
tów RAM-u i kilku megabajtowy dysk twardy. Wszelkie decydujący wpływ na podjęte szczegółowe decyzje.
rozważania opierały się na tym, że najbardziej kosztow- W następnych częściach będziemy omawiać krok po
ną czasowo operacją jest komunikacja z dyskiem twar- kroku wszystkie elementy tej bazy, wracając do omó-
dym. Komunikacja ta polega na zapisie i odczycie stron wionych tutaj decyzji.
(ang. pages) o stałej wielkości. Generalnie jest to nadal
prawdą, ale pojawiło się wiele nowych elementów, jak Cel
np. systemowe: pamięć wirtualna, mapowanie plików Wypiszmy sobie cele, jakie stawiamy przed nową
itp., czy sprzętowe: operacje CAS, pamięć stała oparta bazą danych. Po pierwsze, o czym już pisałem, ma
o FLASH, RAM itp. Jeszcze kilka lat temu baza danych to być baza danych do wbudowywania (czyli biblio-
teka), w architekturze klucz-wartość. Nie lubię ogra-
Paweł od 20 lat zajmuje się tworzeniem oprogramowa-
niczeń, więc najlepiej niech klucz i wartość będą “do-
nia, głównie w C/C++. Od trzech lat rozwija system dy- wolnej” długości. Sama baza danych też nie powin-
namicznej kompilacji Lhimk. na mieć ograniczeń na wielkość. Oczywiście chcemy
Kontakt z autorem: pawel@software.com.pl mieć wsparcie dla transakcji, najlepiej też dla trans-
akcji zagnieżdżonych, a jak się uda, to też dla dłu-

22 www.sdjournal.org Software Developer’s Journal 11/2005


Zbudujmy sobie bazę danych – LhimkDB

gich (ang. long running transactions). Sama architektura po- Najwyższa warstwa obsługuje transakcje aplikacji. LhimkDB
winna być otwarta, czyli powinna dać się rozszerzać i wbudo- jest zbudowany korzystając z algorytmów nieblokujących, nie wy-
wywać np. nowe typy indeksów. Ważne jest też, żeby baza stępuje w niej zjawisko zakleszczenia (ang. deadlock). Jeżeli wy-
danych szybko odtwarzała swój stan po awarii i pozwalała ro- stąpi konflikt wśród transakcji, to jedna z nich jest unieważniana.
bić gorące kopie (ang. hot backups).Jest to krytyczne w serwi- Jeżeli jakaś transakcja może być wciąż unieważniana powsta-
sach internetowych, gdzie system musi działać cały czas i nie je tzw. livelock. Zapobiega się temu np. gwarantując, że transak-
możemy wykonywać czynności administracyjnych, wymagają- cja, która zaczęła wcześniej na pewno skończy z powodzeniem.
cych odłączenia bazy danych choćby na kilka minut. Przyjrzyjmy się teraz po kolei tym wszystkim warstwom.

Projekt Warstwa przechowywania – UDB


Od razu zacznę od najbardziej kontrowersyjnej decyzji. Otóż Baza danych to nic innego, jak uporządkowana metoda dostępu
LhimkDB jest napisany w języku i środowisku Lhimk. Jesz- do danych na dysku (lub ogólniej – w pamięci trwałej). Zawsze
cze przez chwilę nie przestawaj czytać, a zaraz wszyst- u podstawy każdej bazy danych leży jakiś zestaw mechanizmów
ko wyjaśnię. LhimkDB najpierw został napisany w języku C dający dostęp do zapisanych danych. Większość baz danych ma
(w oparciu o kod SkipDB autorstwa Steva Dekorte, http:// własne mechanizmy stronicowania. Polegają one na podziele-
www.dekorte.com/). Początkowo myślałem, że będzie to bi- niu pliku (lub partycji) na strony, które są wczytywane do pamięci
blioteka napisana w C i włączona do Lhimka jako DLL. Jed- i zapisywane na dysk. Specjalne algorytmy decydują o tym, kie-
nak w trakcie pisania, okazało się, że dużo wygodniej będzie dy i jakie strony wczytać, i które strony można zwolnić na dysk,
ten kod przepisać w Lhimku. Został jednak oryginalny kod a które trzymać nadal w pamięci. Oczywiście mamy tu też odpo-
w C, więc jeżeli komuś bardzo przeszkadza Lhimk, to może wiednie patenty, które urozmaicają życie programistów.
wziąć wersję w C i ją dopracować (kod w C jest na poziomie W starszych bazach danych występuje problem tzw. po-
późnej bety, ma wszystkie opisywane tutaj mechanizmy). Jed- dwójnego stronicowania (ang. double paging). Polega on na
nak Lhimk nie powinien być żadną barierą, jeżeli ktoś szuka tym, że jeżeli wczytujemy stronę z dysku do pamięci, to może
biblioteki do baz danych do C/C++ (lub innych języków, któ- być ona zapisana, bez naszej ingerencji, z powrotem na dysk
re potrafią z bibliotek C korzystać). Lhimk jest oparty na C, przez system operacyjny, w ramach obsługi pamięci wirtual-
jest wstecznie zgodny z C i można go używać jako biblioteki nej. Tak się dzieje, kiedy baza danych konkuruje o pamięć z in-
w programach napisanych w C. Można też program w C nymi programami, np. z serwerem WWW. Rozwiązaniem tego
skompilować jako bibliotekę i zanurzyć w Lhimku. Tak czy in- problemu jest mapowanie stron na pamięć, przy użyciu funkcji
aczej, bez żadnych problemów można korzystać z LhimkDB mmap (wszystkie funkcje systemowe podaję dla systemu Linux).
w programach C/C++. Więcej o Lhimku w ramce obok. LhimkDB nie posiada własnego mechanizmu stronicowania.
Wróćmy do bazy danych. Każda baza danych składa się Doszedłem do wniosku, że najlepiej będzie korzystać z mechani-
z kilku współpracujących ze sobą warstw (Rysunek 1.). Na sa- zmów systemu operacyjnego. Pierwszą zaletą jest to, że, oczy-
mym dole jest warstwa obsługująca przechowywanie danych wiście, nie trzeba tego kodu pisać, i przez to LhimkDB jest prost-
na dysku (np. w plikach). Można ją sobie wyobrażać jak stertę szy i łatwiejszy w utrzymaniu. Druga zaleta jest taka, że system
pamięci (ang. heap) z możliwością zapisu na dysk. Zapis ten mapując pliki na pamięć wykorzystuje niskopoziomowe mechani-
musi być tak zorganizowany, żeby w przypadku awarii baza zmy procesorów. Adres w pamięci jest tłumaczony na adres stro-
mogła wrócić do poprzedniego poprawnego stanu. ny+przesunięcie przez procesor z wykorzystaniem bardzo zopty-
Dalej jest warstwa, która organizuje dane tak, żeby można je malizowanych algorytmów (więcej na ten temat można przeczy-
było łatwo odszukać po kluczu. Jest to rodzaj pamięci asocjacyj- tać książce Understanding the Linux Virtual Memory Manager
nej, takiej jak np. hash tablica, ale w naszym wypadku wymaga- Mela Gormana, która jest dostępna w internecie). Cała obsługa
my jeszcze, żeby dane dały się przeglądać posortowane według pamięci trwałej ze stronicowaniem polega na wywoływaniu mmap,
klucza. Do tego celu najczęściej używa się odmian zbalansowa- kiedy wczytujemy plik, msync, kiedy chcemy zapisać dane na
nych drzew (B-Tree), my użyjemy struktury Skip List. dysk, a zapis i odczyt wykonywane są przy pomocy memmove. Ta-
kie rozwiązanie ma też wady. Po pierwsze, jeżeli mapujemy ca-
ły plik na pamięć, to pojawia nam się ograniczenie wielkości pliku
Lhimk do 3 GB, a w praktyce pewnie do 1 GB. Można to obejść na dwa
Lhimk jest środowiskiem dynamicznej kompilacji dla języka o tej proste sposoby. Można podzielić plik na obszary, ale bardzo du-
samej nazwie (Lhimk), który jest oparty na C. Lhimk jest językiem że, np. Po 100 MB, wtedy wielkość zapisywanych danych będzie
obiektowym, o bardzo prostej składni, w zasadzie, każdy kto zna C/ ograniczona do wielkości takiej “strony”. Drugim sposobem jest
C++ może od razu zacząć programować w Lhimku. Do składni C są użycie 64-bitowego adresowania. Oba wyjścia mają sens i mogą
dodane klasy z wielodziedziczeniem (ang. multiinheritance) i możli- być w LhimkDB łatwo zaimplementowane.
wością przeciążania operatorów, szablony, sygnały, wyjątki i kilka Ostatnio niektóre bazy danych zaczęły stosować mmap do ob-
innych drobiazgów. Lhimk jest środowiskiem dynamicznej kompila- sługi stron, głównie, żeby uniknąć problemów z podwójnym stro-
cji, więc cały kod jest kompilowany w momencie uruchamiania pro-
nicowaniem. Bardzo rzadko spotyka się jednak rozwiązania, któ-
gramu. Kompilator jest bardzo szybki, więc trwa to krócej niż łado-
re, tak jak LhimkDB, korzystają z systemowych mechanizmów
wanie wielu bibliotek w skompilowanych programach.
Lhimk jest dostępny częściowo na licencji LGPL i częściowo
stronicowania. Stosowanie mmap oznacza, że nie mamy kontro-
na licencji BSD. Wynika to z tego, że korzysta z kodu TCC, który li nad tym kiedy dane będą zapisane na dysk. To znaczy, mo-
jest na licencji LGPL, natomiast moją intencją jest dawać wszyst- żemy się upewnić, że wszystkie brudne strony (ang. dirty pages)
ko na licencji BSD. zostały już zapisane przy pomocy msync, ale nie możemy zabro-
nić systemowi zapisać czegoś wcześniej. Na tym w końcu pole-

Software Developer’s Journal 11/2005 www.sdjournal.org 23


Bazy
Danych

�����
�� �� Atomowy odczyt i zapis
� ��
Jeżeli kilka wątków zapisuje i odczytuje tę samą pamięć, to trze-
������ ba zapewnić, żeby operacje takie były atomowe. Najprostszą i naj-
częściej stosowaną metodą jest ustawianie loków, które z kolei ko-
rzystają z muteksów (semaforów) i liczników. Ostatnio pojawiły się
������� w procesorach tzw. operacje CAS (Compare and Swap). Nie wcho-
dząc w szczegóły, pozwalają one na atomowy odczyt i zapis frag-
mentów pamięci. Coraz częściej pojawiają się artykuły, biblioteki
i eksperymentalne programy korzystające z tych operacji. Wszyst-
kie przykłady pokazują, że aplikacje budowane przy pomocy ope-
� �� racji atomowych działają szybciej niż aplikacje blokujące z lokami,
szczególnie w systemach wieloprocesorowych. LhimkDB jest przy-
Rysunek 2. Sposób zapisu danych w LhimkDB.
gotowany na pracę przy pomocy operacji CAS.
ga stronicowanie, że system decyduje co zrzucić na dysk, a co
trzymać w pamięci (istnieją polecenia systemowe mlock, mun-
lock, którymi możemy zabraniać i pozwalać systemowi zapisy- tym, że przed każdą operacją zapisujemy do osobnego pliku (lo-
wać strony na dysk, ale wtedy cała obsługa stronicowania spada gu), dane, które pozwolą nam tę operację cofnąć (tzw. undo log).
na nas). Musimy zatem tak zorganizować dane, żeby w każdej Jeżeli chcemy robić to na poziomie pliku, to możemy np. przed
chwili każda ich wersja była poprawna i dało się odtworzyć stan zmianą danych zapisać stare dane (wraz z adresem i długo-
do poprzedniej poprawnie zakończonej transakcji. ścią) do logu. W ten sposób stworzymy znany mechanizm pliku
Najczęściej bazy danych korzystają z mechanizmu Write z dziennikiem (ang. journaled file). Problem z tym rozwiązaniem
Ahead Log (WAL), czyli najpierw zapisz do logu. Polega on na jest taki, że po każdej operacji na pliku, musimy mieć gwarancję,
że plik z logiem jest fizycznie zapisany (czyli wykonać jakiś sync
na pliku z logiem). Oznacza to, że będzie tyle operacji synchroni-
�����������������������������������������������������
zacji ile zmian w bazie, a to może znacznie spowolnić działanie
�����
bazy. Drugi problem polega na umożliwieniu dostępu do takie-
�� ��
� ��
go pliku przez wiele wątków (lub procesów) jednocześnie. Zapi-
sy trzeba oczywiście wykonywać w sposób atomowy i nie mogą
������ interferować z odczytami. To można łatwo osiągnąć przy pomo-
cy sekcji krytycznych lub operacji CAS (więcej o operacjach CAS
w ramce obok). Ale nie chcemy, żeby transakcja mogła odczyty-
������� wać dane, które są zapisane przez inną transakcję, która jeszcze
nie skończyła (nie było commit). Bazy danych stosują w tym celu
kolejne mechanizmy loków (ang. locks). Takie loki są zakładane
najczęściej na całe strony. Jeżeli jeden wątek odczyta jakąś stro-
� �� nę, to np. żaden inny wątek nie będzie mógł tej strony zmieniać.
Jeżeli transakcja obejmuje wiele stron, to może z łatwością dojść
������������������������������������������������� do zakleszczenia (ang. deadlock), czyli sytuacji, kiedy np. wątek
������������������������������������������������������������������ A czeka aż wątek B zwolni stronę 1, a wątek B czeka aż wątek
����� A zwolni stronę 2. W takich bazach danych muszą być wprowa-
��� �� dzone specjalne mechanizmy wykrywające zakleszczenia, któ-
�� �� re reagują najczęściej zabijając przynajmniej jeden wątek (pro-
gramiści często o tym zapominają, a jest to źródło wielu weso-
������ łych nieporozumień z klientami). Takie czekanie na zasoby bar-
dzo spowalnia działanie aplikacji wielowątkowych (czyli np. ser-
werów WWW). Pojawiły się ostatnio rozwiązania oparte na algo-
������� rytmach nieużywających loków (ang. lockless lub lockfree). Naj-
częściej oparte są one na operacjach CAS i strukturach danych
z cieniem (ang. shadow data).
LhimkDB organizuje dane w pliku podobnie jak to się dzie-
�� �� je w strukturach danych z cieniem. Każda dana ma swój PID,
który jest adresem w pliku, który trzyma wszystkie PID (plik ten
Rysunek 3. Sposób obsługi wielodostępu do danych. Transakcja w LhimkDB nazywany jest indeksem, nie mylić z indeksem
A zaczęła się wcześniej i w momencie jej startu ostatnią w sensie bazy danych). Przy każdym PID jest pamiętany adres
zakończoną transakcją zapisu była transakcja
danych w pliku z danymi (w LhimkDB nazywany plikiem z rekor-
o numerze 100. Transakcja B zaczęła się później, poprosiła
o prawa do zapisu i dostała numer 101. Transakcja B zmienia dami, znowu, nie mylić z rekordami w relacyjnej bazie danych).
obiekt o PID 1, i wprowadza jego nową wartość pod aresem 30. PID jest zatem odpowiednikiem adresu dla obiektu. Obiekt mo-
Jednak wciąż jest pamiętana stara wartość, i kiedy transakcja A żemy dowolnie przemieszczać w pliku rekordów, wystarczy wte-
chce odczytać wartość obiektu PID(1), to dostaje stary adres 15. dy wprowadzić jego nowy adres przy PIDw pliku indeksu.

24 www.sdjournal.org Software Developer’s Journal 11/2005


Zbudujmy sobie bazę danych – LhimkDB

Listing 1. Fragment implementacji Skip List z artykułu Thomassa Niemanna “A compact guide to searching and sorting”
(http://epaperpress.com/sortsearch/index.html)
statusEnum findLeft(SkipList *This, keyType key, x->level = newLevel;
nodeType** node){ x->prev = This->update[0];
int i; if (x->prev == This->hdr){
nodeType *x = This->hdr; x->prev = NIL;
*node = NIL; }
for (i = This->listLevel; i >= 0; i--){ printf("update prev, x->prev %d:%d \n",
while (x->forward[i] != NIL x->prev->key, x->prev->rec.stuff);
&& compLT(x->forward[i]->key, key)){ for (i = 0; i <= newLevel; i++){
x = x->forward[i]; x->forward[i] = This->update[i]->forward[i];
} This->update[i]->forward[i] = x;
This->update[i] = x; }
} if (x->forward[0] != NIL){
*node = x; printf("update forward[0] prev, x->forward[0] %d:%d \n",
return STATUS_OK; x->forward[0]->key, x->forward[0]->rec.stuff);
} x->forward[0]->prev = x;
statusEnum insert(SkipList *This, keyType key, }
recType *rec){ return STATUS_OK;
int i, newLevel; }
nodeType *x; statusEnum delete(SkipList *This, keyType key){
findLeft(This,key, &x); int i;
x = x->forward[0]; nodeType *x;
if (x != NIL && x != This->hdr && compEQ(x->key, key)) findLeft(This,key, &x);
return STATUS_DUPLICATE_KEY; x = x->forward[0];
for ( if (x == NIL || !compEQ(x->key, key))
newLevel = 0; return STATUS_KEY_NOT_FOUND;
rand() < RAND_MAX/2 && newLevel < MAXLEVEL; for (i = 0; i <= This->listLevel; i++){
newLevel++); if (This->update[i]->forward[i] != x) break;
printf("new level: %d \n", newLevel); This->update[i]->forward[i] = x->forward[i];
if (newLevel > This->listLevel){ }
This->listLevel = newLevel; free (x);
} while ((This->listLevel > 0)
if ((x = malloc(sizeof(nodeType) && (This->hdr->forward[This->listLevel] == NIL))
+ newLevel*sizeof(nodeType *))) == 0) This->listLevel--;
return STATUS_MEM_EXHAUSTED; return STATUS_OK;
x->key = key; }
x->rec = *rec;

W LhimkDB pamiętamy jednak nie jeden adres, a dwa. ły commitować swoje zmiany po kolei. Trwa to zazwyczaj bar-
Dla każdego z dwóch adresów pamiętamy też numer transak- dzo krótko (na moim notebooku około 1/300 s) i nie stanowi
cji, która wprowadzała te dane (patrz Rysunek 2.). Wyobraź- zasadniczego ograniczenia.
my sobie, że mamy dwie transakcje A i B. Transakcja A zaczę- Drugi problem jest taki, że jeżeli od startu transakcji A by-
ła się wcześniej i w momencie jej startu ostatnią zakończoną ły dwie transakcje B1 i B2, które zmieniały obiekt PID(1), to już
transakcją zapisu była transakcja o numerze 100. Transakcja nie pamiętamy wersji o numerze 100, którą chciałaby odczytać
B zaczęła się później, poprosiła o prawa do zapisu i dosta- transakcja A. W takim wypadku występuje konflikt i transakcja
ła numer 101. Transakcja B zmienia obiekt o PID(1), i wpro- A jest unieważniana. Moje “badania” empiryczne pokazały, że nie
wadza jego nową wartość pod adresem 30 (adres w pliku re- jest to przypadek częsty. Przy dziesięciu równolegle zapisujących
kordów). Jednak wciąż jest pamiętana stara wartość, i kiedy wątkach, walczących o 3 te same obiekty, występuje to zjawisko
transakcja A chce odczytać wartość obiektu PID(1), to dostaje w około 1% transakcji. Gdyby był to jednak znaczący problem,
stary adres 15 (patrz Rysunek 3.). W ten sposób zapisy trans- zawsze można zapamiętywać więcej historycznych wartości.
akcji B nie będą w żaden sposób interferować z transakcją A. Wiemy już jak obiekty są zapisywane, i jak są im przypo-
Pojawia się ograniczenie, że w jednym czasie może być rządkowywane PID. Zastanówmy się nad zarządzaniem pamię-
tylko jedna transakcja z prawami zapisu. Nie jest to dla nas cią. Obiekty są tworzone, kasowane, powstają nowe wersje itp.
problem, ponieważ mówimy tu o transakcjach niskopozio- W związku z tym muszą być mechanizmy, które zarządzają alo-
mowych, aplikacja będzie się posługiwać zupełnie innymi kacją i zwalnianiem pamięci. Jest to dosyć ciekawa dziedzina,
transakcjami, które będą mogły dowolnie równolegle dzia- w przypadku alokacji pamięci, która będzie zapisywana na dysk
łać. Oznacza to tylko tyle, że transakcje aplikacji będą musia- i odtwarzana z dysku, wcale nie ma tak dużo gotowych wzorco-

Software Developer’s Journal 11/2005 www.sdjournal.org 25


Bazy
Danych

Najprostszym rozwiązaniem byłoby trzymanie rekordów


Listing 2. Transakcje mogą zostać odwołane, dlatego w postaci [klucz, dane, PID następnego rekordu] (taki rodzaj li-
muszą być wykonywane w pętli
sty, gdzie zamiast adresu jest PID). Wtedy znalezienie rekordu
DBSystem *dbs = DBSystem::new(); o podanym kluczu polegałoby na przejrzeniu takiej listy, aż od-
DBTransaction *dbt = dbs->startTransaction(); najdziemy żądany klucz. Możemy te rekordy trzymać w postaci
while(1) { posortowanej i wtedy, kiedy znajdziemy rekord, o większym klu-
try() { czu, wiemy, że nie ma co dalej szukać, naszego klucza już nie
//kod transakcji znajdziemy (nazwijmy taką posortowaną listę L0). Wyobraźmy
dbt->commit(); sobie, że mamy 100 rekordów na takiej liście. Bierzemy co dru-
break; gi rekord i tworzymy kolejną listę (nazwijmy ją L1). Teraz może-
} catch(e) { my najpierw odszukać największy klucz niewiększy od naszego
if (e == TRANSACTION_ABORTED) na liście L1, a potem sprawdzić na L0, czy mamy trafienie. W ten
{ sposób szukamy 2 razy szybciej. Możemy też wziąć co czwarty
dbt->abort(); element i zrobić listę L2. Teraz będziemy szukać 4 razy szybciej.
} Możemy wziąć co ósmy, co szesnasty, co 32. element itd. W ten
throw(e); sposób utworzymy drzewo binarne, w którym szybkość odnale-
} zienia elementu jest zależna od logarytmu ilości obiektów. Dla
} statycznych danych nie ma problemu, żeby takie idealne drzewo
utworzyć. Gorzej jest kiedy dane do drzewa są dodawane, usu-
wane i nie wiemy w jakiej kolejności i co nadejdzie dalej.
wych rozwiązań. Najciekawszy jest Vmalloc z AT&T autorstwa Genialny pomysł Skip List polega na tym, że przy każdym
Kiem-Phong Vo (http://www.research.att.com/sw/tools/vmalloc). wkładanym elemencie losujemy do jakich list go zapisać. Z praw-
Jest to spora biblioteka z wieloma możliwościami. Dla mnie tro- dopodobieństwem 1 do listy L0. Z prawdopodobieństwem 1/2 do
chę za ciężka i nie do końca rozumiem licencję (chyba jest w du- listy L1, 1/4 do L2 itd. W ten sposób zawsze na L0 będą wszyst-
chu BSD, ale jest tak długa, że nie mam czasu jej analizować). kie elementy, na L1 mniej więcej połowa, na L2 jedna czwarta
Jest też trochę dokumentów w internecie włącznie z próbą opa- itd. Statystycznie będzie to dokładnie to samo jakbyśmy ręcznie
tentowania banalnego algorytmu przez ... jakżeby inaczej IBM. robili listy ze statycznego zbioru danych. Co więcej, algorytmy są
W LhimkDB jest obecnie takie rozwiązanie: Pamięć jest przy- banalne, można je zamieścić w tym artykule (Listing 1.). Dla po-
dzielana wielkościami będącymi potęgami 2. W momencie, kie- równania, usuwanie elementu z B+-tree jest tematem całych roz-
dy jest zwalniany kawałek pamięci, jego adres jest dodawany do praw (np. Jan Jannink Implementing deletion in B+-Trees).
tablicy o indeksie log2(wielkość) (czyli mamy tablicę 32 elemen- Implementacja Skip List w SkipDB jest praktycznie taka
tową, np. pamięć o wielkości 64 jest dodawana pod adresem 6, sama. Uwzględnia tylko wykorzystanie PID zamiast wskaźni-
pamięć 128 pod adresem 7 itp.). Kiedy chcemy zaalokować ka- ków i obsługę transakcji. SkipDB zapamiętuje odczytane raz
wałek o wielkości 128, sprawdzamy pod adresem 7, czy mamy obiekty (tworzy cache), więc odwoływanie się kilka razy do
jakieś wolne kawałki pamięci. Jeżeli tak, to bierzemy pierwszy. tych samych danych jest bardzo szybkie.
Jeżeli nie, to sprawdzamy, czy mamy jakieś kawałki o wielkości
256, 512 ... (czyli indeksy 8, 9 ...). Jeżeli nic nie znajdziemy, to Transakcje wysokiego poziomu
alokujemy pamięć na końcu pliku. Nie robię dwóch rzeczy: nie łą- UDB i SkipDB, korzystający z UDB, mają pojęcie transakcji
czę kawałków zwolnionej pamięci obok siebie i nie alokuję części na poziomie operacji na dysku. Z opisu UDB wiemy, że w jed-
pamięci, zawsze alokuję całość. W tej chwili ten alokator spraw- nym czasie może być tylko jedna zapisująca transakcja dys-
dza się bardzo dobrze. Oczywiście można go dowolnie wymie- kowa. Programista tworzący aplikacje wielowątkowe przy uży-
niać i dostosowywać do potrzeb swoich aplikacji. ciu LhimkDB powinien korzystać z transakcji wyższego pozio-
W LhimkDB wszystkie te mechanizmy tworzą system nazwa- mu. Transakcja taka ma klon SkipDB, do którego będzie zapi-
ny UDB. Dalej pisząc o UDB będę miał na myśli zapis danych
z możliwością alokacji pamięci i przyznania unikalnego PID.
W Sieci
Pamięć asocjacyjna – SkipDB • Strona domowa projektu Lhimk i LhimkDB
Na poziomie UDB dane można odczytywać w momencie kie- http://www.lhimk.org/
dy znamy ich PID. Jeżeli chcemy odszukiwać dane poda- • Berkeley DB
jąc klucz musimy mieć jakieś mechanizmy wyższego pozio- http://www.sleepycat.com/
mu. W bazach danych stosowane są zmodyfikowane algo- • Baza danych PBL na licencji LGPL
rytmy znane ze zwykłych struktur danych: tablice asocjacyj- http://mission.base.com/peter/source/
ne (ang. hash tables), drzewa binarne (B-Tree) itp. Najczęściej • Baza danych MetaKit
http://www.equi4.com/metakit.html
w bazach danych stosowana jest jakaś wersja B+-tree/B*-
• SkipDB autorstwa Steva Dekorte
tree. Więcej na ten temat można przeczytać w artykule: Tho-
http://www.dekorte.com/
massa Niemanna A compact guide to searching and sorting • Vmalloc z AT&T autorstwa Kiem-Phong Vo
(http://epaperpress.com/sortsearch/index.html). http://www.research.att.com/sw/tools/vmalloc
LhimkDB stosuje algorytm Skip List, wymyślony w 1989 r. • Strona domowa Billa Pugha
przez Billa Pugha (http://www.cs.umd.edu/~pugh/). Jest to tak http://www.cs.umd.edu/~pugh/
prosty algorytm, że mogę go wyjaśnić tutaj w dwóch akapitach.

26 www.sdjournal.org Software Developer’s Journal 11/2005


Zbudujmy sobie bazę danych – LhimkDB

sywać dane (baza docelowa) i własne SkipDB używane wy- Dlaczego takie środowisko jest ważne? Nie wystarczy
łącznie jako cache (baza podręczna). Transakcje współdzielą nam PHP+MySQL?
między sobą wyłącznie obiekt pliku (część UDB), więc wspól- Każdy, kto budował większe aplikacje przy użyciu serwera
ny dostęp musi być kontrolowany wyłącznie do danych pliku. SQL, wie, że w zasadzie serwer SQL architektonicznie się do te-
Transakcja LhimkDB działa tak: kiedy czyta rekord, naj- go nie nadaje. Można tego “naukowo” dowieźć. Weźmy dowolną
pierw czyta z bazy podręcznej. Jeżeli tam go nie ma, to spraw- operację na danych w takiej aplikacji (np. dekretację dokumen-
dza w bazie docelowej. Jeżeli chce zmienić rekord, to wprowa- tu). Jeżeli jest ona wykonywana raz, to może być zrealizowa-
dza te zmiany tylko do bazy podręcznej. Dopiero po sprawdze- na przy pomocy sekwencji: odczytaj z SQL, przetwórz w aplika-
niu wszelkich ewentualnych konfliktów, kiedy jesteśmy pewni, że cji, zapisz do SQL. Ale jeśli będziemy ją chcieli wywołać np. dla
transakcja zakończyła się sukcesem, następuje przepisywanie 1000 elementów, to okaże się, że przetwórz w aplikacji musimy
zmian z bazy podręcznej do docelowej. Czyli w jednym czasie, zapisać w postaci procedury SQL. W ten sposób cały nasz kod
tylko jedna transakcja może wykonywać commit. Transakcja jest z czasem wyemigruje do SQL, a w “aplikacji” zostanie tylko po-
zatem materializowana w postaci zapisów do bazy podręcznej. kazywanie danych. Jest to szczególnie drastycznie widoczne
Jeżeli będziemy tę bazę zapisywać (domyślnie jest tylko w pa- przy aplikacjach webowych, gdzie nic się klientowi nie pokazu-
mięci), to mamy transakcje długie (trwałe), które możemy nawet je, tylko wysyła HTML. Stąd pomysły, żeby np. do serwera SQL
nazywać. Jeżeli będziemy z kolei tworzyć hierarchię takich baz dobudować serwer HTTP. Te pomysły zostały zarzucone, po-
podręcznych, uzyskamy transakcje zagnieżdżone. nieważ architektury serwerów SQL zakładają stałe połączenie
Trzeba pamiętać, że transakcja może zostać unieważnio- z klientem, a HTTP jest protokołem bezstanowym. Rozwiąza-
na. Dlatego, jeżeli chcemy mieć gwarancję, że transakcja się niem może być zatem wbudowanie bazy danych do serwera
wykona, trzeba ją wykonywać w pętli (Listing 2). aplikacyjnego i serwera HTTP. I takie rozwiązanie jest w Lhimku.

Możliwe rozszerzenia Podsumowanie


Najważniejszym rozszerzeniem, jakie jest w LhimkDB, jest to, LhimkDB jest pisany z myślą o nowoczesnych wielowątko-
że to nie jest baza klucz-dane, a tak naprawdę baza drzewiasta. wych aplikacjach uruchamianych na nowoczesnym sprzęcie
Oznacza to, że każdy element bazy może mieć podelementy ad- i systemach operacyjnych. Jest to nowe rozwiązanie, na pew-
resowane kluczami. Jest to bardzo podobne do bazy standar- no nie osiągnęło jeszcze 100 % zaufania, ale kod jest prosty
du MUMPS, które są powszechnie stosowane w największych i jest go niewiele. Wszystkie podstawowe rzeczy można by-
i najbardziej skomplikowanych aplikacjach (np. w bankach, apli- ło wytłumaczyć w jednym artykule. Sama implementacja jest
kacjach medycznych itp.). Na bazie tej hierarchii jest też budo- dość obiecująca. Na pewno jest bardzo dużo miejsca na opty-
wany system bezpieczeństwa, który przypomina system bez- malizację i poprawę, ale i bez tego dziś LhimkDB działa szyb-
pieczeństwa systemu plików. W każdym węźle jest określone kto ko i stabilnie.
może wykonywać i jakie operacje na elementach tego węzła. Je- W tej części pokazałem ogólną architekturę LhimkDB.
śli dodamy, że Lhimk ma implementację serwera WWW, to ma- W następnych częściach zajmiemy się szczegółowo budowa-
my kompletne środowisko do tworzenia aplikacji webowych. niem opisanych tu warstw bazy danych. n
Bazy

Danych

Zbudujmy sobie bazę danych – LhimkDB


Paweł Marciniak
cz 2 Budujemy warstwę dostępu do danych

M
iesiąc temu rozpocząłem cykl artykułów
opisujący jak zbudować od podstaw bazę ������� ������������������

danych. Budujemy bazę danych do wbudo- ����������


��������� �����������
wywania do programów (ang. embedded database),
o architekturze klucz-wartość (ang. key-value). W po- �������
��������������

przednim artykule podałem motywację, dlaczego ta- �������


���������
�������

ką bazę warto zbudować i pokazałem cały projekt,


jego założenia i elementy. Dla przypomnienia na ry- ���

sunku 1. pokazany jest schemat całej bazy LhimkDB. �������


��������������
��������� ����������

W tym artykule zajmiemy się najniższą warstwą, �����


czyli warstwą dostępu do danych – UDB (ang. Unor-
dered Database). UDB jest oczywiście częścią Rysunek 1. Architektura LhimkDB
LhimkDB, ale zarówno jego zadania, jak i sposób im-
plementacji są na tyle niezależne, że mogą zaintere- l remove(pid _ t pid) – usunięcie danych zapisa-
sować kogoś, kto chce mieć po prostu trwałą pamięć nych pod podanym pid (pid może być potem po-
dla swoich programów. Innymi słowy UDB może być nownie użyte, do innych danych)
używane jak niezależna biblioteka. l beginTransaction() -- rozpoczęcie transakcji zapisu
LhimkDB jest pisany w języku Lhimk. Język ten l commitTransaction() -- zakończenie transak-
jest na tyle podobny do C/C++, że nie ma sensu cji wraz z zapisaniem danych. Nie potrzebujemy
się go uczyć, czy wyjaśniać jak w nim pisać progra- rollback. Jeżeli nie chcemy zakończyć transak-
my. Te elementy, które występują w Lhimku, a nie cji, po prostu nie zrobimy commitTransaction().
ma ich w C/C++ są omówione w ramce “Lhimk” lub
w samym tekście artykułu. Jeżeli ktoś nie chce uży- To jest w zasadzie całe API. Typ char[[]] to równo-
wać Lhimka (co całkowicie rozumiem), to może z ła- ważnik (char* buf, uint len), czyli dynamicznej ta-
twością pokazany kod przepisać w C++. Można też blicy z zapisaną długością. Jest to jeden z tych nielicz-
z Lhimka korzystać z wewnątrz programów napisa- nych elementów, które są w Lhimku, a nie ma ich w C/
nych w C/C++. C++ (notacja char fun()[[]], oznacza, że fun zwraca
char[[]], choć dziwnie to wygląda, jest to notacja z C).
Założenia Jeśli chodzi o obsługę transakcji, to chcemy mieć moż-
Po pierwsze ustalmy, czego od UDB oczekujemy. liwość jednego wątku zapisującego i wielu wątków czy-
UDB można przyrównać do obsługi pamięci na ster- tających. Wątki czytające powinny widzieć tylko te da-
cie (ang. heap memory). Tyle, że będzie to pamięć ne, które zostały wcześniej wprowadzone i zatwierdzo-
trwała (ang. persistent) i z obsługą transakcji. Naj- ne przez commitTransaction() (dokładnie mówiąc, wą-
lepiej zdecydować, jaki będzie interfejs, czyli jakie tek czytając dwa razy dane pod tym samym pid po-
funkcje UDB będzie udostępniać. Potrzebujemy ta- winien dostać dwa razy te same dane, czyli interesu-
kich metod: ją nas tylko te zmiany, które zostały dokonane i za-
twierdzone zanim nasz wątek zaczął czytać). Oczywi-
l pid _ t put(char d[[]]) – umieszczenie danych w pa- ście w przypadku awarii chcemy mieć gwarancję, że po
mięci trwałej, zwraca pid, przy pomocy którego ponownym podłączeniu bazy danych, będziemy czy-
będziemy mogli się później do tych danych od- tać tylko wartości wprowadzone, przez poprawnie za-
wołać kończone transakcje (innymi słowy, jeśli awaria wystąpi
l putAt(pid _ t pid, char d[[]]) – umieszczenie w trakcie transakcji zapisu, to chcemy zapisy tej trans-
danych w pamięci pod konkretnym pid (zmiana akcji anulować).
starych danych) Dodatkowo, nie chcemy mieć żadnych ograni-
l char at(pid _ t pid)[[]] – odczytanie danych czeń, ani na wielkość zapisywanych danych (do wiel-
z podanego pid (tego, który zwraca put) kości całej dostępnej pamięci), ani na wielkość całej
bazy danych (do maksymalnej wielkości plików), ani
Paweł od 20 lat zajmuje się tworzeniem oprogramowa-
na wielkość transakcji.
nia, głównie w C/C++. Od trzech lat rozwija system dy-
namicznej kompilacji Lhimk. Obsługa plików
Kontakt z autorem: pawel@software.com.pl U podstawy każdej bazy danych leży obsługa syste-
mu plików. To właśnie zapis do pliku daje nam trwa-

42 www.sdjournal.org Software Developer’s Journal 12/2005


Zbudujmy sobie bazę danych – LhimkDB

�����
wać w specjalnych typach, które zajmują kilka stron). Ge-
�� �� neralnie, im większy jest rozmiar strony tym baza danych
� �� działa szybciej i można zapisywać większe dane, ale ma-
my mniejszą współbieżność.
������ Ale to jest prawda w tradycyjnych bazach danych, u nas
strony są podobne tylko z nazwy. W MFile strona nie stano-
wi jednostki blokowania dostępu, ani ograniczenia w wiel-
������� kości danych. Wielkość stron jest też dużo większa (wydaje
mi się, że optymalna wielkość strony to 4MB) i jest ich dużo
mniej. Zasada działania jest taka, że jeżeli chcemy coś zro-
bić pod adresem np. 12345, to prosimy klasę PageSystem,
� �� aby dała nam stronę dla tego adresu. Wszystkie strony
trzymane są w tablicy, a ich indeks to adres/rozmiar _ stro-
Rysunek 2. Sposób zapisu danych w UDB
ny. Jeżeli strona już jest utworzona, to jest po prostu zwra-
łość (ang. persistance). W LhimkDB pliki są obsługiwane cana. Jeżeli nie, to jest tworzona, mapowana na pamięć
przez klasę MFile – Memory Mapped File. i zwracana. Jeżeli przekroczyliśmy limit ilości stron, to
Idea jest taka: ostatnio najmniej używana strona (ang. Least Recently
Used) jest zwalniana ( munmap).
• Otwieramy plik (lub tworzymy nowy) Jeżeli rozmiar danych przekracza wielkość strony to za-
• Mapujemy plik na pamięć -- wywołanie mmap (wszystkie pis i odczyt są wykonywane w pętli aż cały bufor zostanie za-
funkcje systemowe podaję dla Linuksa) pisany lub odczytany (Listing 2). W rezultacie możemy skon-
• Odczyty i zapisy wykonujemy przy pomocy memcpy figurować MFile, żeby miał jedną małą stronę (np. 4096) i ob-
• Wszystkie operacje zapisu i odczytu są chronione przez
mutex
�����������������������������������������������������
• Tylko jeden wątek może mieć w jednej chwili prawa do zapisu
�����

Najprostsza wersja klasy MFile jest przedstawiona na Listin- �� ��


� ��
gu 1. Jest to w pełni wielowątkowa wersja klasy MFile, która
spełnia wszystkie nasze założenia, poza obsługą dużych pli- ������
ków (teoretycznie do 4GB, w praktyce do 1GB). Kod tej klasy
jest bardzo prosty i czytelny. Jedyne trudności rodzi wielowąt-
kowy dostęp, dlatego trzeba utrzymywać minimalną funkcjo- �������
nalność takich klas.
Obiekt MFile będzie praktycznie jedynym obiektem
współdzielonym przez różne wątki UDB (i dalej LhimkDB).
To znaczy, że różne wątki będą miały wskaźnik do tego sa- � ��
mego obiektu MFile. Kolejne klasy i obiekty, które będzie-
my dalej omawiać, nie będą już współdzielone pomiędzy
�������������������������������������������������
wątkami -- wątki będą je tworzyć samodzielnie. Jeżeli no- ������������������������������������������������������������������
wy wątek chce używać obiektu MFile, musi utworzyć jego
�����
klona: ��� ��
�� ��
MFile *mf = mf_base->clone();
������
Zwiększane jest wtedy pole usage. Plik jest zamykany kiedy
usage osiąga 0.
Zobaczmy teraz jak usunąć barierę wielkości plików. �������
Robimy to bardzo prosto poprzez system stron (ang. pa-
ges). Strony są powszechnie wykorzystywane w syste-
mach baz danych, ponieważ są one pochodną sposobu,
w jaki system operacyjny obsługuje zapis i odczyt plików. �� ��
System operacyjny robi to właśnie przy pomocy stron,
za jednym razem może odczytać lub zapisać jedną stro- Rysunek 3. Sposób obsługi wielodostępu do danych.
nę danych (w moim Linuksie jest to 4096 bajtów, można Transakcja A zaczęła się wcześniej i w momencie jej startu
to sprawdzić funkcją getpagesize()). Strony w bazach da- ostatnią zakończoną transakcją zapisu była transakcja o
numerze 100. Transakcja B zaczęła się później, poprosiła o
nych mają zawsze rozmiar będący niewielką wielokrotno-
prawa do zapisu i dostała numer 101. Transakcja B zmienia
ścią strony systemowej (np. w PostgreSQL-u jest to do- obiekt o PID 1, i wprowadza jego nową wartość pod aresem
myślnie 8192 bajtów). Strony w bazach danych są jednost- 30. Jednak wciąż jest pamiętana stara wartość, i kiedy
ką blokowania dostępu i często stanowią ograniczenie dla transakcja A chce odczytać wartość obiektu PID(1), to
wielkości jednego rekordu (większe dane trzeba zapisy- dostaje stary adres 15.

Software Developer’s Journal 12/2005 www.sdjournal.org 43


Bazy
Danych

sługiwać zapis i odczyt 100MB z 10GB plików. Tyle, że bę- UDBIndex zapamiętuje na początku swojego pliku na-
dzie to długo trwało. Rozmiary i ilość stron są limitowane tak główek, w którym zapisuje numer ostatnio zakomitowa-
naprawdę tym, ile możemy zająć z przestrzeni adresowej (nie nej transakcji (last _ commited) i numer aktualnie działają-
z pamięci!). Jeżeli np. jeden obiekt MFile zabierze 100MB cej transakcji (current). W momencie startu transakcji zapi-
z przestrzeni adresowej, to będziemy mogli takich plików su (begin()) jest zwiększany o jeden numer aktualnej trans-
otworzyć za jednym razem około 10-ciu. Jeżeli nie planuje- akcji i nagłówek jest zapisywany (jest wykonywana trans-
my otwierania dużej ilości plików, nasza aplikacja jest np. de- akcja pliku MFile). Jeżeli przy otwieraniu pliku okaże się,
dykowanym serwerem, to warto zająć nawet 1GB przestrze-
ni adresowej. Oczywiście problem znika (przynajmniej teo-
retycznie) na maszynach 64-bitowych. Na takich maszynach Lhimk
cały ten system stronicowania ma niewielki sens, lepiej pod-
Lhimk jest środowiskiem dynamicznej kompilacji dla języka
piąć cały plik i niech dalej się martwi system operacyjny.
o tej samej nazwie (Lhimk), który jest oparty na C. Lhimk jest ję-
Mamy zatem metodę trwałego zapisu dowolnie dużych
zykiem obiektowym, o bardzo prostej składni, w zasadzie, każ-
danych w dowolnie dużych plikach. W MFile nie ma poję- dy kto zna C/C++ może od razu zacząć programować w Lhim-
cia transakcji, commit oznacza tylko tyle, że będzie zsynchro- ku. Do składni C są dodane klasy z wielodziedziczeniem (ang.
nizowana zawartość pamięci i pliku, czyli wszystko zostanie multiinheritance) i możliwością przeciążania operatorów, szablo-
na pewno na dysk zapisane. Musimy zatem dodać funkcjonal- ny, sygnały, wyjątki i kilka innych drobiazgów. Lhimk jest środo-
ność, która obsłuży transakcje. wiskiem dynamicznej kompilacji, więc cały kod jest kompilowa-
ny w momencie uruchamiania programu. Kompilator jest bardzo
UDBIndex szybki, więc trwa to krócej niż ładowanie wielu bibliotek w skom-
Przechodzimy do właściwej klasy UDB. Tak naprawdę to skła- pilowanych programach.
da się ona z 4 klas: UDBIndex, UDBRecord, UDBRecords Lhimk jest dostępny częściowo na licencji LGPL i częścio-
wo na licencji BSD. Wynika to z tego, że korzysta z kodu TinyCC,
i UDB. Koncepcja jest taka, że będziemy wykorzystywać dwa
który jest na licencji LGPL, natomiast moją intencją jest dawać
pliki -- Index i Records. Zajmiemy się teraz klasą UDBIndex,
wszystko na licencji BSD.
która implementuje Index. Podstawową jednostką kodu w Lhimku jest moduł. Każdy mo-
Index jest czymś w rodzaju tablicy wskaźników. Jeże- duł ma unikalny adres, np.:
li chcemy odczytać dane (rekord) zapisane pod pid=10, to
sięgamy do 10-tej komórki w pliku Index (mówiąc ściśle, to \lhimk.org\Db\UDB

dla dziesiątego obiektu pid=9, gdyż numerujemy od 0), od-


W Lhimku nie ma podziału na pliki nagłówkowe i implementa-
czytujemy pozycję danych w pliku Records i potem odczytu-
cję. Jak chcemy się odwołać do jakiegoś typu lub obiektu z in-
jemy same dane z pliku Records (najpierw nagłówek, w któ- nego modułu, po prostu podajemy całą ścieżkę. To jest np. od-
rym jest zapisana długość tych danych, a potem same da- wołanie do klasy UDB (odwołania do typów poprzedzone są zna-
ne). W ten sposób możemy dowolnie przemieszczać dane kiem '@'):
w pliku Records, bez zmiany pid. Pid jest odpowiednikiem
@\lhimk.org\Db\UDB\UDB
wskaźnika, to właśnie przy pomocy pid będziemy się do
konkretnych danych dalej odwoływać. Na końcu jest dwa razy UDB, ponieważ klasa UDB jest w module
Jest tutaj jedna drobna komplikacja. Otóż, w pliku In- UDB. Obiekty globalne (czyli wbudowane funkcje) są po prostu po-
dex zapamiętujemy dwie pozycje, a nie jedną. Każda przedzane przez '\', np.:
z nich ma przypisany numer transakcji, która dane pod tą
pozycją zapisała. Transakcja, kiedy chce odczytać dane, \printf(“Hello!\n”);

podaje również numer ostatniej zatwierdzonej transakcji


Klasa może mieć metody wirtualne, albo statyczne. W Lhimku ra-
(ang. commited), którą akceptuje (ten który był w momen- czej nie używa się referencji do obiektów, a wyłącznie wskaźniki
cie rozpoczęcia odczytu). W odpowiedzi podawana jest (Lhimk ma wbudowany garbage collector). Nowy obiekt najczęściej
nowsza pozycja, ale starsza od naszej aktualnej transak- tworzony jest tak:
cji. Mówiąc innymi słowy, pamiętamy zawsze jedną histo-
@\lhimk.org\Db\UDB\UDB *udb = \lhimk.org\Db\UDB\UDB::new();
ryczną wartość. Jeżeli wątek nie chce wartości najnow-
szej (bo mógł wcześniej przeczytać starszą wartość), to Gdzie metoda new jest zwykłą metodą statyczną zwracającą
dajemy mu wartość starszą. Jest to pokazane na Rysun- UDB*.
ku 2. i Rysunku 3. Szablony są realizowane na poziomie modułu, który jest jed-
Oczywiście może się zdarzyć tak, że od czasu wystar- nostką kompilacji. Jeżeli chcemy mieć np. listę integerów, napi-
towania naszej transakcji czytającej, dwie różne transakcje szemy to tak:
zmieniły dane pod odczytywanym pid. Wtedy nie ma już hi-
@\lhimk.org\LTL\List\List[int T] *udb = @\lhimk.org\LTL\List\
storycznej wartości, która byłaby dla nas dobra. W takim wy-
List[int T]::new();
padku jest wyrzucany wyjątek i transakcja zawodzi. (Jeże-
li jedna transakcja będzie zmieniać kilka razy te same dane, to W ten sposób moduł \lhimk.org\LTL\List\ zostanie przekompilowa-
nic złego się nie stanie, bo będzie zawsze modyfikować naj- ny z typem T zdefiniowanym jako int. Jest to podejście dużo prostsze
nowszą pozycję w naszej parze). niż w C++, a czasami daje dużo lepsze możliwości (można np. w za-
UDBIndex przyznaje również pid nowym rekordom. W tym leżności od typu T dodawać do klas poszczególne metody).
celu prowadzi listę nieużywanych pid, jak i licznik zapamiętu- To chyba cały podręcznik Lhimka...
jący największy użyty pid.

44 www.sdjournal.org Software Developer’s Journal 12/2005


Zbudujmy sobie bazę danych – LhimkDB

Listing 1. Najprostsza implementacja klasy MFile


class MFile { this->_p->maxPos = noff;
MFilePrivate* _p; this->_p->buf = this->_p->file->mmap(
int init(char* path, char* ext) { pthis->buf, pthis->maxPos, "rw", "s", 0);
this->_p = MFilePrivate::new(); return 0;
//... inicjalizacja pól wycięta ... };
this->open(); int truncate(unsigned long long off) {
return 0; LOCK;
}; this->_truncate_(off);
constructor new(char* path, char* ext) { UNLOCK;
MFile* this = \lhc\new(MFile*); };
this->init(path, ext); int write(unsigned long long pos, void *buf,
return this; unsigned int size) {
}; if (!this->canWrite()) {
void free() { ERROR("MFile::write, thread can not write",
if (this->canWrite()) { ERROR_MINOR);
this->finishWrite(); }
} LOCK;
//Musimy zablokować cały blok if (pos+size >= this->_p->maxPos) {
//ponieważ inne wątki mogą zmieniać usage //truncate without locking
//w tym samym czasie this->_truncate_(pos+size);
LOCK_USAGE; }
if (this->_p->usage<0) { \memmove(this->_p->buf + pos, buf, size);
ERROR("USAGE<0", ERROR_FATAL); this->_p->needsSync = 1;
} UNLOCK;
if (this->_p->usage > 0) { return 0;
this->_p->usage--; };
UNLOCK_USAGE; int read(unsigned long long pos, void *buf,
} else { unsigned int size) {
this->close(); if (pos + size > this->_p->maxPos) {
UNLOCK_USAGE; ERROR("Read out of range", ERROR_MINOR);
\free(this->_p); }
\free(this); LOCK;
} \memmove(buf, this->_p->buf + pos, size);
}; UNLOCK;
MFile *clone() { return 0;
LOCK_USAGE; };
this->_mfile_p->usage++; int sync() {
UNLOCK_USAGE; this->_p->file->msync(this->_p->buf, this->_p->maxPos);
return this; return 0;
}; };
int open() { int commit() {
this->_p->needsSync = 0; return this->sync();
this->_p->file->open("a+"); };
this->_p->maxPos = this->_p->file->size(); int startWrite() {
return 0; LOCK_WRITE;
}; this->_p->write_thread = \lpth\Thread::self();
int close() { };
this->_p->file->munmap(this->_p->buf, int finishWrite() {
this->_p->maxPos); this->_p->write_thread = 0x0;
this->_p->file->close(); UNLOCK_WRITE;
return 0; };
}; int canWrite() {
int _truncate_(unsigned long long off) { int ret = 0;
//Plik powiększamy o GROWSTEP if (((void*)this->_p->write_thread) != 0x0) {
unsigned long long noff = ret = this->_p->write_thread ==
(off/GROWSTEP +1) * GROWSTEP; \lpth\Thread::self();
if (this->_p->buf != 0x0 && this->_p->maxPos>0) { }
this->_p->file->munmap(this->_p->buf, return ret;
this->_p->maxPos); };
} };
this->_p->file->ftruncate(noff);

Software Developer’s Journal 12/2005 www.sdjournal.org 45


Bazy
Danych

Listing 2. Pętla zapisująca dane dowolnej wielkości, Listing 4. Przykłady użycia UDB
niezależnie od wielkości strony.
@\ludb\UDB *udb = \ludb\UDB::new("TESTUDB", 1);
while(size > 0)
{ unsigned long long pid1, pid2, pid3;
Page* p = this->_mfile_p->
page_system->getPageForPos(pos); udb->beginTransaction();
pid1 = udb->putBuf("aaa",4);
unsigned int n_pos = pos - p->off; pid2 = udb->putBuf("bbb",4);
unsigned int wsize = ( (p->size()-n_pos)> pid3 = udb->putBuf("ccc",4);
size ? size : (p->size()-n_pos)); udb->commitTransaction();

\memmove(p->buf + n_pos, buf, wsize); \printf("%llu : %s \n", pid1, udb->at(pid1)->data);


\printf("%llu : %s \n", pid2, udb->at(pid2)->data);
size -= wsize; \printf("%llu : %s \n", pid3, udb->at(pid3)->data);
pos += wsize;
buf += wsize; udb->free();
}

drugim. Znając zatem rozmiar możemy je z łatwością se-


że ostatnia transakcja została przerwana (last _ commi- kwencyjnie przeglądać (część z nich może mieć ustawio-
ted!=current), to są przeglądane wszystkie zapisy w in- ne w nagłówku, że są usunięte). Odczyt rekordu odby-
deksie i usuwane są te, które odnoszą się do przerwanej wa się w dwóch krokach. Najpierw czytany jest nagłówek,
transakcji. Jest to całe recovery po awarii bazy danych. w którym zapisany jest rozmiar danych. Potem, kiedy znamy
Jest to bardzo szybkie i umożliwia szybkie ponowne włą- już rozmiar danych, możemy przeczytać dane (Listing 3.).
czenie np. serwisu internetowego (oczywiście zależy to od Jeżeli chcemy rekord usunąć ustawiamy mu flagę w na-
wielkości pliku, ale przeglądany jest plik indeksu, a nie plik główku i zapisujemy (zajęta przez niego przestrzeń jest do-
z danymi!) . dawana do puli bloków do wykorzystania, o czym piszę da-
lej).
UDBRecord Na tym poziomie nie martwimy się już o współbieżność,
i UDBRecords bo współdzielony między wątkami jest tylko obiekt MFile, któ-
Rekord (nie mylić z rekordem w relacyjnej bazie danych) ry sam się o współbieżność martwi.
składa się z nagłówka i z danych. W nagłówku pamięta-
ny jest rozmiar rekordu, pid i jego stan, czyli, czy nie zo- UDB
stał skasowany. Rekordy są zapisywane do pliku jeden po i alokacja pamięci
UDB jest połączeniem UDBIndex i UDBRecord. W zasadzie
Listing 3. Metody odczytujące nagłówek i dane rekordu. stanowi jednolite API dostępu do obu tych klas.
UDB poza prostymi porządkowymi pracami odpowiada za
int readHeader() alokację przestrzeni w pliku (podobnie jak robi to malloc na
{ stercie). W tej chwili wykorzystywany jest taki algorytm:
this->_p->file->read(this->_p->pos, (unsigned char *)
(this->_p->header), • Alokowane są ilości pamięci będące potęgami 2 (32, 64,
sizeof(UDBRecordHeader)); 128 ... 1024 itp. bajtów)
return 1; • Wolne bloki pamięci trzymane są w tablicy kolejek. Ta-
}; blica ma rozmiar 32 (w systemach 64-bitowych mo-
że mieć rozmiar 64, jeżeli ktoś planuje alokowanie blo-
char readDatum()[[ ]] ków powyżej 4GB). Pod adresem n są trzymane bloki o
{ rozmiarze 2^n (np. pod adresem 10, są bloki o wielkości
unsigned int size = this->_p->header->size; 1024 bajtów).
• Jeżeli chcemy zaalokować pamięć dla bloku o wielkości
char d[[ ]] = \lhc\array_new(char[[]],size); x, to znajdujemy takie n, że 2^n>=x i patrzymy, czy ma-
my jakieś wolne bloki w kolejce pod adresem n (jeżeli np.
\array_resize(d, size); chcemy zaalokować 700 bajtów, to n=10, bo 1024>700
i 512<700, szukamy najpierw zatem wolnych bloków
this->_p->file->read(this->_p->pos + o wielkości 1024). Jeżeli nie ma wolnych bloków pod adre-
sizeof(UDBRecordHeader), sem n, to patrzymy pod n+1, n+2 itd. Oczywiście im dalej
d->data, size); odejdziemy od n, tym więcej “stracimy” miejsca, alokując
return d; np. 1024 bajty dla 16 bajtowego bloku, więc możemy ten
}; proces przerwać. Jeżeli znajdziemy dostępny blok, to alo-
kujemy go w całości.

46 www.sdjournal.org Software Developer’s Journal 12/2005


W sieci
l Strona domowa projektu Lhimk i LhimkDB
http://www.lhimk.org/
l Vmalloc z AT&T autorstwa Kiem-Phong Vo
http://www.research.att.com/sw/tools/vmalloc/

• Jeżeli nie znajdziemy odpowiedniego bloku, to alokujemy


miejsce na końcu pliku.

W momencie otwierania pliku są przeglądane wszystkie re-


kordy i rekordy ustawione jako usunięte są zapisywane do
tablicy kolejek jako wolna przestrzeń. Robi to osobny wątek,
więc nie opóźnia to samego otwarcia pliku i rozpoczęcia pracy
bazy danych. Poza tym nie są nigdzie w pliku zapamiętywane
informacje o alokacjach przestrzeni, więc system ten nie spo-
walnia wykonywania transakcji.

Przykłady użycia
Na Listingu 4. pokazany jest prosty kod korzystający
z UDB. Nie ma tu nic, co mogłoby kogoś zaskoczyć (mo-
że poza charakterystycznymi dla Lhimka znakami '\' przed
printf). Obiekt UDB jest tworzony, transakcja się zaczyna,
zapisujemy dane przy pomocy putBuf, odczytujemy przy
pomocy at.
W przypadku aplikacji wielowątkowych, trzeba bazowy
obiekt UDB przekazać do funkcji wątku i zamiast tworzyć no-
wy obiekt UDB klonujemy obiekt bazowy:

void thread_fun(@\ludb\UDB *base_udb)


{
@\ludb\UDB *udb = base_udb->clone();
... coś robimy ...
udb->free();
}

Trzeba też pamiętać, żeby operacje w wątku wykonywać we-


wnątrz try...catch, gdyż transakcja może zawieść nawet przy
odczycie (przy odczycie nie rozpoczynamy transakcji przez
beginTransaction).

Podsumowanie
Pokazałem jak od podstaw stworzyć warstwę dostępu do
danych – UDB. Udało się spełnić wszystkie zakładane za-
łożenia, włącznie z obsługą transakcji, wielowątkowości
oraz danych i plików dowolnej wielkości. Pokazane UDB
jest dość nietypowe, ale sprawdza się w praktyce. Jest bar-
dzo proste, przez co powinno mieć mało błędów. Z pobież-
nych testów wynika, że LhimkDB-UDB działa bardzo wydaj-
nie, nawet przy bardzo dużych plikach (rzędu 10GB). Moż-
na je też dowolnie konfigurować do potrzeb swoich aplika-
cji. Takie krytyczne parametry jak wielkość strony czy wiel-
kość, o którą zwiększamy plik (ang. grow step), mogą być
zmieniane dla istniejących plików, czego raczej nie spotyka
się w tradycyjnych bazach danych. Bardzo szybkie jest też
odtwarzanie po awarii, co jest szczególnie ważne w aplika-
cjach WWW.
Za miesiąc dodamy kolejną warstwę, która będzie odpo-
wiadać za dostęp do danych według klucza. n

Software Developer’s Journal 12/2005


Bazy

danych

Zbudujmy sobie bazę danych – LhimkDB


Paweł Marciniak
cz 3. Budujemy warstwę dostępu wg klucza

D
wa miesiące temu rozpocząłem cykl artyku-
łów opisujący jak zbudować od podstaw ba- ������� ������������������

zę danych. Budujemy bazę danych do wbu- ����������


��������� �����������
dowywania do programów (ang. embedded databa-
se), o architekturze klucz-wartość (ang. key-value). �������
��������������

W pierwszym artykule podałem motywację, dlaczego �������


���������
�������

taką bazę warto zbudować i pokazałem cały projekt, je-


go założenia i elementy. Dla przypomnienia na rysunku ���

1. pokazany jest schemat całej bazy LhimkDB. W po- �������


��������������
��������� ����������

przednim artykule pokazałem jak zbudować warstwę �����


przechowywania danych – UDB (ang. Unordered Da-
tabase). W tym artykule zajmiemy się warstwą dającą Rysunek 1. Architektura LhimkDB
dostęp do danych wg klucza i umożliwiającą przegląda- ło mi to spać po nocach. W końcu znalazłem rozwią-
nie danych w uporządkowanej kolejności. zanie, które zapamiętuje wszystkie wartości historycz-
Dostęp do danych wg klucza będzie realizowa- ne, dla wszystkich wykonywanych aktualnie transak-
ny przy pomocy struktury SkipList. Najpierw poka- cji. Rozwiązanie jest trywialne: po prostu przy każdej
żę jak zbudować ogólną klasę SkipList, a potem przy zmianie wartości obiektu tworzymy nowe PID, gdzie
pomocy pewnej sztuczki połączymy ją z UDB i uzy- zapamiętujemy wskaźnik do starej wersji. W ten spo-
skamy bazę danych. Wszystkie te elementy są oczy- sób powstaje nam lista wartości historycznych obiek-
wiście częścią LhimkDB, ale zarówno ich zadania, tów. Są one usuwane z bazy, kiedy kończy się najstar-
jak i sposób implementacji są na tyle niezależne, że sza transakcja czytająca, która mogłaby do tych da-
mogą zainteresować kogoś, kto chce mieć po prostu nych sięgnąć. W rezultacie mamy coś co producenci
trwałą pamięć lub ciekawą strukturę danych typu po- baz danych reklamują jako funkcje OLAP, czyli moż-
sortowana pamięć asocjacyjna, dla swoich progra- liwość wykonywania długich analitycznych zapytań,
mów. Innymi słowy, zarówno SkipList jak i UDB mo- bez kolizji z transakcjami zapisującymi.
gą być używane jako niezależne biblioteki. W tej chwili można korzystać z obydwu rozwią-
LhimkDB jest pisany w języku Lhimk. Język ten zań dla UDB. Przy okazji okazało się, że kod jest na-
jest na tyle podobny do C/C++, że nie ma sensu się prawdę bardzo modularny, bo wprowadzona zmiana
go uczyć, czy wyjaśniać jak w nim pisać programy. objęła tylko kilka metod UDBIndex.
Te elementy, które występują w Lhimku, a nie ma ich
w C/C++ są omówione w ramce “Lhimk” lub w samym Założenia
tekście artykułu. Jeżeli ktoś nie chce używać Lhimka Podobnie jak w poprzednim artykule zaczniemy od
(co całkowicie rozumiem), to może z łatwością poka- ustalenia, co chcemy osiągnąć. Mamy już napisane
zany kod przepisać w C++. Można też z Lhimka ko- UDB, które można przyrównać do obsługi pamięci na
rzystać z wewnątrz programów napisanych w C/C++. stercie (ang. heap memory). Tyle, że UDB to pamięć
trwała (ang. persistent) i z obsługą transakcji. W UDB
Jeszcze o UDB każdy obiekt ma przypisany PID (daleki odpowiednik
Oczywiście, nie byłbym sobą, gdybym już nie zmienił wskaźnika), który pozwala odczytywać, zmieniać war-
tego, co opisałem w poprzednich artykułach. Uważ- tość i usuwać obiekty. Teraz dodamy możliwość za-
ni czytelnicy pamiętają, że w pokazywanym przeze pisywania danych adresowanych dowolnym kluczem
mnie rozwiązaniu UDB, były pamiętane 2 wartości (np. tekstem) i przeglądanie ich w kolejności tego klucza.
dla każdego obiektu – obecna i poprzednia. Pozwa- API jest zbudowane w sposób ogólny, bez zna-
lało to jednocześnie czytać i zmieniać dane. Niestety, jomości typów klucza (K) i wartości (T). Ponieważ
jeżeli transakcja czytająca trwała na tyle długo, żeby Lhimk wspiera szablony i programowanie generycz-
w jej czasie wykonały się dwie transakcje zapisujące, ne, można podawać dowolne typy, nie musimy ich
to mogły wystąpić konflikty przy czytaniu. Nie dawa- znać pisząc nasz kod.
A oto nasze API:
Paweł od 20 lat zajmuje się tworzeniem oprogramowa-
nia, głównie w C/C++. Od trzech lat rozwija system dy-
l operator[[K k]] – odczyt wartości zapisanej pod
namicznej kompilacji Lhimk. kluczem k
Kontakt z autorem: pawel@lhimk.org l operator[[K k]]=T t – zapis wartości t pod klu-
czem k

32 www.sdjournal.org Software Developer’s Journal 1/2006


Zbudujmy sobie bazę danych – LhimkDB

�� �� �� �� �� �� �� �� � � � �

Rysunek 2. Zwykła posortowana lista (L0)


�� �� �� �� �� �� �� ��
l remove(K k) – usunięcie wartości zapisanej pod kluczem k
l startTransaction() -- rozpoczęcie transakcji zapisu Rysunek 3. Posortowana lista z linkiem pomiędzy co drugim
l commitTransaction() -- koniec transakcji zapisu z potwier- elementem (L0)
dzeniem wprowadzonych zmian
W bazach danych stosowane są zmodyfikowane algorytmy
+ kod iteratora (end, next, prev itp.) znane ze zwykłych struktur danych: tablice asocjacyjne (ang.
W tym momencie muszę trochę wyjaśnić, jak działa defi- hash tables), drzewa binarne (B-Tree) itp. Najczęściej w ba-
niowanie własnych operatorów w Lhimku. Po pierwsze, to co zach danych stosowana jest jakaś wersja B+-tree lub B*-tree.
napisałem powyżej, jest pseudokodem, w Lhimku nazwy ope- Więcej na ten temat można przeczytać w artykule: Thomassa
ratorów, są legalnymi nazwami metod, bez żadnych dodatko- Niemanna “A compact guide to searching and sorting” (http:
wych znaków. Dla przykładu: //epaperpress.com/sortsearch/index.html). Jest to związane
z tym, że tradycyjne bazy danych chcą upakować wszystko do
l operator _ bb – oznacza kod dla operatora [[]] jednej strony i mieć jak najmniej odczytów i zapisów stron.
l operator _ bba – oznacza kod dla operatora [[]]= Osobiście nie mam wielkiego przekonania, że takie podej-
l operator _ eq – oznacza kod dla operatora == ście ma jeszcze jakieś dobre uzasadnienie (zresztą nie tylko
ja). Nowoczesny sprzęt i systemy operacyjne bardzo zmieniły
itd. Ale to jest drobiazg. O wiele ciekawszy jest operator [[]]= sytuację w ostatnich kilku latach. Dyski (bardziej poprawnie: ich
(operator _ bba). Otóż, w języku C++ jeżeli piszemy kod klasy sterowniki) mają kilka poziomów pamięci, własne algorytmy ko-
np. Vector i chcemy zrobić przypisanie do i-tej komórki, to wy- lejkowania zapisów itp. Nie ma wielkiego sensu umieszczanie
gląda to mniej więcej tak: dzisiaj takiego kodu w bazie danych. Moim zdaniem lepiej sko-
rzystać z jak najprostszych rozwiązań, które będą dobrze wy-
T& operator[](K k) korzystywać nowoczesny sprzęt – np. cache procesora. Dlate-
go LhimkDB stosuje algorytm Skip List, wymyślony w 1989 r.
Oznacza to, że metoda operatora zwróci referencję do obiek- przez Billa Pugha (http://www.cs.umd.edu/~pugh/). Jest to tak
tu, więc w przypadku kodu: prosty algorytm, że mogę go wyjaśnić tutaj w dwóch akapitach.
Weźmy najprostszą listę jednokierunkową w postaci [klucz,
v[10] = 20; dane, wskaźnik następnego rekordu]. W takiej liście znalezienie
rekordu o podanym kluczu polega na przeglądaniu kolejnych re-
do komórki 10 jakoś “magicznie” zostanie przypisana wartość kordów, aż odnajdziemy żądany klucz. Możemy te rekordy trzy-
20. Nie chcę krytykować C++, bo naprawdę lubię ten język
i napisałem w nim sporo dużych programów. Ale akurat refe-
������������������������������������������������������������������
rencje, to jest coś, czego serdecznie nie znoszę. Pewnie je- �����������������
stem za głupi, żeby je zrozumieć. Dlatego celowo nie doda-
� �
łem ich do Lhimka (choć kilka razy kusiło).
Kiedy piszemy kod klasy Vector, to tak naprawdę chcemy
mieć jakiś operator, który będzie odpowiednikiem metody in- � � �
sert. W Lhimku jest taki operator, to: [[]]= (albo: []= lub {}=).
Jeżeli mamy zdefiniowany taki operator, to kod:
� � � �

v[[10]] = 20 ;

�� �� �� �� �� �� �� ��
wywoła

���������������������������������������������������������������
operator_bba(10,20)
�������������������������

Proste. I o to chodziło. Używam wszędzie [[]], a nie pojedyn- � � �


czych [], bo taka jest w Lhimku konwencja (jeżeli mogę użyć
tego słowa w stosunku do tak młodego tworu). Operator [[]] � � � �
jest odwołaniem do tablicy z jakimś dodatkowym “czymś”,
operator [], to szybkie odczytanie wartości z tablicy.
� � � � �
SkipList
Najpierw zajmiemy się zbudowaniem struktury danych, która �� �� �� �� � �� �� �� ��
da nam dostęp do zapisanych wartości wg klucza z możliwo-
ścią przeglądania zgodnie z kolejnością klucza. W tej chwili za- Rysunek 4. Przykład SkipListy z pokazaniem wkładania
pominamy, że budujemy bazę danych i robimy “zwykłą” klasę. nowego elementu

Software Developer’s Journal 1/2006 www.sdjournal.org 33


Bazy
danych

mać w postaci posortowanej i wtedy, kiedy znajdziemy rekord,


o większym kluczu, wiemy, że nie ma co dalej szukać – naszego Lhimk
klucza już nie znajdziemy (nazwijmy taką posortowaną listę L0 – Lhimk jest środowiskiem dynamicznej kompilacji dla języka o tej
Rysunek 2.). Wyobraźmy sobie, że mamy 100 rekordów na takiej samej nazwie (Lhimk), który jest oparty na C. Lhimk jest językiem
liście. Bierzemy co drugi rekord i łączymy je dodatkowymi wskaź- obiektowym, o bardzo prostej składni, w zasadzie, każdy kto zna
C/C++ może od razu zacząć programować w Lhimku. Do składni
nikami tworząc kolejną listę (nazwijmy ją L1 – Rysunek 3.). Teraz
C są dodane klasy z wielodziedziczeniem (ang. multiinheritance)
możemy najpierw odszukać największy klucz niewiększy od na-
i możliwością przeciążania operatorów, szablony, sygnały, wyjąt-
szego na liście L1, a potem sprawdzić na L0, czy mamy trafie-
ki i kilka innych drobiazgów. Lhimk jest środowiskiem dynamicznej
nie. W ten sposób szukamy 2 razy szybciej. Możemy też wziąć kompilacji, więc cały kod jest kompilowany w momencie urucha-
co czwarty element i zrobić listę L2. Teraz będziemy szukać 4 ra- miania programu. Kompilator jest bardzo szybki, więc trwa to kró-
zy szybciej. Możemy wziąć co ósmy, co szesnasty, co 32-ugi ele- cej niż ładowanie wielu bibliotek w skompilowanych programach.
ment itd. W ten sposób utworzymy drzewo binarne, w którym Lhimk jest dostępny częściowo na licencji LGPL i częścio-
szybkość odnalezienia elementu jest zależna od logarytmu ilo- wo na licencji BSD. Wynika to z tego, że korzysta z kodu TinyCC,
ści obiektów. Dla statycznych, z góry określonych danych nie ma który jest na licencji LGPL, natomiast moją intencją jest dawać
problemu, żeby takie idealne drzewo utworzyć. Gorzej jest kiedy wszystko na licencji BSD. Podstawową jednostką kodu w Lhim-
dane do drzewa są dodawane i usuwane, a my nie wiemy w ja- ku jest moduł. Każdy moduł ma unikalny adres, np.: \lhimk.org\
Db\UDB
kiej kolejności i co nadejdzie dalej.
W Lhimku nie ma podziału na pliki nagłówkowe i implementa-
Genialny pomysł Skip List polega na tym, że przy każ-
cję. Jak chcemy się odwołać do jakiegoś typu lub obiektu z innego
dym wkładanym elemencie losujemy do jakich list go zapi- modułu, po prostu podajemy całą ścieżkę. To jest np. odwołanie
sać. Z prawdopodobieństwem 1 do listy L0. Z prawdopodo- do klasy UDB (odwołania do typów poprzedzone są znakiem '@'):
bieństwem 1/2 do listy L1, 1/4 do L2 itd. W ten sposób za- @\lhimk.org\Db\UDB\UDB
wsze na L0 będą wszystkie elementy, na L1 mniej więcej Na końcu jest dwa razy UDB, ponieważ klasa UDB jest w mo-
połowa, na L2 jedna czwarta itd. Statystycznie będzie to dule UDB. Obiekty globalne (czyli wbudowane funkcje) są po pro-
dokładnie to samo jakbyśmy ręcznie robili listy ze statycz- stu poprzedzane przez '\', np.: \printf(“Hello!\n”);
nego zbioru danych. Co więcej, algorytmy są banalne, za- Klasa może mieć metody wirtualne, albo statyczne. W Lhimku
raczej nie używa się referencji do obiektów, a wyłącznie wskaź-
mieściłem je w całości w pierwszym artykule z tego cy-
niki (Lhimk ma wbudowany garbage collector). Nowy obiekt naj-
klu, teraz zamieszczam prezentację graficzną na rysunku
częściej tworzony jest tak: @\lhimk.org\Db\UDB\UDB *udb = \
4. Dla porównania, usuwanie elementu z B+-tree jest tema- lhimk.org\Db\UDB\UDB::new();
tem całych rozpraw (np. Jan Jannink „Implementing dele- Gdzie metoda new jest zwykłą metodą statyczną zwracającą
tion in B+-Trees”). UDB*. Szablony są realizowane na poziomie modułu, który jest jed-
Najlepsze w LhimkDB jest to, że nie tworzymy żadnej spe- nostką kompilacji. Jeżeli chcemy mieć np. listę integerów, napisze-
cjalnej wersji tego algorytmu „dla baz danych”. Korzystamy my to tak: @\lhimk.org\LTL\List\List[int T] *udb = @\lhimk.org\
dokładnie z tego samego kodu, który obsługuje „zwykłe” Ski- LTL\List\List[int T]::new();

pListy, łącząc je bardzo prosto z kodem UDB. Zanim pokażę W ten sposób moduł \lhimk.org\LTL\List\ zostanie przekom-
pilowany z typem T zdefiniowanym jako int. Jest to podejście dużo
jak to zrobić, musimy mieć jeszcze jeden mechanizm: trwały
prostsze niż w C++, a czasami daje dużo lepsze możliwości (można
wskaźnik (ang. persistent pointer).
np. w zależności od typu T dodawać do klas poszczególne metody).
Sygnały są odpowiednikami tablicy wskaźników do funkcji. Sygnał
Persistent Pointer (PPointer) deklarujemy tak: signal changed(A*); Potem możemy dodać meto-
Zostawmy na chwilę Skip Listę i zastanówmy się jak zintegro- dę, która będzie wywoływana: this->changed << a->handle; //bez ()!
wać z naszą bazą danych UDB, najprostszą listę, taką jak L0 Jeżeli gdzieś w kodzie jest: this->changed->call(a); to zostanie wy-
z powyższego opisu (tyle, że nieposortowaną). wołanie: a->handle(); To chyba cały podręcznik Lhimka...
Klasa Lista może mieć takie pola:

Lista* next; Oznacza to, że next będzie trwałym wskaźnikiem, a nie zwy-
int val; kłym wskaźnikiem. Używamy go jak tablicy, żeby móc prze-
int key; ciążyć operator przypisania (patrz wyżej): l->next[[0]] = l2;
To w rezultacie wywoła: PPointer::operator_bba(0, l2);
Wiadomo o co chodzi, będziemy szukać elementów z odpo- Nałóżmy na obiekt klasy Lista jeszcze obowiązek pamię-
wiednim key i zwracać ich val. Używać chcemy tej struktury tania swojego PID (metody getPid i setPid).
w najprostszy sposób: Wewnątrz operator _ bba klasy PPointer, postępujemy tak
(pełny kod znajduje się na Listingu 1.):
Lista *l = Lista::new(key, val);
Lista *l2 = Lista::new(key2, val2); l Zapamiętaj wskaźnik do obiektu, jeżeli obiekt nie był jesz-
l->next = l2; cze zapisany do bazy danych (ma getPid()==0), to zapisz
go do bazy danych.
W momencie, kiedy robimy l->next=l2, chcielibyśmy, żeby l2
zostało “zapisane do bazy danych”. W tym celu zrobimy drob- Podobnie działa operator_bb (odczyt):
ną modyfikację. Wskaźnik next zastąpimy taką strukturą:
l Jeżeli masz wskaźnik do obiektu, to go zwróć (znaczy, że już
PPointer[Lista T] *next; go czytaliśmy), jeżeli nie, to weź pid i odczytaj obiekt z bazy.

34 www.sdjournal.org Software Developer’s Journal 1/2006


Zbudujmy sobie bazę danych – LhimkDB

Listing 1. Implementacja klas PPointer i PPointerSystem


class PPointerSystem { @\lstream\Stream *s = \lstream\Stream::new();
@\ludb\UDB *udb; a->serialize(s);
@\lmap\Map[unsigned long long K][A* T] *cache; this->udb->atPut(a->getPid(), s->data);
@\lmap\Map[unsigned long long K][A* T] *changed; it->next();};
void *user_data; this->changed->clear();};
constructor new(char* path, int DELETE=0) { A* getRoot() {
PPointerSystem* this = \lhc\new(PPointerSystem*); A* root = this->at(1);
this->udb = \ludb\UDB::new(path, DELETE); if (!root) {
this->cache = \lmap\Map[unsigned long long K] root = A::newRoot(this);
[A* T]::new(); if (!this->udb->isInTransaction()) {
this->changed = \lmap\Map[unsigned long long K] this->beginTransaction();
[A* T]::new(); this->put(root);
return this; }; this->commitTransaction();}
PPointerSystem* clone() { if (root->getPid()!=1) {
PPointerSystem* clone = \lhc\new(PPointerSystem*); ERROR("Root must be saved first, at pid==1",
clone->udb = this->udb->clone(); ERROR_MINOR);}}
clone->cache = \lmap\Map[unsigned long long K] return root;};};
[A* T]::new(); class PPointer {
clone->changed = \lmap\Map[unsigned long long K] PPointerSystem* ps;
[A* T]::new(); A* objects[[]];
return this; }; A* parent;
void beginTransaction() { unsigned long long pids[[]];
this->udb->beginTransaction(); signal changed(A*);
}; constructor new(PPointerSystem* ps, A* parent) {
void commitTransaction() { PPointer* this = \lhc\new(PPointer*);
this->commit(); this->ps = ps;
this->udb->commitTransaction(); }; this->objects = \lhc\array_new(A*[[]],1);
void free() { this->pids = \lhc\array_new(unsigned long long[[]],1);
this->udb->free(); }; this->changed = \lhc\signal_new(signal changed(A*));
A* at(unsigned long long pid) { this->parent = parent;
A* a = this->cache[[pid]]; return this;};
if (!a) { A* operator_bb(unsigned int ind) {
char data[[]] = this->udb->at(pid); A* r= this->objects[[ind]];
if (data) { if (!r && this->pids[[ind]]!=0) {
@\lstream\Stream *s = \lstream\Stream:: r=this->ps->at(this->pids[[ind]]);}
new(data); return r;};
a = A::deserialize(this, s); A* operator_bba(unsigned int ind, A* obj) {
this->cache[[pid]] = a;} } this->objects[[ind]] = obj;
return a; }; if (obj) {
unsigned long long put(A* a) { if (obj->getPid()==0) {
@\lstream\Stream *s = \lstream\Stream::new(); this->ps->put(obj);}
a->serialize(s); this->pids[[ind]] = obj->getPid();
unsigned long long pid = this->udb->put(s->data); } else {
a->setPid(pid); this->pids[[ind]] = 0;}
this->cache[[a->getPid()]] = a; this->changed->call(this->parent);
return pid; }; return obj;};
void atPut(unsigned long long pid, A* a) { void resize(unsigned int size) {
@\lstream\Stream *s = \lstream\Stream::new(); \array_resize(this->objects,size);
a->serialize(s); \array_resize(this->pids,size);
this->udb->atPut(pid, s->data); this->changed->call(this->parent);};
a->setPid(pid); void serialize(@\lstream\Stream *s) {
this->cache[[pid]] = a; //we save only pids
return pid; }; int size = this->pids->size;
void remove(A* a) { s->putInt(size);
this->udb->removeAt(a->getPid()); int i = 0;
this->cache->remove(a->getPid()); for (i=0; i<size; i++) {
this->changed->remove(a->getPid()); }; s->putLongLong(this->pids[i]);}};
void markChanged(A* a) { void deserialize(@\lstream\Stream *s) {
if (a->getPid()>0) { //we save only pids
this->changed[[a->getPid()]] = a; }}; int size = s->getInt();
void commit() { this->resize(size);
@\lmap\Iterator[unsigned long long K][A* T] int i = 0;
*it = this->changed->getIterator(); for (i=0; i<size; i++) {
while (!it->end()) { this->pids[i] = s->getLongLong();};};};
A *a = it->get();

Software Developer’s Journal 1/2006 www.sdjournal.org 35


Bazy
danych

Dane do zapisu pobieramy od obiektu metodą serialize,


a nowy obiekt z odczytanych danych robimy przy pomocy W sieci
metody deserialize. W zasadzie każda klasa, która spełnia te
warunki może być używana z PPointer i będzie “automatycz- l Strona domowa projektu Lhimk i LhimkDB
nie” zmieniana w bazę danych. http://www.lhimk.org
Do pełni szczęścia musimy mieć jeszcze jakiś obiekt Root, od l Strona domowa Billa Pugha
którego zaczynamy zapisywanie i czytanie. Taki obiekt pamięta- http://www.cs.umd.edu/~pugh/
my pod ustalonym z góry PID, w naszym wypadku jest to 1.
Jedyny haczyk, który jeszcze został, jest taki, że nie mo-
żemy bezpośrednio do zapisu i odczytu użyć UDB. Musimy to właśnie zrobiliśmy prawie wszystko, co trzeba, żeby przero-
mieć pewną strukturę pośrednią, która będzie zapamiętywać bić “zwykłą” SkipListę, na bazę danych. Wystarczy jeszcze do
wszystkie już odczytane obiekty (taki podręczny cache). In- tej klasy dodać metody setPid i getPid oraz serialize i dese-
aczej, mogłoby się zdarzyć, że odczytamy obiekt dwa razy rialize, żeby spełnić warunki opisane wyżej.
i będziemy mieli dwa egzemplarze tego samego obiektu Teraz możemy utworzyć nową klasę SkipList (nazwa
(z tym samym PID). Dodatkowo, nie będziemy zapisywać obiek- może być ta sama, bo jest trzymana w innym miejscu). Bę-
tu za każdym razem, kiedy jest zmieniana jego wartość. Będzie- dzie ona dziedziczyć z naszej „normalnej” klasy SkipList,
my zapamiętywać zmienione obiekty i zapisywać dopiero pod- ale podamy nasz nowy SkipListElement jako parametr kom-
czas robienia commitTransaction(). Klasa, która to realizuje nazy- pilacji szablonu:
wa się PPointerSystem i pokazana jest na Listingu 1.
class SkipList : \...\SkipList[SkipListElement
PPointer + SkipList == baza danych SkipListElement][T T][K K]
Zobaczmy teraz jak połączyć PPointer i SkipListę. SkipLi-
sta, podobnie jak każda inna lista, zbudowana jest elementów Proste prawda? Wiem, że nie jest to proste, ale działa! Co naj-
(SkipListElement), które połączone są między sobą wskaźni- ważniejsze, nie mamy dwóch implementacji klas SkipList – jed-
kami. Każdy element SkipListy ma tablicę wskaźników for- nej do „zwykłych” zastosowań, drugiej do baz danych. Dodatko-
ward do elementów przed nim, i jeden wskaźnik prev, służą- wo korzystanie z obu klas jest identyczne. Jedyny wyjątek jest
cy do przeglądania listy wstecz. Żeby wyszła nam sztuczka taki, że wersji bazodanowej, przed zapisem trzeba rozpocząć
z PPointer, wskaźnik prev też trzymamy w tablicy, tyle, że jedno- transakcję i po skończeniu modyfikacji transakcję zakończyć.
elementowej. W “zwykłym” SkipListElement mamy takie pola: Idąc dalej, nasza „baza danych” dziedziczy pośrednio
z bazowych klas typu Container. Możemy zatem korzystać
SkipListElement *prev[[]]; ze wszelkich algorytmów uogólnionych, np. z wypisywania,
SkipListElement *forward[[]]; przeszukiwania itp. Możemy dowolnie w kodzie mieszać
dostęp do danych zapisanych na dysku ze zwykłymi struk-
Jeśli zrobimy nowy SkipListElement z takimi polami: turami w pamięci. Właśnie z tej cechy będziemy korzystać
za miesiąc, kiedy zbudujemy wysokopoziomowe transak-
@\lpp\PPointer[SkipListElement A] *prev; cje, czyli takie jakie powinny być wykorzystywane w „zwy-
@\lpp\PPointer[SkipListElement A] *forward; kłych programach”.

Listing 2. Przykłady użycia klasy SkipList i Db\SkipList Podsumowanie


Jak widać baza danych klucz-wartość, to zwykła struktura ta-
//Zwykład SkipLista: blicy asocjacyjnej (w naszym wypadku użyliśmy SkipListy)
@\lskiplist\SkipList[int T][int K]* list z elementami obsługi persystencji i transakcji. Mając w miarę
= \lskiplist\SkipList[int T][int K]::new(); nowoczesne środowisko programowania, można oba te pro-
list[[1]] = 1; blemy rozdzielić i na końcu łatwo połączyć np. przy pomocy
list[[1]] += 1; szablonów. Nie ma tu żadnej zasługi Lhimka, można to samo
int v = list[[1]]; osiągnąć np. w C++. W rezultacie baza danych jest jeszcze
list->remove(1); jednym typem generycznym (których mamy dużo np. w STL-
list->free(); //nie musimy tego robić u) i można do baz danych stosować wszystkie techniki znane
//SkipLista – baza danych: z programowania uogólnionego (generycznego).
@\lskiplistdb\SkipList[int T][int K]* list Zamiana SkipListy w bazę danych jest bardzo prosta (kil-
= \lskiplistdb\SkipList[int T][int K]::new(“BAZA_ kadziesiąt minut), nie ma praktycznie żadnego powodu, że-
DANYCH”); by nie dało się w ten sposób zamienić dowolnej innej struktu-
list->startTransaction(); ry. Pozwala to na łatwe eksperymentowanie i daje wielką ela-
list[[1]] = 1; styczność w wykorzystaniu zaprezentowanego rozwiązania.
list[[1]] += 1; Za miesiąc dodamy jeszcze jedną warstwę, która pozwo-
int v = list[[1]]; li uruchamiać dowolną ilość transakcji odczytu i zapisu jed-
list->remove(1); nocześnie. Będzie ona też odpowiedzialna za rozwiązywa-
list->commitTransaction(); nie konfliktów. Zobaczymy też jak można naszą bazę klucz-
list->free(); //zamyka bazę danych wartość zamienić w bazę drzewiastą (po dzisiejszym artykule,
myślę, że wszyscy wiecie jak to zrobić). n

36 www.sdjournal.org Software Developer’s Journal 1/2006


Bazy

danych

Zbudujmy sobie bazę danych – LhimkDB cz 4


Paweł Marciniak
Dodajemy strukturę drzewa

T
rzy miesiące temu rozpocząłem cykl artyku- tor desrialize można napisać “ręcznie”, można też
łów opisujący jak zbudować od podstaw ba- wykorzystywać informacje o klasach z RTTI, do auto-
zę danych. Zbudowaliśmy bazę danych – matycznego generowania takiego kodu.
LhimkDB – do wbudowywania do programów (ang. Klasa Datum spełnia w Lhimku jeszcze jedną
embedded database), o architekturze klucz-wartość funkcję. Otóż, umożliwia ona programowanie gene-
(ang. key-value). Teraz dodamy do niej możliwość ryczne (uogólnione). Zobaczmy konkretny przypa-
zapisu danych w strukturze drzewa (ang. tree struc- dek. W miejscu, gdzie chcemy zapisać dane do bazy
tured database). Wszystkie przedstawione do tej po- danych, musimy zrobić coś takiego:
ry warstwy LhimkDB mogą być używane niezależ-
nie. Tak będzie i tym razem, dodanie struktury drze- key->serialize(stream);
wa nie oznacza, że nie można z LhimkDB korzystać value->serialize(stream);
jak z bazy klucz-wartość.
LhimkDB jest pisany w języku Lhimk. Język ten Przy czym, nie znamy typów key i value. Gdybyśmy
jest na tyle podobny do C/C++, że nie ma sensu się założyli, że są to zawsze obiekty, to sprawa by była
go uczyć, czy wyjaśniać jak w nim pisać programy. rozwiązana, wystarczy zażądać implementacji tych
Te elementy, które występują w Lhimku, a nie ma ich dwóch metod. Ale tak nie jest, bo mogą to być rów-
w C/C++ są omówione w ramce “Lhimk” lub w samym nież typy proste. W C++ rozwiązuje się to poprzez
tekście artykułu. Jeżeli ktoś nie chce używać Lhimka specjalizację metod. W C++ napisalibyśmy to tak:
(co całkowicie rozumiem), to może z łatwością pokaza-
ny kod przepisać w C++. Można też z Lhimka korzystać stream->serialize(key);
z wewnątrz programów napisanych w C/C++. stream->serialize(value);

Datum W kodzie klasy Stream, musielibyśmy zdefiniować


Zanim przejdziemy do struktury drzewiastej, zajmie- wiele specjalizacji metody serialize, ze względu na
my się rzeczą prostą, przyziemną, ale niezbędną – typ argumentu:
możliwością zapisu dowolnych typów danych do bazy.
W bazach danych najmniejszy element, jaki może- void serialize(int val);
my zapisać, przyjęło się nazywać datum. Jest to typ void serialize(float val);
danych podobny do typu Variant znanego z języków ...
wyższego poziomu, lub różnych obiektowych biblio-
tek. Nie ma w nim nic magicznego, po prostu przecho- Wtedy kompilator dobierze dla nas „odpowiednią” me-
wuje on dane wraz z identyfikatorem typu. Dodatkowo todę, która dobrze obsłuży nasz typ. Taka jest teoria.
Datum musi umieć wykonać serializację tych danych, W praktyce zasady automatycznej konwersji pomię-
czyli zamianę na strumień bajtów, który zostanie za- dzy typami w C++ są dość złożone. Do tego docho-
pisany do bazy. W przypadku typów prostych, łańcu- dzą klasy, które mogą mieć niejawne operatory kon-
chów znaków itp. jest to oczywiście banalne. Sytuacja wertujące (ang. implicit convertion operators). Łatwo
nieco się komplikuje w przypadku obiektów klas. Na napisać, nawet bardzo niewielki program w C++, któ-
szczęście Lhimk jest środowiskiem dynamicznej kom- ry będzie nie do zrozumienia. W większych projektach
pilacji, możemy zatem zapisać nazwę klasy i później stosowanie takich technik jest po prostu zabronione.
z tej nazwy odtworzyć obiekt (wywołując specjalny Oczywiście, w związku z tymi trudnościami, nie
konstruktor deserialize). W Lhimku nazwa klasy jest chciałem w Lhimku implementować specjalizacji me-
również URL-em do miejsca w internecie, skąd moż- tod (inaczej nazywanej polimorfizmem parametrycz-
na ściągnąć kod tej klasy. Dlatego, nawet jeśli czyta- nym, lub jeszcze inaczej: multimetody). Miałem jesz-
my czyjąś bazę danych i nie mamy tych klas, to sys- cze jeden powód: szybkość kompilacji, która jest
tem może je automatycznie załadować i skompilować szczególnie ważna w systemach, gdzie użytkowni-
(oczywiście w trakcie działania programu). Konstruk- kom daje się kod źródłowy, a kompilacja następuje
przy starcie programu. Dlatego w Lhimku jest inne
Paweł od 20 lat zajmuje się tworzeniem oprogramowa-
rozwiązania. Do budowania generycznego kodu słu-
nia, głównie w C/C++. Od trzech lat rozwija system dy- ży operator '#'. Np. operator '#s' zamienia wszystko
namicznej kompilacji Lhimk. w string. Możemy napisać:
Kontakt z autorem: pawel@lhimk.org
napis = “Ilość : “ + 100#s + “ wartość: “ + 11.22#s;

40 www.sdjournal.org Software Developer’s Journal 2/2006


LhimkDB cz 4 - Dodajemy strukturę drzewa

��� ����� �������


Lhimk ��� ���� ���

Lhimk jest środowiskiem dynamicznej kompilacji dla języka o tej sa- ��� �������� ��������
mej nazwie (Lhimk), który jest oparty na C. Lhimk jest językiem obiek-
��� ����� �����������������������
towym, o bardzo prostej składni, w zasadzie, każdy kto zna C/C++
może od razu zacząć programować w Lhimku. Do składni C są doda- ��� ������� �������
ne klasy z wielodziedziczeniem (ang. multiinheritance) i możliwością
przeciążania operatorów, szablony, sygnały, wyjątki i kilka innych dro- ��� �����������
biazgów. Lhimk jest środowiskiem dynamicznej kompilacji, więc cały
��� ����� ������
kod jest kompilowany w momencie uruchamiania programu. Kompi-
lator jest bardzo szybki, więc trwa to krócej niż ładowanie wielu biblio- ��� ������ ��������
tek w skompilowanych programach. Lhimk jest dostępny częściowo
na licencji LGPL i częściowo na licencji BSD. Wynika to z tego, że ��� ���������� ��������
korzysta z kodu TinyCC, który jest na licencji LGPL, natomiast mo-
��� ��� ������
ją intencją jest dawać wszystko na licencji BSD. Podstawową jed-
nostką kodu w Lhimku jest moduł. Każdy moduł ma unikalny adres,
Rysunek 1. Budowa drzewa przy pomocy typu Datum i list
np.: \lhimk.org\Db\UDB W Lhimku nie ma podziału na pliki nagłów-
kowe i implementację. Jak chcemy się odwołać do jakiegoś typu Wystarczy, że metoda serialize będzie zaimplementowana
lub obiektu z innego modułu, po prostu podajemy całą ścieżkę. To w klasie Datum. Możemy teraz używać bazy danych do prze-
jest np. odwołanie do klasy UDB (odwołania do typów poprzedzo- chowywania dowolnych wartości:
ne są znakiem '@'): @\lhimk.org\Db\UDB\UDB Na końcu jest dwa ra-
zy UDB, ponieważ klasa UDB jest w module UDB. Obiekty globalne
@\lds\SkipList[Datum* T][Datum* K]* list =
(czyli wbudowane funkcje) są po prostu poprzedzane przez '\', np.:
\lds\SkipList[Datum* T][Datum* K]::new("SKIPDB", 1);
\printf(“Hello!\n”); Klasa może mieć metody wirtualne, albo sta-
tyczne. W Lhimku raczej nie używa się referencji do obiektów, a wy- list->beginTransaction();

łącznie wskaźniki (Lhimk ma wbudowany garbage collector). Nowy list[[10#d]] = "abcd"#d;


obiekt najczęściej tworzony jest tak: @\lhimk.org\Db\UDB\UDB *udb = list->commitTransaction();
\lhimk.org\Db\UDB\UDB::new(); Gdzie metoda new jest zwykłą me-
todą statyczną zwracającą UDB*. Szablony są realizowane na pozio- Struktura drzewiasta cz. 1
mie modułu, który jest jednostką kompilacji. Jeżeli chcemy mieć np. Skoro możemy przechowywać wartości dowolnych typów, to
listę integerów, napiszemy to tak: @\lhimk.org\LTL\List\List[int co się stanie jak zapiszemy jako element inną listę? Utworzy-
T] *udb = @\lhimk.org\LTL\List\List[int T]::new(); W ten spo-
my drzewo! W zasadzie implementację drzewa dostajemy
sób moduł \lhimk.org\LTL\List\ zostanie przekompilowany z ty-
prawie za darmo, dzięki programowaniu generycznemu i ty-
pem T zdefiniowanym jako int. Jest to podejście dużo prostsze niż
powi Datum. Jest to rzecz, nad którą myślałem dwa tygodnie,
w C++, a czasami daje dużo lepsze możliwości (można np. w zależ-
ności od typu T dodawać do klas poszczególne metody). Sygnały
a potem napisałem w godzinę – naprawdę! To naturalne roz-
są odpowiednikami tablicy wskaźników do funkcji. Sygnał deklaru- wiązanie było tak proste, że bardzo długo nie mogłem go “na
jemy tak: signal changed(A*); Potem możemy dodać metodę, która poważnie” zaakceptować (patrz Rysunek 1).
będzie wywoływana: this->changed << a->handle; //bez ()! Je- Zanim naprawdę to zrobimy wprowadzimy jeszcze jedną
żeli gdzieś w kodzie jest: this->changed->call(a); to zostanie wy- ważną konstrukcję: transakcje użytkownika.
wołanie: a->handle(); W Lhimku programowanie generyczne jest
możliwe dzięki operatorom #s, #d, #i itp. Np.: napis = “Ilość : “ + Transakcje użytkownika
count#s + “ wartość: “ + value#s; Konwertuje count i value na Do tej pory nasz system był budowany wg zasady: mamy tyl-
string, niezależnie od ich typów. Operator #d konwertuje na Da-
ko jeden wątek zapisujący i dowolną ilość wątków czytają-
tum, #i na integer itd.
cych. W prawdziwych zastosowaniach jest to oczywiście nie
To chyba cały podręcznik Lhimka...
do przyjęcia. Trzeba by było wtedy wszystkie zapisy grupo-
wać i robić w jednej zwartej paczce na koniec wykonywania
operacji (np. obsługi zapytania HTTP).
Możemy też napisać: Problemy te rozwiązuje klasa Transaction. Jest to banalne
rozwiązanie polegające na tym, że mamy dwie listy – cache i tar-
napis = “Ilość : “ + count#s + “ wartość: “ + value#s; get. Wszelkie zmiany w danych (zapis i usunięcie) są zapisywa-
ne najpierw w liście cache, która jest oczywiście listą w RAM-ie.
nie martwiąc się o typy zmiennych count i value. W przypadku Dopiero, kiedy potwierdzamy transakcję metodą commit, wszelkie
typów prostych (wbudowanych) kompilator wykona odpowied- zmiany z cache'a są zapisywane do targetu. Tylko na ten, bardzo
nią konwersję. W przypadku obiektów klas, będzie wywołana krótki czas, transakcja ma wyłączność na zapis. Wszelkie kon-
metoda operator _ hash _ s. flikty pomiędzy transakcjami są rozwiązywane przez klasę Db,
Dokładanie tak samo możemy przekonwertować dowolną która ma listę wszystkich transakcji. W skrócie, jest sprawdzane,
daną dowolnego typu na Datum (tyle, że operatorem '#d'). Za- czy jakaś transakcja która zaczęła później niż nasza, a skończy-
tem nasz kod serializacji wygląda tak: ła wcześniej, nie zmieniła tych samych danych co my.
Jest tutaj przemycona jedna bardzo ważna i bardzo trudna
key#d->serialize(stream); do zrozumienia koncepcja. Otóż sprawdzamy konflikty na po-
value#d->serialize(stream); ziomie zapisu: klucz-wartość. Interesuje nas, czy ktoś nie zmie-

Software Developer’s Journal 2/2006 www.sdjournal.org 41


Bazy
danych

Listing 1. Sposób korzystania z transakcji W sieci


@\ldb\Db[int T][int K]* db =
• Strona domowa projektu Lhimk i LhimkDB
\ldb\Db[int T][int K]::new("DB", 1);
http://www.lhimk.org
@\ltrans\Transaction[int T][int K]* tr =
• Strona domowa Billa Pugha
db->startTransaction(); http://www.cs.umd.edu/~pugh/
for(i=0; i<maxnum;i++)
{
tr[[i]]=i; tuicyjne jest to, że, skoro są listą, to do nich zapisujemy dane
} (Listing 1).
tr->commit();
db->free(); Struktura drzewiasta cz. 2
Wróćmy teraz do naszego drzewa. Po pierwsze, teraz wsta-
wiamy elementy do transakcji, a nie do listy (jakkolwiek dzi-
nił wartości zapisanej pod tym samym kluczem. Z poprzednie- wacznie by to nie brzmiało), po drugie, drzewo utworzymy
go artykułu pamiętamy, że np. zapis jednego elementu klucz- wstawiając nową transakcję, a nie listę. Tym samym wszystkie
wartość modyfikuje również kilka innych elementów. Te modyfi- wątpliwości się rozwiały: transakcja jeszcze nie „wstawiona”
kacje dotyczą jednak wyłącznie wskaźników, i dopóki baza jest do bazy po prostu zapisuje swoje dane do cache'u. W trak-
spójna, nie wpływają na logikę aplikacji. Dlatego, sprawdzając cie zapisu transakcji, zapiszą się też wszystkie elementy, któ-
konflikty na tym poziomie, mamy ich po prostu dużo mniej i du- re są transakcjami (gałęziami drzewa), bo zapis ten będzie re-
żo więcej transakcji zakończy się sukcesem (jest dużo mniej alizowany w trakcie serializacji. Możemy mieć w jednej bazie
sytuacji, gdzie dwie transakcje zmieniły ten sam obiekt i system UDB (patrz art. sprzed dwóch miesięcy) dowolną ilość list, po-
musi jedną transakcję unieważnić). (Temat do rozważań: dla- nieważ lista musi wiedzieć wyłącznie, gdzie jest jej początek.
czego obiektowe bazy danych się nie przyjęły?) Listy takie nie kolidują ze sobą, po prostu wskazują na swoje
Nasze transakcje, są po prostu jeszcze jedną implementa- elementy. Dziwne, ale to naprawdę działa!
cją listy, tym razem z cache'em i możliwością zapisu. Mało in- Pójdziemy jeszcze krok dalej. Nie będziemy do bazy za-
pisywać obiektów klasy Transaction, będziemy zapisywać
Listing 2. Przykładowy kod klasy Invoice (faktura) obiekty dowolnej klasy, o ile potrafi się zachować jak transak-
cja (w rzeczywistości np. zawiera transakcję). W ten sposób
class Invoice : AccountingDocument { możemy dowolnie kształtować funkcjonalność takich obiek-
... tów. Możemy dodawać własne metody, ale też możemy wyko-
Datum* operator_bba(Datum* key, Datum* val) { nywać dodatkowe działania np. przy wprowadzaniu danych.
char* fname = key->getString(); Możemy np. dodać kontrolę, czy wprowadzane „pola” są
if (fname == "Date") { odpowiednie (czyli tworzyć tzw. semistrukturę, ang. semistruc-
@\ldate\Date* date = val->getClass( tured database). Możemy obsługiwać indeksy, czyli zapisywać
@\ldate\Date*); „wskaźnik” do obiektu w wyznaczonym miejscu, tak, żeby go
this->setDate(date); można było szybko odnaleźć. Możemy też wykonywać dodat-
} else kowe obliczenia. Ilustrację tego można zobaczyć na Listingu
if (fname == "Value") { 2, gdzie pokazany jest kod klasy Invoice (faktura). Oczywiście
float value = val->getFloat(); jest to kod czysto poglądowy, a nie produkcyjny. Najważniejsze
this->setValue(value); w tym przykładzie jest to, że dziedziczymy z klasy Accounting-
} Document, która jest wspólną podstawą dla wszelkich doku-
... mentów księgowych (np. może wykonywać dekretację).
else { W ten sposób powstała bardzo nowoczesna obiektowo-
ERROR(“wrong field”, ERROR_MINOR); drzewiasta (chyba zastrzegę ten nazwę :) baza danych. Ca-
} ła jej konstrukcja i sposób działania jest oparta na tym, że nie
return val; ma kodu “lepszego” i “gorszego”, napisanego w procedurze
}; składowanej SQL albo w kodzie aplikacji. Dzięki temu, moż-
void setDate(@\ldate\Date* date) { na naprawdę w sposób przejrzysty i obiektowy budować duże
this->addToIndex("Date", date); aplikacje biznesowe i portale WWW.
AccountingDocument::operator_bba(this,
"Date"#d, date); Podsumowanie
}; To już koniec naszej serii o LhimkDB. W ciągu czterech odcin-
void setValue(float value) { ków od obsługi trwałej pamięci doszliśmy do struktur obiekto-
AccountingDocument::operator_bba(this, wo-drzewiastych. Stworzyliśmy nie tylko jeszcze jedną bazę
"Value"#d, value); klucz-wartość, ale też zupełnie nową architekturę, pozwalają-
this[["Brutto"]] = value * this[[“VAT]]->getFloat(); cą na lepsze pisanie dużych systemów biznesowych. Przede
}; wszystkim chciałem pokazać, że napisanie takiej bazy danych
}; jest proste, że może to zrobić każdy kto ma kilka wolnych
chwil i niezbyt dużą wiedzę o programowaniu – jak ja. n

42 www.sdjournal.org Software Developer’s Journal 2/2006

You might also like