Home ORDIX AG             Dienstleistung             Trainingsshop    Kunden / Referenzen Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  3/2005
suche: 
Dieser Artikel richtet sich an Java Entwickler, die den JBoss Cache in ihrer Java-Anwendung in Kombination mit JBoss als "Standalone" oder im Cluster nutzen wollen.

Glossar

Hashmap

Datenstruktur, die Schlüssel auf Werte abbildet. Bei den Schlüsseln wird hierfür der Hashwert ermittelt.

NONE

Isolationslevel des TreeCache. Bei diesem Level werden Transaktionen nicht unterstützt. Der Entwickler muss sich selbst programmtechnisch um Datenkonsistenz kümmern.

READ_UNCOMMITED

Isolationslevel des TreeCache. Bei diesem Level können Lesezugriffe immer stattfinden, während Schreibzugriffe exklusiv sind. Es sind jedoch "dirty reads" möglich, wenn innerhalb einer Transaktion T1 ein Datum X gelesen wird, das vorher innerhalb einer Transaktion T2 geändert, jedoch die Änderung noch nicht bestätigt (commit) wurde.

REPEATABLE_READ

Defaulteinstellung für den Isolationslevel des TreeCache. Bei diesem Level sind Lesezugriffe nur möglich, solange es keinen noch nicht abgeschlossenen Schreibzugriff gibt und umgekehrt. Das Problem der "non-repeatable reads" ist ausgeschlossen, jedoch sind sogenannte "phantom reads" möglich. D. h. beim wiederholten Auslesen einer Reihe von Datensätzen unter Nutzung gleicher Selektionskriterien (where-Klausel) innerhalb einer Transaktion T1 können Datensätze auftauchen, die innerhalb einer Transaktion T2 zwischenzeitlich neu angelegt wurden.

SERIALIZABLE

Isolationslevel des TreeCache. Bei diesem Level werden Zugriffe mit exklusiven Sperren derart synchronisiert, dass "phantom reads" nicht möglich sind. Die mit einer Transaktion assoziierten Sperren werden erst am Ende der Transaktion wieder freigegeben.

JBoss Cache

Caches sind in der Informationsverarbeitung eine wohlbekannte Technik, um die Performance zu erhöhen. So fnden sich in einer Client/Server-Architektur Caches auf den diversen Schichten. Von der Datenbank über den Server bis hin zu den Clients. Auch alle J2EE Application Server benutzen einen Cache - und JBoss macht da keine Ausnahme.

Der vom JBoss verwendete Cache trägt den naheliegenden Namen JBoss Cache. Dahinter verbergen sich zwei Implementierungen mit den Namen "TreeCache" und "TreeCacheAOP", wobei letztere eine Ableitung von "TreeCache" ist. Beide können, da sie in einem eigenständigen Projekt entwickelt werden, auch unabhängig von JBoss eingesetzt werden.

TreeCache Architektur
Abb. 1: TreeCache Architektur..

Dieser Artikel geht auf die technischen Details der TreeCache-Implementierung ein und stellt deren wichtigste Eigenschaften sowohl bei einem lokalen als auch bei einem Einsatz in einem JBoss Cluster vor.

Überblick

Einem Cache liegt immer eine Datenstruktur zugrunde, in der die verwalteten Daten abgelegt werden. Der TreeCache verwendet, wie sein Name schon sagt, einen Baum, der beispielhaft in Abbildung 1 zu sehen ist. In dem Baum besitzt jeder Baumknoten eine eigene "Hashmap", in der die eigentlichen Daten abgelegt werden.

Bei der Datenstruktur handelt es sich also um eine Verschmelzung eines Baums mit mehreren Hashmaps. Entsprechend ist auch die API für den Zugriff auf den Cache aufgebaut (siehe Abbildung 2). Allgemein betrachtet muss bei allen Datenoperationen immer der Baumknoten über einen Pfad identifziert werden. So erwartet z. B. die put-Methode drei Parameter.

// Methoden, um Daten einzufügen
put(Fqn node, Object key, Object key)
put(String node, Object key, Object key)
put(Fqn node, Hashmap values)
put(String node, Hashmap values)

// Methoden, um Daten abzufragen
get(Fqn node)
get(String node)
get(Fqn node, Object key)
get(String node, Object key)

// Methoden, um Daten zu entfernen
remove(Fqn node, Object key)
remove(String node, Object key)
remove(Fqn node)
remove(String node)
removeData(String node)

Abb. 2: API des TreeCache..

Der erste Parameter stellt den Pfad zum Baumknoten dar, der zweite und dritte Parameter sind der Schlüssel und der Wert für die "Hashmap". Existiert beim Einfügen im Baum kein solcher Pfad, so wird dieser vom TreeCache automatisch erzeugt.

Pfade

Der Pfad ist entweder ein durch "/"-separierter String oder ein Objekt vom Typ fqn. Bei einem separierten String identifziert jeder Substring zwischen den "/" einen Baumknoten. Der Pfad "/a/b/c" besteht also aus den drei Baumknoten a, b, und c. Wobei a der Elternknoten von b und b der Elternknoten von c ist.

Man ist aber nicht auf die Nutzung von Strings angewiesen. Durch die Verwendung von fqn-Objekten lassen sich alle Objekte als Identifkatoren nutzen, die die Methoden hashCode() und equals() implementieren. Der Konstruktor der Klasse Fqn erwartet als Parameter ein Object-Array, dessen Elemente die Identifkatoren der einzelnen Baumknoten sind. Intern werden alle "/"-separierten Strings in fqn-Objekte transformiert. So erzeugt der Sourcecode in Abbildung 3 zwischenzeitlich den in Abbildung 4 gezeigten Baum.

TreeCache tree = new TreeCache();
...
tree.put("/ordix/adresse", "strasse", "Westernmauer 12");
tree.put("/ordix/adresse", "plz", "33098");
tree.put("/ordix/adresse", "tel", "05251/10630"));
tree.put("/ordix/adresse", "www", "www.ordix.de");
tree.put(
    new Fqn(new Object[] {"ordix", "bereiche"}),
    new Hashmap());
String tel=(String)tree.get("/ordix/adresse", "tel");
// Vollständige Adresse aus dem Cache löschen
// tree.remove("ordix/adresse");
...

Abb. 3: Beispiel einer direkten Nutzung der TreeCache API..

TreeCache gemäß Sourcecode aus Abb. 3
Abb. 4: TreeCache gemäß Sourcecode aus Abb. 3.

Eigenschaften

Um den Cache im Cluster vernünftig nutzen zu können, braucht es jedoch etwas mehr als eine einfache API zum Einfügen und Entfernen von Daten. Aus diesem Grund lässt sich der TreeCache darüber hinaus zu einem replizierenden, transaktionalen, synchron oder asynchron arbeitenden Cache konfgurieren. Das geschieht entweder über die API oder üblicherweise über eine XML-Konfgurationsdatei.

Replikation

Der Cache kann als lokaler oder replizierender Cache konfguriert werden. Lokale Caches arbeiten nicht im Cluster und replizieren ihre Änderungen nicht zu anderen Cluster-Knoten. Replizierende Caches replizieren dagegen ihre Änderungen mit allen anderen Cluster-Knoten.

Da die Cluster-Knoten innerhalb unterschiedlicher Java Virtual Machines (JVM) laufen, müssen alle von einem replizierenden Cache verwalteten Daten serialisierbar sein. Die Replikation kann dabei bei jeder Änderung stattfnden (keine Transaktion) oder erst beim commit einer Transaktion. Je nach Konfguration kann die Replikation synchron oder asynchron stattfnden.

Synchrone Replikation

Synchrone Replikation blockt den Aufrufer so lange, bis die Änderung in allen zusammenarbeitenden Caches repliziert wurde. Das kann unter Umständen sehr lange dauern, hat jedoch den Vorteil, dass der Aufrufer am Ende weiß, dass die Änderung clusterweit durchgeführt wurde.

Asynchrone Replikation

Bei einer asynchronen Replikation wird der Aufrufer nicht geblockt, da die Replikation im Hintergrund durchgeführt wird. Der Aufrufer bekommt nicht mit, wenn aus irgendwelchen Gründen die Replikation nicht durchgeführt werden konnte. In diesem Fall wird eine Fehlermeldung ins Log geschrieben.

Transaktionen

Wie bereits oben beschrieben, kann der Cache so konfguriert werden, dass Änderungen erst nach einem commit zu anderen Cluster-Knoten repliziert werden. Dazu sind zwei Voraussetzungen nötig:

Im Falle eines commit werden die innerhalb der Transaktion geänderten Daten zu anderen Cluster-Knoten repliziert.

Bei einem rollback muss der Cache die bis dahin innerhalb der Transaktion durchgeführten, lokalen Änderungen rückgängig machen.

Sperren und Isolationslevel

Da auf Daten innerhalb eines Caches mehrere Zugriffe gleichzeitig stattfnden können, gibt es die Möglichkeit, Daten zu sperren. Die Zugriffe können dabei innerhalb einer Transaktion oder alleinstehend ohne Transaktionskontext durchgeführt werden.

In beiden Fällen werden solche Zugriffe auf eine Instanz vom Typ GlobalTransaction abgebildet, die clusterweit eine eindeutige ID besitzt.

Der Cache unterstützt momentan nur pessimistische Sperren auf Baumknotenebene, die zur Laufzeit vom Cache verwaltet werden und nur dem Cache bekannt sind. Unterschiedliches Sperrverhalten lässt sich durch die Konfguration der Isolationslevel realisieren.

Die Isolationslevel des Caches sind die gleichen, die JDBC für Transaktionen defniert:

Der Isolationslevel REPEATABLE_READ ist dabei die Defaulteinstellung.

Schreib- und Lesesperren

Im Cache wird genau wie bei Datenbanksystemen zwischen Schreib- und Lesesperren unterschieden. Eine Schreibsperre serialisiert alle anderen Schreib- und Lesezugriffe. Eine Lesesperre serialisiert alle anderen Schreibsperren.

Existiert auf einem Baumknoten bereits eine Schreibsperre, so kann dieser Baumknoten von keiner anderen GlobalTransaction mit einer weiteren Schreib- oder Lesesperre belegt werden. Bei einer Lesesperre sind nur weitere Lesesperren zulässig. Damit ein Baumknoten mit einer Schreibsperre belegt werden kann, müssen zuvor alle anderen Schreib- und Lesesperren aufgehoben sein.

Aufräumen

Der Cache besitzt auch ein Aufräumkonzept, welches dafür sorgt, dass Knoten innerhalb des Caches abhängig von bestimmten Kriterien (z. B. Alter von Knoten oder Cache-Größe) automatisch entfernt werden. Die Strategien, welche Knoten wirklich entfernt werden, sind in sogenannte eviction policy Implementierungen ausgelagert.

Diese Implementierungen sind auf Basis des Observable Entwurfsmusters an den Cache gekoppelt und implementieren das Interface org.jboss.cache.TreeCacheListener. Über dieses Interface wird die eviction policy über neu hinzugefügte Knoten usw. informiert. Für die eviction policy gelten bestimmte Regeln.

Regeln für die eviction policy

Die Default-Implementierung ist die "LRU eviction policy", die die Least-Recently-Used-Strategie implementiert. Sie besitzt die folgenden Konfgurationsparameter:

Konfgurationsparameter der LRU eviction policy

Daten laden

Daten können nicht nur über die API (siehe Abbildung 2) in den Cache gelangen. Per XML-Konfgurationsdatei lassen sich sogenannte "CacheLoader" konfgurieren, über die der Cache Daten aus einer Datenquelle laden kann. Wenn ein CacheLoader konfguriert ist, stehen folgende Möglichkeiten zur Verfügung:


<!-- ==================================================================== -->
<!-- Defnes TreeCache confguration -->
<!-- ==================================================================== -->

<mbean code="org.jboss.cache.TreeCache" name="jboss.cache:service=TreeCache">
    <attribute name="CacheLoaderClass">
        org.jboss.cache.loader.bdbje.BdbjeCacheLoader
    </attribute>
    <attribute name="CacheLoaderConfg">location=c:\\tmp\\bdbje</attribute>
    <attribute name="CacheLoaderShared">true</attribute>
    <attribute name="CacheLoaderPreload">/</attribute>
    <attribute name="CacheLoaderFetchTransientState">false</attribute>
    <attribute name="CacheLoaderFetchPersistentState">true</attribute>
</mbean>

Abb. 5: XML-Konfguration des CacheLoaders.

Welche CacheLoader-Implementierung zusammen mit dem Cache benutzt wird, wird in der JBossCache XML-Konfgurationsdatei festgelegt (siehe Abbildung 5). Folgende Implementierungen stehen zur Verfügung:

Darüber hinaus besitzt ein CacheLoader z. B. die interessante Möglichkeit, beim Starten bestimmte Daten initial in den Cache zu laden. Was geladen wird, wird in der JBossCache XML-Konfgurationsdatei über das Attribut CacheLoaderPreload defniert.

Fazit

Dieser Artikel stellt nur die wichtigsten Eckdaten des JBoss Cache dar. Wer den JBoss im Cluster einsetzt, für seine Java-Anwendung eine Cache-Implementierung sucht oder einfach mal Lust hat, sich die Implementierung eines Caches anzusehen, der sollte sich genauer mit diesem Produkt der JBoss Inc. befassen. Es lohnt sich.

Christoph Borowski (info@ordix.de).