
| 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. |
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.
|
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.
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.
|
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.
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(); |
![]() |
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.
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 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.
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.
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.
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.
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.
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.
Die Default-Implementierung ist die "LRU eviction policy", die die Least-Recently-Used-Strategie implementiert. Sie besitzt die folgenden Konfgurationsparameter:
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:
|
<!-- ==================================================================== --> |
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.
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).