Prevayler, czyli jak sobie radzić bez relacyjnej bazy danych…

post_img

Prevayler to biblioteka implementująca ideę systemu persystentnego w języku Java. Strona główna projektu dostępna jest pod adresem www.prevayler.org a plik jar jest do pobrania ze strony  http://sourceforge.net/project/showfiles.php?group_id=36113

A czym jest system persystentny? To po prostu idea przechowywania danych  wszystkich danych w pamięci operacyjnej w ich „naturalnej” postaci zamiast standardowych tabel relacyjnej bazy danych.

Jak to działa? Spójrzmy na rysunek

Diagram prevayler

Diagram prevayler*

Prevayler [1] jest warstwą przetrzymującą obiekty biznesowe[2] obsługującą żądania z zewnątrz w formie transakcji za pomocą zdefiniowanego interfejsu transakcyjnego [3], np. interfejsu Commands czy Transaction. Każde żądanie[4] jest zapisywane na dysk[5] (np.  w postaci pliku .command lub .journal, zależnie od wykorzystywanego interfejsu) w razie awarii systemu i konieczności odzyskiwania danych. Dodatkowo Prevayler umożliwa zapisanie całego grafu zależności między obiektami[6] w postaci tzw snapshota tak często jak tylko programista uzna to za konieczne. Przy restarcie systemu dane są odzyskiwane przez odczytanie ostatniego snapshota oraz wykonanie później zapisanych komend/transakcji*. Przez transakcję rozumiem tutaj wykonanie trwałej zmiany na obiekcie biznesowym poprzez wywołanie żądania za pomocą wybranego interfejsu transakcyjnego, który zapewnia, że żadna inna operacja na danym obiekcie biznesowym nie zostanie wykonana w tym samym czasie. By korzystać z prevaylera nie trzeba wiedzieć więcej o nim samym, jednak dla zainteresowanych przedstawiam poniższy diagram

Wewnętrzna architektura prevaylera

Wewnętrzna architektura prevaylera**

Prevaylent Business Object reprezentuje obiekt, który chcemy persystować. Jak wcześniej pisałem, wszelkie zmiany na nim wykonywane są za pomocą transakcji (Transaction, TransactionWithQuery). Odczyt obiektów lub danych może być wykonywany poza transakcjami, gdyż nie zmieniają one stanu obiektu (Query). Trwałość transakcji i ich odtwarzalność gwarantują komponenty TransactionLogger i Snapshot Manager. TransactionLogger wychwytuje wywołanie transakcji i loguje je w sposób trwały (zapis transakcji na dysk) i nietrwały, który przechowuje informacje w pamięci operacyjnej. Zapis grafu obiektów wykonywany jest przez Snapshot Managera, który jest wspierany przez różnorodne metody serializacji obiektów (javowe i XMLowe). Odzyskiwanie stanu ze snapshota i serializacja obiektów w czasie transakcji jest wspierana przez komponeny Publisher i Subscriber. Rollback transakcji wspierany jest przez komponent Censorship – każda logowana transakcja jest najpierw wykonywana na kopii systemu persystentnego. I dopiero po poprawnym wykonaniu transakcji na kopii zmiany są wykonywane na obiekcie biznesowym. Komponent Clock odpowiada za czułość transakcji na czas wykonania. Replication synchronizuje dane przy wielu klientach podłączonych do aplikacji. Multithreading wspiera wielowątkowość. PrevaylerFactory odpowiada za konfigurację całej struktury i wszystkich komponentów prevaylera**. Aplikacje korzystające z tej biblioteki muszą więc zdefiniować jedynie obiekty oraz komendy które będą wykonywać zmiany (transakcje).

Zalety Prevaylera*:

  • nie potrzeba nam żadnego serwera do obsługi bazy danych
  • nie wymaga od nas żadnych specjalizowanych interfejsów, klas bazowych itp. (za wyjątkiem java.io.Serializable, który należy dodać do klas obiektów biznesowych by można je było zapisać na dysku)
  • zapewnia sekwencyjność transakcji, więc nie trzeba się martwić o różnego rodzaju zakleszczenia i spójność danych
  • jest najszybszą możliwą metodą obsługi obiektów biznesowych, ponieważ są one trzymane są w pamięci operacyjnej
  • w bibliotekę włączony jest interfejs transakcyjny Transaction, więc nie musimy szukać czy tworzyć własnej obsługi zmian na obiektach

Wady Prevaylera*:

  • wymusza deterministyczność w pisaniu komend. Nigdy nie należy używać takich metod jak System.currentTimeInMillis() w obsłudze transakcji, gdyż przy jej ponownym wykonaniu wartość ulegnie zmianie co może spowodować niespójność danych oraz niedeterministyczne zachowanie w systemie
  • nieumiejętne pisanie komend może bardzo szybko doprowadzić do wyczerpania zapasów pamięci trwałej, poprzez występowanie tzw. babtyzmu. Jest to problem polegający na wprowadzaniu do komend obiektów posiadających referencje do innych obiektów, które posiadają kolejne obiekty itd. co sprowadza się do zapisania całego grafu zależności przy wywołaniu pojedynczej transakcji. Co gorsza ponowne wykonanie takiej komendy najprawdopodobniej doprowadzi do niespójności danych gdyż odtworzony obiekt będzie posiadać referencję do nieprawidłowego obiektu (wszystkie obiekty są serializowane na dysk, więc po deserializacji uzyskamy kopie a nie oryginalne obiekty)
  • obiekty muszą tworzyć hierarchiczną strukturę z grafu z jednym wyspecyfikowanym korzeniem

Co zrobić by cieszyć się dobrodziejstwami prevayera i uniknąć kłopotów z niespójnością danych?*

  • w komendach nigdy nie używaj metod których wynik zależy od czasu wykonania (np.  System.currentTimeInMillis())
  • przekazuj identyfikatory zamiast obiektów, a następnie je wyszukuj, prevayler jest tak szybki, że nie wpłynie to na czas wykonania komendy

Przykład*** (cały projekt jest do ściągnięcia tutaj).

Na projekt składają się 4 klasy:

  1. Main, która odpala aplikację
  2. NumberKeeper, który przechowuje liczby
  3. NumberStorageTransaction, która implementuje interfejs transakcyjny
  4. PrimeCalculator która generuje liczby pierwsze i zapisuje je do NumberKeeper za pomocą transakcji NumberStorageTransaction

Prześledźmy co się dzieje krok po kroku w tej aplikacji

public class Main {

public static void main(String[] args) throws Exception {

(…)

Prevayler prevayler = PrevaylerFactory.createPrevayler(new NumberKeeper(), „demo1”);

new PrimeCalculator(prevayler).start();

}

(…)

}

Metoda main tworzy nam na starcie implementację prevaylera, do konstruktora przekazujemy 2 parametry: klasę która ma być korzeniem naszego grafu, oraz miejsce gdzie będą zapisywane/odczytywane transakcje. Następnie tworzymy generator liczb pierwszych i uruchamiamy go.

class PrimeCalculator {

private final Prevayler _prevayler;

private final NumberKeeper _numberKeeper;

PrimeCalculator(Prevayler prevayler) {

_prevayler = prevayler;

_numberKeeper = (NumberKeeper)prevayler.prevalentSystem();

}

void start() throws Exception {

int largestPrime = 0;

int primesFound = 0;

int primeCandidate = _numberKeeper.lastNumber() == 0 ? 2 : _numberKeeper.lastNumber() + 1;

while (primeCandidate <= Integer.MAX_VALUE) {

if (isPrime(primeCandidate)) {

_prevayler.execute(new NumberStorageTransaction(primeCandidate));

largestPrime = primeCandidate;

primesFound = _numberKeeper.numbers().size();

System.out.println(„Primes found: „ + primesFound + „. Largest: „ + largestPrime);

}

primeCandidate++;

}

}

//additional methods

}

Najistotniejszą linijką  w powyższym kodzie jest

_prevayler.execute(new NumberStorageTransaction(primeCandidate));

W której to dzieje się cała „magia” czyli wykonujemy operację dodania liczby do NumberKeeper i zapisu transakcji na dysk. Odpowiada za to nasza klasa transakcyjna

class NumberStorageTransaction implements Transaction {

private static final long serialVersionUID = -2023934810496653301L;

private int _numberToKeep;

private NumberStorageTransaction() {} //Necessary for Skaringa XML serialization

NumberStorageTransaction(int numberToKeep) {

_numberToKeep = numberToKeep;

}

public void executeOn(Object prevalentSystem, Date ignored) {

((NumberKeeper)prevalentSystem).keep(_numberToKeep);

}

}

Jak widać, wybranym serwisem transakcyjnym w naszym przykładzie jest Transaction, który zawiera jedną metodę

public void executeOn(Object prevalentSystem, Date executionTime)

W niej zmieniamy stan naszego obiektu, w tym przypadku korzenia wzbogacając listę liczb pierwszych o kolejny element. Transakcja zapisywana jest na dysk, więc po ponownym uruchomieniu aplikacji, wyszukiwanie zacznie się od momentu na którym skończyliśmy. Nie musimy pisać żadnego dodatkowego kodu do zapisu/odczytu danych. O to wszystko dba prevayler.

Zachęcam do eksperymentów z biblioteką.

Materiały źródłowe:

* – www.prevayler.org/wiki

** – „Horizontal Decomposition of Prevayler” I.Godil, H. Jacobsen

*** przykład jest również dostarczany wraz z biblioteką (folder demos)


Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

2 komentarzy do “Prevayler, czyli jak sobie radzić bez relacyjnej bazy danych…

  • smentek

    „przekazuj identyfikatory zamiast obiektów, a następnie je wyszukuj, prevayler jest tak szybki, że nie wpłynie to na czas wykonania komendy”

    Czy należy przez to rozumieć, że mając dwa obiekty Firma i Pracownik. To nie jest dopuszczalna relacja: Firma zawiera Pracownika (kompozycja). Czy oba te obiekty powinny mieć unikalne identyfikator a obiekt Firma zawierać zamiast referencji do obiektu Pracownik identyfikator pracownika?

    • Adam Andrzejewski Autor wpisu

      Taka relacja jest jak najbardziej dopuszczalna. Chodzi o to, by do komendy zapisującej zmiany na obiekcie nie przekazywać całych obiektów tylko ich identyfikatory – unika się wtedy problemu baptyzmu, a same komendy zajmują dużo mniej miejsca na dysku. Oba obiekty powinny mieć unikalne identyfikatory.