
|
EHCache Easy Hibernate Cache. Cache Provider für den Second Level Cache. |
|
Log4j Framework zum Loggen von Anwendungsmeldungen in Java. Es wird von unzähligen Open-Source- als auch kommerziellen Softwareprodukten verwendet und hat sich als De-facto-Standard etabliert. |
|
SLF4J Simple Logging Facade for Java. Das SLF4J führt alle klassischen Logging-Frameworks wie z. B. Log4j oder java.util.logging zusammen. |
|
|||||||||||||||||||||||||
| Abb.1: Auflistung der gängigsten Cache Provider. | |||||||||||||||||||||||||
|
|||||||||||||||||||||||||
| Abb. 2: Unterstützte Caching-Strategien | |||||||||||||||||||||||||
|
|||||||||||||||||||||||||
| Abb. 3: Log-Kategorien. | |||||||||||||||||||||||||
1 instantiating cache region: Student usage strategy: read-only 2 Thread-0 entering run() in Thread 8 3 Thread-1 entering run() in Thread 9 4 Thread-0 checking cached query results 5 Thread-0 query results were not found in cache 6 Thread-0 select "Spaltennamen" where NAME like 'Larry' 7 Thread-0 Caching: Student#1 8 Thread-0 Caching: Student#2 9 Thread-0 Caching: Student#3 10 Thread-0 caching query results; timestamp=4939808663855104 11 Thread-0 Durchlauf: 1 Zeit: 218 12 Thread-0 checking cached query results 13 Thread-0 Checking query spaces for up-to-dateness: [STUDENT] 14 Thread-0 returning cached query results 15 Thread-0 Cache hit: Student#1 16 Thread-0 Cache hit: Student#2 17 Thread-0 Cache hit: Student#3 |
|||||||||||||||||||||||||
| Abb. 4: Logging-Ausgabe: Read-only. | |||||||||||||||||||||||||
1 instantiating cache region: Student usage strategy: read-write 2 Thread-1 entering run() in Thread 8 3 Thread-0 entering run() in Thread 9 4 Thread-0 checking cached query results 5 Thread-0 query results were not found in cache 6 Thread-0 select "Spaltennamen" from STUDENT where NAME like 'Larry' 7 Thread-0 Caching: Student#1 8 Thread-0 Cached: Student#1 9 Thread-0 Caching: Student#2 10 Thread-0 Cached: Student#2 11 Thread-0 Caching: Student#3 12 Thread-0 Cached: Student#3 13 Thread-0 Caching query results; timestamp=4939807278854144 14 Thread-0 Durchlauf: 1 Zeit: 219 15 Thread-0 checking cached query results 16 Thread-0 Checking query spaces for up-to-dateness: [STUDENT] 17 Thread-0 returning cached query results 18 Thread-0 Cache lookup: Student#1 19 Thread-0 Cache hit: Student#1 20 Thread-0 Cache lookup: Student#2 21 Thread-0 Cache hit: Student#2 22 Thread-0 Cache lookup: Student#3 23 Thread-0 Cache hit: Student#3 24 Thread-0 Durchlauf: 2 Zeit: 16 25 Thread-0 exiting run() in Thread 9 26 Thread-1 checking cached query results 27 Thread-1 Checking query spaces for up-to-dateness: [STUDENT] 28 Thread-1 returning cached query results 29 Thread-1 Cache lookup: Student#1 30 Thread-1 Cached item was locked: Student#1 31 Thread-1 select "Spaltennamen" from STUDENT where MATRIKELNUMMER=? 32 Thread-1 Caching: Student#1 33 Thread-1 Item was already cached: Student#1 34 Thread-1 Cache lookup: Student#2 35 Thread-1 Cached item was locked: Student#2 36 Thread-1 "Spaltennamen" from STUDENT where MATRIKELNUMMER=? 37 Thread-1 Caching: Student#2 38 Thread-1 Item was already cached: Student#2 39 Thread-1 Cache lookup: Student#3 40 Thread-1 Cached item was locked: Student#3 41 Thread-1 select "Spaltennamen" from STUDENT where MATRIKELNUMMER=? 42 Thread-1 Caching: Student#3 43 Thread-1 Item was already cached: Student#3 44 Thread-1 Durchlauf: 1 Zeit: 250 45 Thread-1 checking cached query results 46 Thread-1 Checking query spaces for up-to-dateness:[STUDENT] 47 Thread-1 returning cached query results 48 Thread-1 Cache lookup: Student#1 49 Thread-1 Cache hit: Student#1 50 Thread-1 Cache lookup: Student#2 51 Thread-1 Cache hit: Student#2 52 Thread-1 Cache lookup: Student#3 53 Thread-1 Cache hit: Student#3 54 Thread-1 Durchlauf: 2 Zeit: 0 55 Thread-1 exiting run() in Thread 8 |
|||||||||||||||||||||||||
| Abb. 5: Logging-Ausgabe: Read-write. | |||||||||||||||||||||||||
|
|||||||||||||||||||||||||
| Abb. 6: Transaktionales Verhalten. | |||||||||||||||||||||||||
Im ersten Artikel der Reihe [1] wurde bereits näher auf das Cache-System von Hibernate und dessen Auswirkungen eingegangen. Es empfiehlt sich, diesen Artikel vorab zu verinnerlichen.
Der Second Level Cache kann auf der Ebene eines Prozesses bzw. einer Virtual Machine oder auf der Ebene eines Clusters eingesetzt werden. Bei der ersten Möglichkeit werden alle Objekte, die in einer Virtual Machine gespeichert oder geladen werden, gecacht. Werden beispielsweise von mehreren Studenten Informationen über ein Buch abgefragt, dann muss das Buch nicht für jeden Studenten neu geladen werden, sondern kann direkt aus dem Second Level Cache bezogen werden.
Die Steuerung des Second Level Cache erfolgt über einen Cache Provider. Die gängigsten Cache Provider sind in Abbildung 1 aufgelistet.
Weiterhin kann optional für das Mapping einer Klasse oder Collection die Caching-Strategie festgelegt werden. Es wird zwischen folgenden Strategien unterschieden:
Read-only - nur lesen bitte
Bei dieser Strategie wird davon ausgegangen, dass die gelesenen Daten nie geändert
werden. Es findet keine Transaktionsisolation
statt. Wird ein Objekt trotzdem verändert, wird eine Exception ausgeführt.
Nonstrict-read-write
Bei dieser Strategie können die Daten geändert werden.
Es gibt jedoch keine Garantie für die Konsistenz zwischen der Datenbank und dem Cache.
Es könnte passieren, dass bei zeitgleichem Zugriff veraltete Daten aus dem Cache gelesen werden.
Diese Strategie sollte nur bei Daten angewendet werden, die sich selten
ändern und von ihrer Bedeutung nicht so wichtig sind.
Read-write
Die Daten können geändert werden. Der Unterschied zur nonstrict-read-write-Strategie besteht darin,
dass die Transaktionsisolation auf der Ebene Read Commited zugesichert wird. Dabei werden nur die Daten gesehen,
die bereits commited sind. Datensätze aus nicht beendeten Transaktionen werden nicht beachtet. Die Lösung
kann eingesetzt werden, wenn Daten oft gelesen werden. Darüber hinaus ist die Lösung dort sinnvoll, wo es
wichtig ist, zu verhindern, dass in zeitgleich ablaufenden Transaktionen veraltete Daten sichtbar werden.
Diese Strategie ist nicht in einem Cluster anwendbar.
Transactional - das Rundum-Sicherheitssystem
Hierbei wird die Transaktionsisolation auf der Ebene Repeatable Read zugesichert.
Das bedeutet, dass laufende Transaktionen keine Daten ändern können, die von nicht
beendeten Transaktionen gelesen werden. Diese Strategie sollte bei Daten eingesetzt werden,
die oft gelesen werden und deren Bedeutung sehr wichtig ist (z. B. Kontostand).
In Abbildung 2 ist eine Übersicht über die unterstützten Caching-Strategien der vorgestellten Caching-Provider dargestellt.
Um die Auswirkungen der Caching-Strategien des EHCache mit aktiviertem Query Cache zu untersuchen, wurde folgendes Szenario entwickelt:
In diesem Szenario werden zwei Threads erstellt, die zeitgleich dieselbe Abfrage ausführen. Aus diesem Grund konnte die exakte Laufzeit nicht ermittelt werden.
Stattdessen wurde mit Hilfe von Log4j der Verkehr mitgeschnitten und ausgewertet. Um Systemabläufe zu loggen, benutzt Hibernate das Simple Logging Facade for Java (SLF4J) [2]. Dabei wurde viel Wert darauf gelegt, dass die Logging-Ausgaben detailliert, aber trotzdem noch gut lesbar sind. Abbildung 3 zeigt einen Ausschnitt der zur Verfügung stehenden Log-Kategorien.
Sehr empfehlenswert ist auch die Statistik API, die es ermöglicht, das Verhalten von Hibernate zur Laufzeit zu beobachten [3]. Die zur Verfügung stehenden Möglichkeiten werden in einem zukünftigen ORDIX News Artikel beleuchtet.
Beide Mechanismen sind bei dem Test zum Einsatz gekommen.
Read-only
Wie in Abbildung 4 dargestellt, werden Thread 0 und Thread 1 nahezu gleichzeitig gestartet. Thread 0 überprüft
beim ersten Durchlauf den Query Cache und sucht nach dem Select-Statement. Der Query Cache ist jedoch leer.
Daraufhin führt Thread 0 eine Datenbankabfrage durch und selektiert alle Studenten mit dem Namen "Larry".
Dabei werden der First Level Cache, der Second Level Cache und der Query Cache gefüllt. Der Query Cache
beinhaltet nun das Select-Statement und die Identifikatoren der Ergebnismenge. Weiterhin wird der Zeitstempel gesetzt.
Im zweiten Durchlauf von Thread 0, wird wieder der Query Cache und dessen Timestamp geprüft (siehe Zeile 12). Es wurde keine Änderung der Tabelle vorgenommen und somit ist der Datensatz aktuell. Das Select-Statement und die dazugehörigen Identifikatoren der Ergebnismenge wurden gefunden. Im nächsten Schritt wird im Second Level Cache nach den Identifikatoren gesucht. Die Suche ist erfolgreich und es findet ein Cache Hit statt, weil bereits m ersten Durchlauf die Objekte in dem Second Level Cache gespeichert wurden. Die beiden Durchläufe von Thread 1 verfahren nach dem gleichen Prinzip wie der zweite Durchlauf von Thread 0.
Nonstrict-read-write
Die nonstrict-read-write-Strategie verfolgt den gleichen Ansatz wie die read-only-Strategie.
Read-write
Abbildung 5 stellt die Logging-Ausgabe bei der read-write-Strategie dar.
Die beiden Durchläufe von Thread 0 verfolgen wieder den gleichen Ansatz
wie die beiden vorherigen Strategien (siehe Zeile 1 bis 25). Thread 1 reagiert hingegen anders.
Beim ersten Durchlauf von Thread 1 wird der Query Cache erfolgreich durchsucht. Dann wird der
Second Level Cache nach den Identifikatoren durchsucht. Die Suche ist jedoch erfolglos, weil
Thread 1 nicht auf den Second Level Cache zugreifen kann (siehe Zeile 30).
Thread 0 hat noch kein commit durchgeführt und blockiert somit den Second Level Cache (siehe Abbildung 6). Daraufhin startet Thread 1 eine Datenbankabfrage, die anhand der Matrikelnummer des Studenten alle Spalten selektiert (siehe Zeile 31). Danach versucht Thread 1, die Objekte im Cache abzulegen. Die Objekte sind bereits im Cache vorhanden und werden nicht überschrieben.
In Zeile 45 bis 55 wird der zweite Durchlauf von Thread 1 beschrieben. Die Objekte werden wieder aus dem Second Level Cache geladen, weil Thread 0 committed hat.
Transactional
Die Caching-Strategie Transactional wird von dem EHCache Provider nicht unterstützt.
Generell lässt sich sagen, dass sich die Performance von transactional bis read-only stetig verbessert. Zwischen read-only und nonstrict-read-write konnte allerdings anhand der Logging-Ausgaben kein Performance-Vorteil ermittelt werden. Die beiden Strategien verfolgen über weite Strecken den gleichen Ansatz. Ein Unterschied besteht darin, dass bei read-only die Tabelle nicht modifiziert werden darf. Auch muss Hibernate (bei einem Update) nicht überprüfen, ob ein Cache noch gültig ist. Read-write benötigt bei der gleichen Abfrage und dem zeitgleichen Zugriff mehr Zeit, weil es passieren kann, dass der Second Level Cache blockiert wird und die Daten somit wieder aus der Datenbank gelesen werden müssen. Negativ fällt hier auch auf, dass er dies mit einem Einzelsatz-Zugriff macht, was bei einer entsprechenden Menge von Sätzen performance-kritisch werden kann.
Ehe die Entscheidung für eine Caching-Strategie fällt, sollte man sich die Frage stellen, ob die Daten transaktionssicher sein sollen oder nicht und sich anschließend für eine der eben vorgestellten Strategien entscheiden.
Für weitere Informationen besuchen Sie unser Seminar "Hibernate und die Java Persistence API" oder sprechen Sie uns an!
Alexander Keil (info@ordix.de).