J2EE Persistenzmanagement Architekturen

Werbung
Klassifizierung und Bewertung von
Persistenz-Management Technologien in
J2EE Architekturen
unter besonderer Betrachtung von
Skalierbarkeit und Ausfallsicherheit
Thomas Hertz
Diplomarbeit
Technische Universität München
Fakultät für Informatik
Aufgabensteller: Prof. Dr. rer. nat. Florian Matthes
Betreuer: Dipl. Inf. Thomas Büchner
15. Mai 2003
2
ERKLÄRUNG
Ich versichere, dass ich diese Diplomarbeit selbständig verfasst und nur die angegebenen
Quellen und Hilfsmittel verwendet habe.
München, Mai 2003....................................................................................................Thomas Hertz
3
„All efforts to replace N technologies by 1 new technology
usually end up with N plus 1 technologies.”
OMG EAI/Workshop 2000
4
INHALTSVERZEICHNIS
Erklärung ............................................................................................................................................ 3
Inhaltsverzeichnis.............................................................................................................................. 5
Abbildungs- und Tabellenverzeichnis ............................................................................................ 7
1
2
3
4
5
Einleitung ................................................................................................................................... 9
1.1
Motivation........................................................................................................................ 9
1.2
Einordnung und Vorgehensweise .............................................................................. 10
1.3
Gliederung ..................................................................................................................... 11
1.4
Danksagung ................................................................................................................... 12
Aufbau komplexer Client/Server Anwendungen............................................................... 13
2.1
Übersicht und Anforderungen.................................................................................... 13
2.2
Die Mehrschichten-Architektur als mögliche Lösung ............................................ 14
2.3
Ausfallsicherheit............................................................................................................ 16
2.4
Lastverteilung zur Erreichung von Ausfallsicherheit .............................................. 17
Datenbanksysteme .................................................................................................................. 21
3.1
Übersicht........................................................................................................................ 21
3.2
Die wichtigsten Zugriffsschnittstellen....................................................................... 22
3.3
Transaktionen................................................................................................................ 25
3.4
Praxisbeispiel ................................................................................................................. 28
Java 2 Enterprise Edition....................................................................................................... 29
4.1
Übersicht........................................................................................................................ 29
4.2
Enterprise JavaBeans.................................................................................................... 30
4.3
Die EJB-Persistenz-Schnittstellen.............................................................................. 33
4.4
Transaktionen in EJB-Architekturen......................................................................... 40
O/R-Mapping.......................................................................................................................... 45
5.1
Übersicht und Gliederung........................................................................................... 45
5.2
Datenhaltung................................................................................................................. 45
5.3
Datenzugriff .................................................................................................................. 52
5.4
Caching........................................................................................................................... 59
5
6
CCMP – Cached CMP ........................................................................................................... 75
7
6.1
DCache .......................................................................................................................... 76
6.2
CMP2BMP .................................................................................................................... 87
6.3
J2EEDemo.................................................................................................................... 94
6.4
Performance .................................................................................................................. 96
Bewertung und Ausblick........................................................................................................ 99
Anhang 1: Nutzung des Prototyps ............................................................................................. 103
Allgemein ............................................................................................................................... 103
DCache................................................................................................................................... 108
CMP2BMP............................................................................................................................. 112
J2EEDemo ............................................................................................................................ 114
Literaturverzeichnis....................................................................................................................... 119
6
ABBILDUNGS- UND
TABELLENVERZEICHNIS
Abbildung 1: Strukturierung dieser Arbeit ..................................................................................10
Abbildung 2: Aufbau eines komplexen Client/Server Systems................................................15
Abbildung 3: Verteiltes Relationales Datenbanksystem (RDBMS) .........................................22
Abbildung 4: EJB-Architektur.......................................................................................................31
Abbildung 5: Definition eines Enterprise JavaBeans.................................................................35
Abbildung 6: Anwendung des Serialized LOB Patterns............................................................48
Abbildung 7: Anwendung des Single Inheritance Patterns.......................................................49
Abbildung 8: Brute-Force Datenzugriff nach [Amb03]..............................................................53
Abbildung 9: Datenzugriff mittels Data Access Objects (DAO).............................................54
Abbildung 10: Trennung von logischer und physikalischer Schicht beim Datenzugriff ......55
Abbildung 11: Nutzung eines Persistenzframeworks ................................................................55
Abbildung 12: Synchronisation in verteilten Caches..................................................................62
Abbildung 13: Zugriffszeit nach Ort der Datenhaltung ............................................................66
Abbildung 14: Wege durch den Intra- und Inter-Transaction Cache......................................71
Abbildung 15: Automatisches Auffinden von Abhängigkeiten................................................72
Abbildung 16: Die einzelnen Komponenten des CCMP-Frameworks...................................76
Abbildung 17: Architekturübersicht DCache..............................................................................77
Abbildung 18: DCache-Baumstruktur für das Cachen von Entity-Beans ..............................79
Abbildung 19: Sequenzdiagramm eines Cachezugriffs ..............................................................80
Abbildung 20: Architekturübersicht des JDBCWrappers .........................................................86
Abbildung 21: Zwei Wege zur Implementierung einer eigenen EJB-Persistenzschicht.......88
Abbildung 22: Mögliche Einsprungpunkte für Cachesysteme .................................................90
Abbildung 23: Ablauf der CMP zu BMP Konvertierung..........................................................91
Abbildung 24: Architekturübersicht BMP2CMP-Framework..................................................93
Abbildung 25: Architekturübersicht J2EEDemo .......................................................................95
Abbildung 26: Performancevergleich CMP / CCMP ................................................................98
7
Tabelle 1: Verfügbarkeit von Systemen nach [GS91]................................................................. 17
Tabelle 2: Isolationslevel bei Transaktionen ............................................................................... 27
Tabelle 3: Einfaches O/R-Mapping nach [CK96]...................................................................... 46
Tabelle 4: Benötigte Metadaten zum Zugriff über ein Persistenzframework ........................ 56
Tabelle 5: Beispielhaftes Typ-Mapping von Java zu SQL......................................................... 58
Tabelle 6: Gegenüberstellung EJB und CRUD-Pattern............................................................ 88
Tabelle 7: Konfiguration des Microsoft Web Application Stress Tool................................... 97
Tabelle 8: Verzeichnisstruktur des Prototyps ........................................................................... 105
Tabelle 9: Verzeichnisstruktur DCache ..................................................................................... 109
Tabelle 10: Konfigurationsparameter des DCache-Frameworks........................................... 109
Tabelle 11: Verzeichnisstruktur des CMP2BMP-Frameworks............................................... 113
Tabelle 12: Konfigurationsparameter des CMP2BMP-Frameworks..................................... 113
Tabelle 13: Verzeichnisstruktur der J2EE-Demoapplikation................................................. 115
8
1 EINLEITUNG
1.1 Motivation
Die Nutzung von Applikationsservern im Rahmen geschäftlicher Anwendungsarchitekturen ist heutzutage nahezu zwingend. Durch sie wird eine Reihe von Technologien wie z.B.
Webserver, Transaktionsmonitor oder Messaging-System zu einem gut harmonierenden
und in sich schlüssigen Framework zusammengefügt. Neben der Bereitstellung einer
Umgebung für die Ausführung von Business-Logik realisieren sie auch die so wichtige
Verbindung zu Datenbanksystemen. Ziel der Applikationsserver ist es, die Entwicklung
eines modularen, ausfallsicheren und hochskalierbaren Systems zu ermöglichen.
Im Java-Umfeld setzt sich J2EE (Java 2 Enterprise Edition) [Sun01a] einschließlich der
Komponententechnologie EJB (Enterprise JavaBeans) [Sun01c] für Applikationsserver
immer mehr durch. EJB bietet einen Rahmen für die Entwicklung von BusinessFunktionalität und nimmt dem Entwickler immer wiederkehrende Aufgaben wie SecurityManagement, Transaktionssicherung oder Datenspeicherung ab.
Wie bereits angedeutet ist eine der Hauptaufgaben eines Applikationsservers die Anbindung aller Arten von Datenbanksystemen. Die J2EE-Spezifikation bietet auch hier einige
Hilfen für den Entwickler, ist aber leider in diesem Bereich teilweise nur sehr vage
formuliert oder adressiert wichtige Punkte gar nicht. Für die Speicherung der BusinessObjekte in einem relationalen Datenbanksystem stehen nur vergleichsweise einfache
Abbildungs- und Abfragemöglichkeiten zur Verfügung.
Die Integration bestehender Datenbanksysteme mit moderner Komponententechnik kann
somit von den heutigen Applikationsservern oft nicht ohne weitere Hilfsmittel geleistet
werden. Vor diesem Hintergrund sind Mechanismen notwendig, die eine flexible
Abbildung von Operationen und Anfragen der EJB-Objekte auf relationale Datenbanken
ermöglichen. Diese Aufgabenstellung wird üblicherweise durch Persistenz-Frameworks
erfüllt.
Die EJB-Spezifikation bietet zwei grundsätzliche Varianten für die Objekt-Persistenz an:
CMP (Container-Managed Persistence) [Sun01d, S. 125ff] und BMP (Bean-Managed
Persistence) [Sun01d, S. 243ff]. Doch muss für die Integration eines Persistenz-Frameworks
in den Applikationsserver eine detailliertere Betrachtung erfolgen, da zum einen keine
standardisierten Schnittstellen zwischen Applikationsserver und Persistenz-Framework
spezifiziert sind und zum anderen der Einsatz der so genannten Entity-Beans nicht bei
jedem Applikationsserver eine performante, ausfallsichere und hochskalierbare Architektur
gewährleistet.
Bei der Integration ist nicht nur die Frage zu lösen, wie die EJB-Technologie an das
Persistenz-Framework angebunden werden kann. Es stellt sich auch die Frage, ob und
wann ein Transaktionssystem nötig ist, wie dieses an den Applikationsserver angebunden
9
werden kann und ob das entstehende Gesamtsystem in der Praxis ausreichend viele
Transaktionen verarbeiten kann, um in großen verteilten Anwendungsarchitekturen
[Web98] einsetzbar zu sein.
1.2 Einordnung und Vorgehensweise
Abbildung 1 verdeutlicht die grobe Strukturierung dieser Arbeit. Der erste Teil widmet sich
den einzelnen Architektur-Schichten einer komplexen Client/Server-Architektur. Dies
beinhaltet eine Vorstellung der wichtigsten Persistenzmanagementschnittstellen der EJBArchitektur, der Auswahl von geeigneten Datenbanksystemen und Lösungen zur Erreichung von Skalierbarkeit und Ausfallsicherheit auf diesen Bereichen.
Kapitel 6
Client
Kapitel 2
Load-Balancer
Kapitel 4
CMP
CMP
BMP
Kapitel 5
CMPContainer
BMP
Generated
BMP
O/R-Mapping
JDBC-Treiber
Kapitel 3
DB
Abbildung 1: Strukturierung dieser Arbeit
Dies ist die Annäherung an den Kern dieser Arbeit, die genauere Betrachtung des
Persistenzmanagements, welches vor allem durch das sogenannte O/R-Mapping (ObjektRelationales Mapping) bestimmt wird. Es werden Architektur- und Entwurfsmuster
vorgestellt, mit deren Hilfe man den Zugriff auf relationale Daten von objektorientierten
Programmiersprachen und das intelligente Zwischenspeichern (engl. Caching) von häufig
benötigten Daten in einem verteilten System lösen kann. Es wird aufgezeigt, wie sich diese
Muster in J2EE-Systeme integrieren und damit unter der einheitlichen Programmierschnittstelle für Enterprise JavaBeans nutzbar machen lassen.
10
Nachdem dieser erste Teil der Diplomarbeit die horizontalen Architekturschichten näher
beleuchtet hat, wird im zweiten Teil der praktische Einsatz der erlangten Ergebnisse durch
den Entwurf eines Prototyps belegt. Dieser zeigt als eine Art ‚vertikaler Durchstoss’ die
beispielhafte Implementierung der vorgestellten Muster. Desweiteren werden anhand von
zwei Produktvorstellungen (Oracle Real Application Cluster und HP Traffic Director) in
den jeweiligen Kapiteln aufgezeigt, wie die Herausforderungen an komplexe skalierbare
und ausfallsichere Systeme im Bereich der Datenhaltung und der Lastverteilung in der
Praxis gelöst werden können. Den Abschluss bilden eine Bewertung der heutigen Technologien und Verfahren und ein Ausblick in die nähere Zukuft.
1.3 Gliederung
Kapitel 2, Aufbau komplexer Client/Server Anwendungen, stellt eine Einführung in
das Gebiet der Client/Server Anwendungen dar. Es werden die Anforderungen an komplexe und verteilte Mehrschichten-Systeme aufgezeigt. Auch wird die Bedeutung der
Lastverteilung für die Erfüllung der wichtigen Anforderungen Skalierbarkeit und Ausfallsicherheit anhand von praxisbezogenen Lösungen erwähnt und beschrieben. Es findet eine
Einordnung statt, in welche Bereiche diese Arbeit vordringen will und welche Gebiete
nicht oder nicht ausführlich behandelt werden.
In Kapitel 3, Datenbanksysteme, wird der Aufbau von Datenbanksystemen erklärt, es
wird der Unterschied zwischen objektorientierten und relationalen Datenbanksystemen
aufgezeigt und die Frage beantwortet, warum relationale Datenbanken immer noch die
erste Wahl bei der Verwaltung von großen Datenmengen sind. Nach einer Einführung in
die Theorie der Transaktionsverwaltung werden die wichtigsten Zugriffsschnittstellen
(SQL, JDBC, SQLJ) für die Benutzung in Java-Programmen vorgestellt. Anhand des
Produktes Oracle Real Application Cluster wird bestätigt, dass transparente, skalierbare und
ausfallsichere Datenbanksysteme auf dem Markt verfügbar sind.
Kapitel 4, Java 2 Enterprise Edition, geht nach einer grundsätzlichen Einführung der
Java 2 Enterprise Edition (J2EE) und der Komponententechnologie Enterprise JavaBeans
(EJB) auf die Persistenzschnittstellen CMP (Container Managed Persistence) und BMP
(Bean Managed Persistence) ein. Auch werden die Abfragesprache EJB QL (Enterprise
JavaBeans Query Language) und ihre Vor- und Nachteile ausführlich behandelt.
Abschließend wird aufgezeigt, welche Unterstützung J2EE im Bereich der
Transaktionsverwaltung durch CMT (Container Managed Transactions) anbietet und wie es
möglich ist, manuelle Transaktionssteuerung mittels BMT (Bean Managed Transactions) zu
betreiben.
Kapitel 5, O/R-Mapping, geht auf die Notwendigkeit ein, eine Schnittstelle zwischen der
objektorientierten Geschäftslogikschicht und der relationalen Datenbankschicht zu
benutzen. Es werden verschiedene Architektur- und Entwurfsmuster vorgestellt, die dieses
sogenannte Objekt-Relationale Mapping lösen können. Es wird im Detail über die
Notwendigkeit referiert, Daten in verteilten und skalierten Systemen oberhalb der
11
Datenbankschicht zwischenzuspeichern (zu cachen), und Lösungen hierzu aufgezeigt.
Auch die möglichen Folgen für die Transaktionssicherheit bei Verwendung von Caches
werden ausführlich erläutert.
Kapitel 6, CCMP – Cached CMP, beschreibt die genaue Architektur und den Aufbau des
im Rahmen dieser Diplomarbeit entworfenen und implementierten Prototyps mitsamt
seiner drei Komponenten
·
DCache, ein Framework für einen verteilten, transaktionssicheren Cache für
beliebige Javaobjekte,
·
CMP2BMP, ein Konverter, der aus CMP Entity-Beans BMP Entity-Beans erzeugt
und unter Nutzung der DCache-Komponente die Persistenzschicht selbst generiert,
·
J2EEDemo, eine J2EE-Beispielapplikation, an der das Zusammenspiel von DCache
und CMP2BMP demonstriert werden kann.
1
Die Beschreibungen des Prototyps werden durch UML -Klassen- und Sequenzdiagramme
weiter verdeutlicht.
Kapitel 7, Bewertung und Ausblick, fasst die im Zuge dieser Arbeit gewonnenen Erkenntnisse zusammen. Es wird aufgezeigt, welche Probleme bei der Anwendung der J2EEPlattform im Bereich Skalierbarkeit, Ausfallsicherheit und Performance zur Zeit noch
existieren und in welche Richtung die zukünftige Entwicklung geht oder gehen sollte, um
möglichst vielen der nötigen Anforderungen gerecht zu werden.
Anhang 1, Nutzung des Prototyps, dient als Handbuch für die Benutzung und Weiterentwicklung der einzelnen Prototyp-Komponenten DCache, CMP2BMP und J2EEDemo.
1.4 Danksagung
Ich danke allen, die mir bei dieser Arbeit geholfen haben: Herrn Prof. Dr. Matthes, der es
mir ermöglicht hat, über dieses interessante Thema zu schreiben, Thomas Büchner für
seine wertvollen Tipps, Martin, Andreas, Tom für das Korrekturlesen, Sabine für ihre
Fähigkeit, mich immer wieder auf andere Gedanken zu bringen, Jeanette für die seelische
Unterstützung, meinen Eltern und meiner Schwester für die Bereitstellung des sonnigen
Gartens, hybris für die Freizeit und R.E.M., deren Musik mich während der gesamten
Vorbereitungs- und Schreibarbeit begleitet hat.
1
UML ist eingetragenes Warenzeichen der Object Management Group (OMG). Für mehr Informationen
über die Unified Modelling Language siehe [OMG03].
12
2 AUFBAU KOMPLEXER CLIENT/SERVER
ANWENDUNGEN
2.1 Übersicht und Anforderungen
Ein Client/Server-System [Gei95] besteht aus zwei verschiedenen Arten von Rechnern Client-Rechnern und Server-Rechnern - die durch ein Netzwerk miteinander verbunden
sind. Die Client-Rechner sind Arbeitsstationen der einzelnen Anwender, während der
Server-Rechner die Geschäftslogik steuert und die Daten (Informationen, Dateien oder
Computerprogramme) hält, die für diese Arbeit benötigt werden.
Benutzer der Client-Rechner arbeiten mit einer Client-Software zur Abfrage der benötigten
Daten am Server. Dies ist in der Praxis bei webbasierten Anwendungen ein sogenannter
Web-Browser, der die Informationen darstellt. Die Server-Software, die auf dem ServerRechner ausgeführt wird, erhält die Datenabfragen und gibt daraufhin entsprechende
Ergebnisse an den Client-Rechner zurück. Typisch für solche Anwendungen ist, dass die
Initiative (der Aufruf des Dienstes) vom Client ausgeht, und ein Server von vielen Clients
aufgerufen werden kann und somit hohe Transaktionsraten auftreten können.
Voraussetzung für das Client/Server-Computing ist die optimale Interoperabilität zwischen
den verwendeten Systemen. Das setzt offene Architekturen voraus, die sich durch
standardkonforme Implementierungen bei den Schnittstellen und den Funktionen
abzeichnen.
Die Verteilung der Serverseite auf mehrere Rechner macht ein verteiltes, skaliertes System
aus. Dadurch wird außer der verteilten Präsentation auf mehreren Client-Rechnern auch
die Server-Software oder ein Teil von ihr dezentralisiert.
An die Entwicklung verteilter Client/Server Anwendungen werden eine Reihe von
Anforderungen gestellt:
·
Skalierbarkeit. Die Anwendung muss ohne großen Aufwand aufrüstbar sein um
auch für eine gesteigerte Last dem einzelnen Benutzer schnelle Antwortzeiten zu
bieten. Das System muss verteilbar auf mehrere physikalische Server sein, man
spricht hier von Verteilten Systemen (engl. Distributed Systems, [TS01])
·
Robustheit. Die Anwendung sollte 24 Stunden an 7 Tagen in der Woche
einsatzfähig sein, das bedeutet es soll keine oder eine möglichst geringe ungewollte
Ausfallzeit (engl. Downtime) geben. Es darf nicht zu inkonsistenten Zuständen
innerhalb der Applikation durch Fehlverhalten oder Absturz der Anwendung
kommen.
13
·
Portabilität. Die Anwendung sollte plattformunabhängig sein. Dies bedeutet, dass
sie ohne oder mit sehr wenigen Anpassungen auf verschiedenen Hardware- und
Betriebssystemplattformen eingesetzt werden kann. Die Programmiersprache Java
2
bietet beispielsweise eine hohe Portabilität. (Write Once, Run Anywhere )
·
Sicherheit. Die Sicherheit des Benutzers darf nicht beeinträchtigt werden. Es muss
Konzepte für die Vergabe von Rechten und Zugriffsbeschränkungen auf Teilbereiche der Anwendung geben. Es darf nicht möglich sein, ohne entsprechende
Autorisation an schützenswerte Informationen und Daten zu gelangen.
·
Wiederverwendbarkeit. Die Anwendung soll klare Schnittstellen aufweisen und
komponenten-basiert aufgebaut sein. Dadurch können einzelne Teile sehr einfach
in anderen Systemen wieder verwendet werden. Dies spart Entwicklungszeit und
damit Kosten.
·
Integrationsfähigkeit. Die Anwendung soll leicht auf externe Programme und
Datenquellen zugreifen können. Des Weiteren muss es möglich sein, die
Anwendung oder Teile von ihr in andere Fremdsysteme einzubetten. Hohe
Wiederverwendbarkeit fördert meist auch die Integrationsfähigkeit von Systemen.
·
Wartbarkeit. Änderungen der Anwendung müssen für Benutzer sofort verfügbar
3
gemacht werden können (z.B. Hot Deployment in J2EE-basierten Systemen).
Änderungen an der Konfiguration müssen schnell und sicher durchgeführt werden
können.
2.2 Die Mehrschichten-Architektur als mögliche Lösung
Anwendungen sind von den Anfängen des Internets bis heute durch ganz verschiedene
Entwicklungen und Techniken geprägt worden. Neben der so genannten Ein-SchichtArchitektur, z.B. einfaches statisches HTML, sind heutzutage vielfältige Lösungen mit
mehr als einer Architektur-Schicht im Internet zu finden. Zahlreiche serverseitige
Programmier- und Skriptsprachen bereichern die Anwendungsentwicklung. Der
Komplexitätsgrad der Anwendungen steigt hierbei zudem noch durch die Verwendung von
unterschiedlichen Hardware- und Softwarelösungen.
Um einigen der im letzten Kapitel vorgestellten und immer wichtiger werdenden
Anforderungen an komplexe Client/Server Anwendungen wie Skalierbarkeit, Robustheit,
Portabilität, Sicherheit und Integration genügen zu können, wechseln immer mehr Anbieter
von den typischen Ein- oder Zweischichtenarchitekturen auf Mehrschichtenarchitekturen
mit mindestens drei Ebenen.
2
3
‚Write Once, Run Anywhere’ ist ein Warenzeichen von Sun Microsystems Inc.
Feature verschiedener J2EE Server, bei dem bestimmte Verzeichnisse vom Server zur Laufzeit überwacht
und neue Applikationen automatisch erkannt und aktiviert werden.
14
Die mit diesem Wechsel verbundene stärkere Trennung von Präsentationslogik,
Geschäftslogik und Datenhaltungsschicht und die Festigung von Schnittstellen zwischen
den einzelnen Ebenen hat zu einer viel höheren Modularisierung und damit
Wiederverwendbarkeit geführt. Durch das Vorhandensein von klaren Schnittstellen können
sich Hersteller auf das Entwickeln einzelner Komponenten spezialisieren. Diese
Komponenten können leichter in andere Systeme integriert werden und in zukünftigen
Projekten mit wenigen Anpassungen erneut verwendet werden.
Dies macht deutlich, dass wichtige Anforderungen, die an die Entwicklung von verteilten
Client/Server Anwendungen gestellt werden, sich mit Hilfe von MehrschichtenArchitekturen lösen lassen.
Clients
Ausfallsicherheit /
Skalierbarkeit
Loadbalancer
Präsentation
Präsentation
Präsentation
Funktionalität
Funktionalität
Funktionalität
Ausfallsicherheit /
Skalierbarkeit
Persistenzmanagement
DB
DB
Abbildung 2: Aufbau eines komplexen Client/Server Systems
Der typische Aufbau einer solchen Mehrschichten-Architektur wird in Abbildung 2
vorgestellt. Es ist eine vorgelagerte Schicht zu sehen, die zwischen der Client- und der
Präsentationsschicht liegt. In dieser Schicht werden große Teile der wichtigen
Anforderungen Ausfallsicherheit und Skalierbarkeit durch so genannte Loadbalancer
(Lastverteilungssysteme) erfüllt. Dieser Vorgang wird unten detaillierter beschrieben.
15
2.3 Ausfallsicherheit
Die Begriffe Ausfallsicherheit oder Verfügbarkeit kann man als den Umfang bezeichnen, in
dem ein System gegen Störungen unempfindlich ist und korrekt nach seinen Vorgaben
arbeitet. Vor einer genaueren Definition fehlt noch eine Erläuterung, was unter einem
System zu verstehen ist, das ‚fehlerfrei’ oder ‚korrekt nach seinen Vorgaben’ arbeitet.
Hierbei ist nicht nur die physikalische Funktionstüchtigkeit zu verstehen, also die
Verfügbarkeit der Hardware und Netzwerkinfrastruktur, sondern auch die logische
Funktionstüchtigkeit. Ein System, welches für die Kunden zu langsam reagiert, also bei
bestehender Nutzerlast keine ausreichende Performance bietet, ist nicht verfügbar im Sinne
der Definition. Auch fallen logische Fehler im Programmablauf und undeterminiertes
Verhalten in diese Kategorie.
Zwei wichtige Begriffe im Zusammenhang mit Ausfallsicherheit sind die folgenden:
·
MTTF (mean-time-to-failure) ist die durchschnittliche Zeit, welche ein System ohne
Fehler arbeitet, nachdem es aufgesetzt oder repariert worden ist.
·
MTTR (mean-time-to-repair) ist die durchschnittliche Zeit, die benötigt wird, ein
fehlerhaftes System zu reparieren.
Mit diesem Hintergrund kann man den Begriff Ausfallsicherheit (engl. Availability) nun wie
folgt definieren:
Availabili ty =
MTTF
(MTTF + MTTR )
Die Aussage “It is paradoxical that the larger a system is, the more critical is its availability,
and the more difficult it is to make it highly-available.” [GS91] macht deutlich, wie schwierig
es ist, in komplexen Systemen eine hohe Ausfallsicherheit zu erreichen. Die derzeit
ausfallsichersten Systeme liegen nach Tabelle 1 im Bereich der High-availability
(Hochverfügbarkeit), sie weisen also eine Ausfallzeit von nur 5 Minuten pro Jahr auf.
16
System Type
Down Time
(Minutes per year)
Availability
Unmanaged
50000
90%
Managed
5000
99%
Well-managed
500
99.9%
Fault-tolerant
50
99.99%
High-availability
5
99.999%
Very-high-availability
.5
99.9999%
Ultra availability
0.05
99.99999%
Tabelle 1: Verfügbarkeit von Systemen nach [GS91]
2.4
Lastverteilung zur Erreichung von Ausfallsicherheit
A distributed system is one in which the failure of a computer you didn’t even know
existed can render your own computer unusable. (Leslie Lamport)
Lastverteilung (engl. Loadbalancing) bezeichnet die Aufgabe, Anfragen von Clients nach
einer bestimmten Strategie auf einen bestimmten zuständigen Server weiterzuleiten. Die
Aufgabe der Lastverteilung ist immer dann nötig, wenn zur Erreichung von Skalierbarkeit
und Ausfallsicherheit bestimmte Bereiche des Systems redundant ausgelegt werden. Diese
Redundanz dient dazu, ein System ohne oder mit möglichst wenigen sogenannten
einzelnen Ausfallpunkten (engl. Single Points of Failure) zu erhalten [GS96].
Da aber eine Anfrage typischerweise von nur genau einem Server bearbeitet wird, muss
darüber entschieden werden, an welchen Server die Anfrage geleitet werden soll.
In der Praxis findet man sehr häufig zwei Strategien, nach denen diese Entscheidung
getroffen wird. Eine sehr einfach zu implementierende Möglichkeit ist die Anwendung des
4
so genannte Round-Robin Verfahrens, welches hintereinander in fester Reihenfolge über
die Menge aller verfügbaren Server iteriert um danach wieder beim ersten zu beginnen.
4
Eine mögliche Herkunft des Wortes Round-Robin ist die französische Bezeichnung rond ruban (rundes Band),
mit der eine Petition bezeichnet wurde, bei der die Unterschriften im Kreis herum geschrieben wurden, um
die Reihenfolge zu verschleiern, in der sie geleistet wurden.
17
Eine weitere, aber durchaus anspruchsvollere und schwieriger zu implementierende
Strategie ist das Load-based oder lastbasierte Verfahren, bei dem ständig die Auslastung der
verfügbaren Server protokolliert wird und diejenigen Server mit hoher Auslastung weniger
Anfragen zugewiesen bekommen. Dadurch wird erreicht, dass die hoch ausgelasteten
Server entlastet werden. Durch die entstehende Dynamik stellt sich mit fortlaufender Zeit
ein Gleichgewicht in der Auslastung aller Rechner ein. Eine gute Einführung zu HTTPLoadbalancing-Strategien bietet [GT02, Kapitel 20].
Ein kritischer Punkt in einem Lastverteilungssystem ist die Erkennung von Fehlern und die
entsprechende Reaktion darauf. Dies entscheidet auch über die Ausfallsicherheit des
gesamten Systems. Der Loadbalancer ist dafür verantwortlich zu erkennen, welche unter
ihm arbeitenden Server in einem vorher zu definierenden Rahmen korrekt und mit für die
Clients erträglicher Performance arbeiten. Anfragen dürfen nicht an Server geleitet werden,
die ausgefallen sind, überlastet sind oder in sonstiger Art nicht korrekt arbeiten (z.B. durch
Liefern korrupter Ausgaben). Diese müssen aus der Verteilung ausgeschlossen und die
Clients auf andere Server geleitet werden. Nur eine gute und zuverlässige Strategie in
diesem Punkt garantiert eine weitgehende Ausfallsicherheit des Systems.
Aus diesem Punkt kann man die folgende Aussage ableiten:
Ausfallsicherheit = Skalierbarkeit + gute Fehlererkennung
Da Loadbalancer einzelne Anfragen nicht selbst beantworten müssen, sondern diese nur
5
weiterleiten , werden keine großen Anforderungen an die Rechenleistung gestellt. Der
natürlich vorhandene Bedarf an Netzwerkressourcen kann oftmals durch Maßnahmen wie
z.B. Local Triangulation [Rad01] oder Out-Of-Path-Return [Hew03a] noch weiter gesenkt
werden. Die dem Autor bekannten Systeme sind im Praxiseinsatz nicht an ihre
Leistungsgrenzen gestoßen und waren auch für große verteilte Systeme mit hoher
Transaktionszahl geeignet.
Die Technik und Ausgereiftheit dieser Systeme ist weit fortgeschritten. Dies liegt auch
größtenteils an ihrem sehr beschränkten und überschaubaren Aufgabengebiet.
Loadbalancer sind meist von anderen Komponenten in einem komplexen Systemaufbau
unabhängig. Unter dem Loadbalancer arbeitende Ebenen wie die Datenhaltungssysteme
müssen nichts von der Existenz eines Loadbalancers wissen. Die vielleicht einzige
Ausnahme ist der im Folgenden beschriebene Punkt.
Da der Loadbalancer als Zwischenschicht eingezogen wurde, haben die verarbeitenden
(Web-)Server keinen direkten Kontakt mit dem Client. Die Server sehen nun immer die IPAdresse des Loadbalancers als ihren Client und nicht mehr die des eigentlichen Clients, der
die Anfrage initiiert hat. Dadurch wird eventuell eine auf den Servern vorhandene
5
Teilweise ist es nötig, die ankommenden Anfragen auf der Applikationsschicht (Schicht 7 des OSISchichtenmodells [DZ83]) zu analysieren (z.B. um Informationen über die HTTP-Session herauszufiltern).
Die dafür nötige Rechenleistung ist aber vernachlässigbar.
18
Programmlogik, die z.B. auf die IP-Adresse des Clients reagiert, gestört. Hier bieten
fortgeschrittene Loadbalancer die Möglichkeit, die ursprüngliche Adresse in einem
zusätzlichen HTTP-Header an die verarbeitenden Server durchzureichen. Diese haben
dann die Möglichkeit, die IP-Adresse auszulesen und entsprechend darauf zu reagieren.
Die Aufgabe der Lastverteilung wird entweder durch Hardware-Loadbalancer oder
Software-Loadbalancer übernommen. Software-Loadbalancer nutzen eine vorhandene
Serverinfrastruktur und bestehen aus einer Software, welche die für die Lastverteilung
nötigen Aufgaben übernimmt, während Hardware-Loadbalancer eigene physikalische
Server sind, die dediziert Lastverteilungsaufgaben übernehmen.
Beispiel für eine softwarebasierte Loadbalancing-Lösung:
Das Modul mod_oc4j des im Oracle 9iAS integrierten Apache Webservers leitet nach dem
Round-Robin Verfahren eingehende HTTP(S) Anfragen an eine der verfügbaren
Applikationsserver-Instanzen im Cluster weiter [Ora02b].
Beispiel für eine hardwarebasierte Loadbalancing-Lösung:
Der E-Commerce Traffic-Director SA8220 von Hewlett Packard kann eingehende HTTPRequests an einen der verfügbaren Webserver weiterleiten. Es ist ein Loadbalancer in 2U
Rackgröße, der sehr einfach einzurichten und zu konfigurieren ist. Um Ausfallsicherheit
auch auf der Loadbalancer-Ebene zu gewährleisten, lässt sich ein baugleiches Gerät in
einem so genannten ‚Backup-Modus’ betreiben. Dieser gleicht über ein serielles Kabel
Konfigurationseinstellungen mit dem primären Gerät ab und überprüft ständig dessen
Betriebsbereitschaft um im Fehlerfall automatisch alle Aufgaben zu übernehmen. Weitere
Informationen zu diesem Hardware Loadbalancer findet sich unter [Hew03a].
19
20
3 DATENBANKSYSTEME
3.1 Übersicht
Ein Datenbanksystem (DBS) ist ein System zur dauerhaften Speicherung und zum
effizienten Suchen in großen Datenmengen. Moderne Datenbanksysteme bestehen aus
folgenden zwei Komponenten:
·
Die Datenbank (DB): Sammlung von Daten. Diese werden meist mittels geeigneter
Hardware wie z.B. Festplatten persistent gehalten.
·
Das Datenbank-Management-System (DBMS): Programm zum Management von
Datenbanken beliebiger Anwendungen in einem spezifizierten Format.
In der Praxis werden heutzutage meist zwei unterschiedliche Arten von Datenbanksyste6
men eingesetzt:
Die sogenannten relationalen Datenbank-Management-Systeme (RDBMS) [Kel98]
speichern die Daten und die Verknüpfung (Relationen) verschiedener Datensätze in
zweidimensionalen Tabellenstrukturen. Der Zugriff auf die Daten erfolgt über eine
Datenbankabfragesprache und erzeugt, ändert und findet einzelne Zeilen und Spalten in
dieser Tabellenstruktur.
Einen anderen Weg gehen die objektorientierten Datenbank-Management-Systeme
(OODBMS), die sich an objektorientierte Programmiersprachen anlehnen und den Zugriff
auf die Daten über Objekte kapseln. Die Daten werden nicht in einer Tabellenstruktur
gespeichert, sondern entweder in XML-Dateien, serialisierten Java-Objekten oder
proprietären Herstellerformaten. Diese Art der Datenbanksysteme ist noch sehr neu, für
den Praxiseinsatz in großen Systemen sind OODBMS oft noch nicht ausgereift genug oder
bieten keinen ausreichend performanten Zugriff.
Richard Monson-Haefel beschreibt die erste Regel für den Entwurf und die Architektur
von verteilten Systemen folgendermaßen:
Use relational database systems for persistence. They are ubiquitous, proven, standardized, maintainable, robust, and well supported by third-party tools. While ObjectDatabases are a better fit for object-based systems, they are not well supported by
third-party tools like reporting systems and data warehousing systems. In addition,
standardization in OODBS is not as mature as it is in relational database systems, so
applications tend to be less portable across database products. [Mon00]
6
Im weiteren Verlauf dieser Arbeit wird mit dem Begriff Datenbank ein komplettes Datenbanksystem im Sinne
der angegenbenen Definition referenziert, nicht ausschliesslich die Sammlung der Daten.
21
Der Autor dieser Arbeit schließt sich dieser Aussage an.
In großen Projekten ist der Einsatz bekannter erprobter relationaler Datenbanksysteme
erste Wahl. Aus diesem Grund konzentriert sich diese Arbeit vor allem auf diese Systeme.
Eine Übersicht über die derzeit bekanntesten relationalen und objekt-orientierten
Datenbanksysteme findet sich unter [Jav03].
Abbildung 3 zeigt schematisch die Einbettung eines RDMBS und seiner Schnittstellen in
den Kontext einer zugreifenden Applikation. Die Sprache SQL und Javaschnittstelle JDBC
werden in den folgenden Unterkapiteln adressiert.
Applikation
JDBC-Treiber
SQL
SQL
DBMS
DBMS
DB
DB
DBS
DBS
Abbildung 3: Verteiltes Relationales Datenbanksystem (RDBMS)
3.2 Die wichtigsten Zugriffsschnittstellen
3.2.1 SQL (Structured Query Language)
SQL ist die Computersprache, mit der die meisten relationalen Datenbanken erstellt,
manipuliert und abgefragt werden. SQL ist eine sogenannte 4GL (Fourth-Generation
Language). Sie ist nichtprozedural, d. h. der Fragesteller stellt eine Frage, gibt aber keinen
Algorithmus zur Lösung vor. In einer höheren Programmiersprache (3GL) wie Pascal, C
oder Java müsste er angeben, wie die gesuchten Informationen gefunden werden können,
z. B. vom Öffnen der Datei bis zum schrittweisen Iterieren über die Datensätze. Zur
weiteren Spracheinordnung von SQL siehe auch [SM90].
22
SQL ist ein ISO- und ANSI-Standard, der mehrfach spezifiziert wurde bzw. noch wird.
Die größten Meilensteine in der Entwicklung waren:
·
SQL 86 (1986 definiert).
·
SQL 89 (1989 definiert). Zwei mögliche Ebenen des Sprachumfangs: Level 1
und Level 2.
·
SQL 92 oder SQL2 (1992 definiert). Vier mögliche Ebenen des Sprachumfangs:
Entry Level, Transitional, Intermediate Level und Full Level.
·
SQL3 (Spezifikation ist gerade in Arbeit). Informationen zu den Neuerungen in
SQL3 sind in [Sey94] zu finden.
Neben einem bestimmten SQL-Standard unterstützen Datenbanksysteme meist Teile
höherer Standards sowie eigene SQL-Erweiterungen. SQL 89 Level 2 ist auch heute noch
Basis des von vielen Datenbanksystemen unterstützen SQL, bei SQL 92 sind die meisten
Systeme nur Entry Level-Compliant (z. B. Oracle).
Trotz aller Bestrebungen, mittels SQL eine einheitliche Spezifikation für den Zugriff auf
relationale Datenbanksysteme zu schaffen, ist dies in der Praxis oft nicht erreicht worden.
Die Wahrscheinlichkeit, dass eine komplexe SQL-Anfrage auf mehreren relationalen
Datenbanksystemen ohne jegliche Anpassungen syntaktisch korrekt ist und semantisch
gleich abgearbeitet werden kann, ist sehr gering. Es zeichnet sich in naher Zukunft hier
auch keine komplette Vereinheitlichung ab, dies ist aus der Sicht des Autors aber auch
nicht unbedingt notwendig. Im Falle eines Wechsels der Datenbank ist eine Umstellung im
Bereich der SQL-Abfragen zwar oft nötig, der Aufwand hierfür aber vernachlässigbar. Das
größere Problem ist die Tatsache, dass häufig proprietäre Zusatzfunktionalität des
Datenbankherstellers eingesetzt wurde, dessen Umstellung einen meist um Faktoren
größeren Aufwand hervorruft.
3.2.2 JDBC (Java Database Connectivity)
JDBC ist eine Java Standardschnittstelle, die die Anbindung relationaler Datenbanken
erlaubt. Es bietet eine Möglichkeit, verschiedene Datenbanksysteme anzusprechen, SQLBefehle abzusetzen und die Ergebnisse auszulesen. Auch werden explizit Möglichkeiten
zum Steuern von Transaktionen (siehe Kapitel 3.3) angeboten. Zu der Mehrzahl der
relationalen Datenbanksysteme gibt es Java-Treiber, die die JDBC-Spezifikation erfüllen.
Diese wurden entweder vom Hersteller der Datenbank oder von Fremdherstellern
entwickelt.
Im Gegensatz dazu bieten die meisten OODBMS keine JDBC-Treiber. Dies widerspräche
auch der Intention der objektorientierten Systeme, da JDBC die nicht objektorientierte
Abfragesprache SQL kapselt. Um kompatibel zu JDBC-basierten Programmen zu sein,
tauchen trotzdem vereinzelt Implementierungen auf, die eine Art R/O-Mapping
23
(Relational auf Objekt-Mapping) betreiben - es werden relationale Anfragen in SQL
formuliert und anschließend in die objektorientierte Darstellung der Datenbank gewandelt.
Die Schnittstelle JDBC ist Bestandteil der Java 2 Standard Edition (J2SE). Die benötigten
Klassen und Interfaces befinden sich im java.sql-Package. Eine installierte JavaUmgebung und ein JDBC-Treiber für die gewünschte Datenbank sind also ausreichend,
um SQL-Anfragen abzusetzen.
Der folgende Programmausschnitt zeigt ein einfaches Beispiel, welches demonstriert, wie
mittels JDBC mit einem Oracle-Datenbanksystem gearbeitet werden kann:
//--register the Oracle JDBC-Driver
Class.forName( oracle.jdbc.OracleDriver.class.getName() );
//--connect to the database
Connection conn =
DriverManager.getConnection( "jdbc:oracle:thin:@SERVER:1521:SID",
"username",
"password" );
//--enable transactions
conn.setAutoCommit( false );
//--create statement object
Statement stmt = conn.createStatement();
//--execute the two SQL-statements
stmt.executeUpdate( "UPDATE Konto SET Amount=235 WHERE KTONr=71113" );
stmt.executeUpdate( "UPDATE Konto SET Amount=0 WHERE KTONr=172329" );
//--commit the transaction
conn.commit();
//--close the connection
conn.close();
3.2.3 SQLj (Embedded SQL for Java)
Eine zweite Möglichkeit, aus Java heraus auf relationale Datenbanken zuzugreifen, ist SQLj
[SQL03]. Es ist durch den Zusammenschluss führender Datenbankhersteller wie Oracle,
Sybase, SUN, IBM, Informix, Microsoft sowie Sun Microsystems entstanden und stellt
einen ANSI-Standard für Embedded SQL dar. Durch den ebenfalls standardisierten
Präprozessor SQLj Translator ist es möglich, Java-Programme mit eingebettetem,
statischem SQLj zu entwickeln.
Im Unterschied zu JDBC fügt der Programmierer in seinen Java-Sourcecode spezielle
Befehle ein, die von einem Präprozessor übersetzt werden.
System.out.println(“now performing select query..”);
#sql {SELECT * FROM bsp_tabelle};
24
SQLj bietet einige Vorteile, zu nennen sind vor allem die im Vergleich zu JDBC viel
kompaktere Schreibweise und die durch den Präprozessor gegebene Möglichkeit einer
Syntaxkontrolle noch vor der Laufzeit.
Allerdings kann gerade die Pflicht der Benutzung eines Präprozessors auch als Nachteil
angesehen werden. Es macht die Entwicklung und Einbettung in Drittapplikationen
komplexer und schwieriger. Des Weiteren ist es durch die Präkompilierung aller SQLStatements nicht möglich, zur Laufzeit dynamische Anfragen zu generieren.
Diese Arbeit wird nicht weiter auf SQLj als Schnittstelle zu relationalen Datenbanken
eingehen. SQLj hat sich in der Praxis beim Einsatz in Applikationsservern nicht
durchsetzen können.
3.3 Transaktionen
Eine Transaktion bezeichnet eine Folge von logisch zusammengehörenden
Datenbankanweisungen. Sie dient dazu, einen konsistenten Datenbankzustand in einen
anderen konsistenten Datenbankzustand zu überführen [GR93].
Bezüglich der Ausführung von Transaktionen garantiert das Datenbanksystem die
Einhaltung von vier grundlegenden Eigenschaften: Atomarität, Konsistenz, Isolation und
Dauerhaftigkeit. Man spricht hierbei von den sogenannten ACID-Eigenschaften, abgeleitet
von den Anfangsbuchstaben der englischen Begriffe Atomicity, Consistency, Isolation und
Durability [HR01]. Diese im Folgenden näher erläuterten Eigenschaften charakterisieren
zugleich das Transaktionskonzept.
1. Atomarität (Atomicity, "Alles oder Nichts")
Die Ausführung einer Transaktion soll aus Sicht des Benutzers ununterbrechbar verlaufen,
so dass sie entweder vollständig oder gar nicht ausgeführt wird. Dies bezieht sich vor allem
auf die im Rahmen der Transaktion auszuführenden Änderungen der Datenbank. Tritt
während der Ausführung einer Transaktion ein Fehler auf (Programmfehler, HardwareFehler, Absturz des Betriebssystems usw.), der die ordnungsgemäße Fortführung
verhindert, werden seitens des DBS sämtliche bereits erfolgten Änderungen der
Transaktion zurückgesetzt.
2. Konsistenz (Consistency)
Die Transaktion ist die Einheit der Datenbankkonsistenz. Dies bedeutet, dass sie die
Datenbank von einem konsistenten in einen wiederum konsistenten (nicht
notwendigerweise unterschiedlichen) Zustand überführt. Von besonderer Bedeutung ist
dabei die Einhaltung der logischen Konsistenz, so dass die Inhalte der Datenbank einem
möglichst korrekten Abbild der modellierten Wirklichkeit entsprechen. Hierzu können
beim Datenbankentwurf semantische Integritätsbedingungen (zulässige Wertebereiche,
25
Schlüsseleigenschaften usw.) definiert werden, welche vom DBS automatisch zu
überwachen sind. Das DBS garantiert somit, dass am Ende einer jeden Transaktion
sämtliche Integritätsbedingungen erfüllt sind. Änderungen, welche zu einer Verletzung der
Integritätsbedingungen führen, werden abgewiesen, d. h., sie führen zum Zurücksetzen der
Transaktion. Voraussetzung für die logische ist die physische Konsistenz der Datenbank,
also die korrekte interne Repräsentation und Speicherung der Daten im Datenbanksystem.
Zu beachten ist, dass die Konsistenz im Allgemeinen nur vor und nach Ausführung einer
Transaktion gewährleistet wird.
3. Isolation (Isolation)
Datenbanksysteme unterstützen typischerweise eine große Anzahl von Benutzern, die
gleichzeitig auf die Datenbank zugreifen können. Trotz dieses Mehrbenutzerbetriebs wird
garantiert, dass dadurch keine unerwünschten Nebenwirkungen eintreten, wie z. B. das
gegenseitige Überschreiben desselben Datenbankobjektes.
4. Dauerhaftigkeit (Durability)
Das DBS garantiert die Dauerhaftigkeit bzw. Persistenz erfolgreicher Transaktionen, deren
Operationen vollständig ausgeführt wurden. Dies bedeutet, dass Änderungen dieser
Transaktionen alle künftigen Fehler überleben, insbesondere auch Systemabstürze oder
Speicherausfälle.
Eine vollständige Implementierung der Isolation ist in der Praxis nur sehr schwierig
umzusetzen, wenn man keine beeinträchtigenden Performanceeinbussen in Kauf nehmen
will. Aus diesem Grund werden hier meist auch abgeschwächte Formen der Isolation
(Isolationslevel) angeboten, die performanter implementiert werden können.
Um zu verstehen, welche Unterscheidungen in diesem Bereich gemacht werden können,
müssen die Begriffe Dirty Read, Nonrepeatable Read und Phantom Read erklärt werden.
Dies sind Phänomene, die auftreten können, falls keine vollständige Isolation von der
Datenbank implementiert ist.
Dirty Read: Ein Dirty Read liegt vor, wenn Transaktion A einen Wert liest, der von der
noch nicht abgeschlossenen Transaktion B gesetzt wurde. Die Datenbank kann sich
während der Laufzeit von Transaktion B in einem inkonsistenten Zustand befinden. Falls
Transaktion B nicht erfolgreich ausgeführt wird, hat Transaktion A einen nicht gültigen
oder ‚schmutzigen’ (engl. dirty) Wert gelesen.
Nonrepeatable Read: Transaktion A liest einen Wert, danach ändert eine gleichzeitig
laufende Transaktion B diesen Wert und wird abgeschlossen. Falls nun Transaktion A den
Wert erneut liest und den durch Transaktion B geänderten Wert erhält, spricht man von
einem Nonrepeatable Read. Dies verstößt gegen die Isolationsbedingung.
26
Phantom Read: Ein Phantom Read geschieht, wenn eine Transaktion neue Datenbankzeilen sehen kann, die erst nach dem Beginn der eigenen Transaktion von einer anderen
Transaktion hinzugefügt wurden.
Datenbanksysteme implementieren einen Schutz vor derartigen Phänomenen meist mit
Hilfe von sogeanannten Locks (Sperren). Weitere Informationen hierzu findet man
beispielsweise unter [WV02] oder [Amb00]. Eine Einordnung des Schutzgrades wird durch
die sogenannten Isolationslevel angegeben. Je höher dieser ist, desto mehr Schutz wird
geboten. Tabelle 2 zeigt genau, welche der Phänomene in den vier bekanntesten
Isolationsleveln auftreten dürfen.
Isolationslevel
Dirty Reads
Nonrepeatable
Reads
Phantom
Reads
Read Uncommitted
Ja
Ja
Ja
Read Committed
Nein
Ja
Ja
Nonrepeatable Read
Nein
Nein
Ja
Serializable
Nein
Nein
Nein
Tabelle 2: Isolationslevel bei Transaktionen
Unter [Sim03] werden anschauliche Beispiele gegeben, mit denen nachzuvollziehen ist,
welche Isolationslevel in bekannten Datenbanksystemen unterstützt werden.
Die ACID-Bedingungen für Transaktionen müssen auch in verteilten Datenbanksystemen
garantiert werden können. Dies geschieht durch ein sogenanntes Commit-Protokoll. Das
einfachste und meistbenutzte Commit-Protokoll ist das Zwei-Phasen-Commit-Protokoll
(2PC; engl. two-phase-commit protocol) [Bir95, Kap. 13.6.1]. Beim 2PC-Protokoll wird ein
Koordinator festgelegt. Die anderen beteiligten Rechnerknoten werden als Teilnehmer
(oder Agenten) bezeichnet. Nachdem der Koordinator bestimmt wurde, verteilt dieser die
Aufträge an die anderen Teilnehmer.
Folgende Phasen werden bei Anwendung des 2PC-Protokolls durchlaufen:
1. Wahlphase
a) Der Koordinator fragt die Teilnehmer, ob sie ein Commit durchführen können.
b) Die Teilnehmer teilen dem Koordinator ihre Entscheidung mit.
2. Entscheidungsphase
a) Der Koordinator entscheidet, indem er die erhaltenen Ergebnisse auswertet.
b) Die Knoten, die mit »ja« geantwortet haben, warten auf die Entscheidung und
führen bei positivem Bescheid die Transaktion lokal aus.
27
3.4 Praxisbeispiel
Oracle bietet mit dem Oracle 9i Real Application Cluster (RAC) ein Zusatzmodul zur ihrer
Datenbank Oracle 9i Enterprise Edition, die eine Skalierung von mehreren Oracle 9iInstanzen ermöglicht. Der Autor beschreibt die Erfahrungen mit der Benutzung einer
RAC-Installation auf einem HP-UX 11i Cluster mit insgesamt 10 PA-RISC/8700
Prozessoren und 20 GB Hauptspeicher verteilt auf 4 physikalische Server. Die Rechner
waren mit einem 1GB-Ethernet verbunden, die Anbindung an das Storage-System erfolgte
per Fibrechannel.
Der einzelnen Cluster-Knoten können von einer zentralen Stelle administriert werden, der
Wartungsaufwand nach erfolgreicher Installation und Konfiguration ist nicht bedeutend
höher als bei einer einzelnen Oracle-Instanz. Alle bekannten Konzepte (z.B. Archive-Logs,
Initialisierungsfiles) funktionieren entweder exakt sehr ähnlich wie in einer EinzelInstallation.
Als größte Fragestellung vor den ersten Tests blieb die Glaubhaftigkeit der folgenden
Aussage aus dem Konzept-Handbuch von Oracle, der sehr viel Bedeutung beigemessen
wurde:
The concept of transparency implies that Real Application Clusters environments are
functionally equivalent to single-instance Oracle database configurations. In other
words, you do not need to make code changes to deploy applications on Real
Application Clusters if your applications ran efficiently on single-instance Oracle
configurations. [Ora02a, S. 34]
Diese Aussage konnte in der Praxis vollkommen nachvollzogen werden. Für einen
Datenbank Client (z.B. einen J2EE-kompatiblen Applikationsserver) verhält sich die RACInstallation wie eine einzelne Oracle Instanz. Die Logik der Lastverteilung, d.h. die
Entscheidung, welchem Cluster-Knoten ein Client zugewiesen wird, und das Failover im
Falle von Funktionsstörungen auf einem Cluster-Knoten wird vom OCI-Datenbanktreiber
übernommen und erfolgt vollkommen unsichtbar und transparent. Transaktionen werden
korrekt ausgeführt und verteilt.
Durch diese Transparenz wurde eine Modularisierung geschaffen, die es ermöglicht, die
Skalierbarkeit und Ausfallsicherheit von Datenbanksystemen völlig unabhängig von der des
restlichen Systems zu betrachten. Eine Zusammenfassung der Fähigkeiten des Oracle Real
Application Cluster findet sich im technischen Whitepaper [Ora01].
28
4 JAVA 2 ENTERPRISE EDITION
4.1 Übersicht
Mit der Java 2 Enterprise Edition (J2EE) [Sun01a] stellt Sun eine komplette Plattform für
server-side-computing zur Verfügung. J2EE versucht eine hardware- und betriebssystemunabhängige, multi-user fähige, portable, sichere Plattform für serverseitige Anwendungen
zu sein. Ein Meilenstein von J2EE sind die Enterprise JavaBeans, ein Standard für das
Erstellen von serverseitigen Komponenten in Java.
J2EE ist eine Spezifikation und kein fertiges Produkt. Es spezifiziert Regeln, die der
Entwickler beim Erstellen von serverseitigen Komponenten einhalten muss. Die Hersteller
implementieren dann die J2EE Spezifikation in ihren J2EE-kompatiblen Produkten. Aus
diesem Grund ist J2EE nicht an einen Hersteller gebunden, auch nicht an den Erfinder
Sun Microsystems. Es gibt eine Reihe kommerzieller Anbieter von Applikationsservern, die
die J2EE-Spezifikation implementieren.
J2EE kann man auch als Zusammenfügung verschiedenster sogenannter „middle-wareservices“ oder Dienste begreifen [NBW+01]. Die wichtigsten werden im Folgenden kurz
erläutert:
Enterprise JavaBeans (EJB): EJB [Mon01] [RAJ02] definiert, wie serverseitige
Komponenten aussehen und definiert einen „Vertrag“ zwischen den Komponenten und
dem Applikationsserver, der die Komponenten verwaltet. EJB ist einer der
Hauptbestandteile von J2EE und wird im Kapitel 4.2 ausführlich behandelt.
Remote Method Invocation (RMI) and RMI-IIOP: RMI stellt InterprozessKommunikation und andere Services für Kommunikation zur Verfügung. RMI-IIOP ist
eine Erweiterung von RMI und verwendet das Internet-Inter-ORB Protokoll und kann
somit für die Integration von bestehenden CORBA-Komponenten verwendet werden.
RMI bildet die Basis für die Kommunikation der einzelnen Komponenten in verteilten
J2EE-Systemen.
Java Naming and Directory Interface (JNDI): JNDI lokalisiert den physikalischen Ort
von verwendeten Komponenten und anderen Ressourcen im Netzwerk. Diese Ressourcen
wie Datenquellen oder Enterprise JavaBeans werden dem zentralen JNDI-Service bekannt
gemacht und können von Clients erfragt werden.
Java Database Connectivity (JDBC): JDBC ist die am weitesten verbreitete relationale
Datenbankschnittstelle für Javaprogramme. Sie wurde in Kapitel 3.2 bereits ausführlich
behandelt.
29
Java Transaction API (JTA) und Java Transaction Service (JTS): Die JTA und JTSSpezifikation erlaubt sichere und verlässliche Übertragung der Komponenten und deren
Daten über das Netzwerk (siehe Kapitel 4.4).
Java Messaging Service (JMS): JMS dient der asynchronen Kommunikation zwischen
Komponenten. Message-Driven-Beans, ein Typ der Enterprise JavaBeans, nutzen
beispielsweise diese Schnittstelle.
Java Servlets and JavaServer Pages (JSP): Java Servlets und JSP sind Technologien, die
zur Entwicklung dynamischer Webseiten eingesetzt werden.
JavaMail: JavaMail ist eine Schnittstelle und dient dem Versenden und Empfangen von EMails aus Javaprogrammen.
Connectors: Die Connectors Architecture verbindet J2EE mit bereits bestehenden Mainframe- und Enterprise Resource Planning (ERP) Systemen.
4.2 Enterprise JavaBeans
Enterprise JavaBeans sind einer der wichtigsten Teile der Java 2 Enterprise Edition
Plattform. Sun Microsystems definiert Enterprise JavaBeans 2.0 folgendermaßen:
The Enterprise JavaBeans (EJB) architecture is a component architecture for the
development and deployment of component-based distributed business applications.
Applications written using the Enterprise JavaBeans architecture are scalable,
transactional, and multi-user secure. These applications may be written once, and then
deployed on any server platform that supports the Enterprise JavaBeans specification.
[SUN01]
Die EJB-Architektur ist eine neue serverseitige komponentenbasierte Architektur für
verteilte unternehmenskritische Geschäftsanwendungen. Sie spezifiziert Schnittstellen,
Aufgaben und Zuständigkeiten einzelner Komponenten. Die EJB-Architektur unterstützt
durch ein breites Dienstleistungsspektrum die schnelle Realisierung von skalierbaren,
robusten und mehrbenutzerfähigen serverseitigen Anwendungssystemen.
Da die EJB-Spezifikation kein Produkt ist, muss sie von den Server- und
Applikationsserver-Herstellern umgesetzt werden.
Der Einsatz von EJB eignet sich besonders als serverseitiges Komponentenmodell für
verteilte Mehrschichten-Umgebungen. Webbasierte Anwendungen weisen häufig eine
Multitier-Architektur auf, wobei die mittlere Schicht für die Anwendungslogik
verantwortlich ist. Enterprise JavaBeans stellen ein Komponentenmodell für die
Realisierung der Anwendungslogik in dieser mittleren Schicht zur Verfügung, es ist damit
eine so genannte Middleware.
30
EJB bietet zudem Unterstützung für Basisdienste wie Transaktionsverwaltung, Sicherheits-,
Persistenz-, oder Ressourcenmanagement. Entwickler von Web-Anwendungen werden
dadurch entlastet und können sich auf die Implementierung der eigentlichen Präsentationsund Anwendungslogik konzentrieren.
4.2.1 Die Architektur
Im Folgenden werden die wesentlichen Bestandteile der EJB-Spezifikation bzw. EJBArchitektur und -Laufzeitumgebung vorgestellt und detailliert erläutert. Dadurch wird ein
Überblick über die einzelnen Bestandteile dieser Architektur gegeben und die Eignung und
Praxistauglichkeit von EJB für die Entwicklung von komplexen verteilten Client/ServerAnwendungen kann somit besser erörtert werden. Abbildung 4 gibt eine einfache
Übersicht über die EJB-Architektur.
Applikationsserver / EJB-Server
EJB-Client 1
(z.B. Webapplikation)
EJB-Container
EJB-Objekte
EJB-Client 2
(z.B. Importapplikation)
Abbildung 4: EJB-Architektur
Der EJB-Server
Zurzeit existiert eine Vielzahl von Anbietern für EJB-Server am Markt. Ein EJB-Server
kann als Teil eines Applikationsservers gesehen werden, der nicht nur die Implementierung
der EJB-Spezifikation bietet, sondern die aller geforderten J2EE Spezifikationen. In der
Praxis gibt es nur sehr wenige Hersteller, die reine EJB-Server, aber viele, die komplette
Applikationsserver vertreiben. Der Benutzer der EJB-Plattform ist somit nicht wie bei
anderen Frameworks auf einen bestimmten Serverhersteller eingeschränkt, sondern kann
31
dank der durch die Spezifikation gegebenen sehr hohen Portabilität mit wenig oder keinem
Entwicklungsaufwand den zu Grunde liegenden Server austauschen. Dies wird zwar
heutzutage in der Praxis noch nicht immer erreicht, doch sind hier große Fortschritte zu
sehen. Die Weiterentwicklung der Spezifikation wird mit großem Interesse durch die
Vielzahl der Serveranbieter und die noch größere Anzahl der Benutzer stetig
vorangetrieben.
Der EJB-Container
Der EJB-Container ist das Herzstück des EJB-Servers. Der in der EJB-Spezifikation
abgedeckte Umfang an Funktionalitäten erstreckt sich von der Verwaltung der Persistenz
von Objekten über die Bereitstellung von Diensten wie Transaktionen und SicherheitsManagement (zum Beispiel Zugriffsbeschränkungen auf EJB-Objekte), bis hin zu einer
vollständigen Lebenszyklusverwaltung der eingesetzten Objekte. Hierbei werden all diese
Funktionalitäten vom EJB-Server zur Verfügung gestellt und können vom Entwickler
durch das Ansprechen oder Implementieren von vordefinierten Schnittstellen einfach
genutzt werden. Auch bietet der EJB-Container Hilfen und Schnittstellen für die
Installation von EJB-Objekten (Deployment).
Ein Versäumnis von Sun war es, die Schnittstelle des EJB-Containers zum umgebenden
Applikationsserver nicht genügend zu konkretisieren. Dies macht den EJB-Container nicht
einfach austauschbar und ersetzbar. Ein Problem, welches auch das Design des Prototyps
zu dieser Arbeit stark beeinflusst hat.
Vor allem die Persistenzmöglichkeiten und ihre Implikationen in Hinblick auf
Skalierbarkeit und Performance werden im Fortlauf dieser Arbeit genauer betrachtet.
Die EJB-Objekte
Die zu implementierende Geschäftslogik und Funktionalität wird nach dem EJBParadigma in einzelne Objekte, die Enterprise JavaBeans, zerlegt. Ist ein solches Objekt auf
dem Server installiert, so kann man auf die durch dieses EJB zur Verfügung gestellte
Funktionalität zugreifen. In der EJB-Spezifikation wird zwischen mehreren grundsätzlich
verschiedenen Arten von Beans unterschieden.
Zum einen sind Entity-Beans vorgesehen, die vornehmlich zur Datenspeicherung benutzt
werden. Instanzen von Entity-Beans werden mit einem primären Schlüssel erzeugt. Sie
können über diesen Schlüssel wiedergefunden und somit über mehrere Sitzungen hinweg
benutzt werden. Die Eigenschaften von Entity-Beans werden üblicherweise in einer
Datenbank gespeichert, so dass sie nach einem Serverneustart immer noch vorhanden und
nutzbar sind. Zur Speicherung in einer Datenbank bietet die EJB-Spezifikation die zwei
alternativen Methoden Container Managed Persistence (CMP) und Bean Managed
Persistence (BMP) an, die später genauer erläutert werden.
32
Zum anderen können so genannte Session-Beans genutzt werden, um Instanzen von EJB’s
für nur eine Sitzung zu erzeugen und zu verwenden. Diese Art Beans wird dafür genützt,
verarbeitende Funktionalitäten zu beherbergen, welche eventuell zur Datenhaltung auf
Entity-Beans zurückgreifen. Sie selbst sollten keine Daten persistent halten.
Bei diesen Session-Beans findet eine Unterscheidung danach statt, ob die einzelnen BeanInstanzen an einen Client gekoppelt sein und ihren Status behalten sollen oder nicht. Im
ersten Fall werden sie Stateful Session Beans genannt. Ein typischer Einsatzort ist die
Implementierung einen Warenkorbes in einem e-commerce-System. Falls diese
Beibehaltung des Status nicht benötigt wird, können Stateless Session Beans verwendet
werden. Hier kann eine Instanz die Aufrufe mehrere Clients bedienen, was bedeuten
würde, dass es möglich ist, dass zwei hintereinander folgende Aufrufe desselben Clients auf
das selbe Bean andere Ergebnisse liefern könnten.
Ein weiterer zentraler Bestandteil ist eine neue Art von Beans, die Message-driven Beans.
Diese sind dadurch gekennzeichnet, dass die Methodenaufrufe nicht mehr ausschließlich
synchron durchgeführt werden, sondern der Java Messaging Service (JMS) auch asynchrone
Aufrufe gestattet. Dies ermöglicht es den Clients, Nachrichten in eine Warteschlange zu
positionieren und ohne auf die Antwort zu warten, im Programmfluss fortzufahren.
Weitere Informationen zu den verschiedenen Bean-Arten finden sich in [Mon01].
Der EJB-Client
Ein EJB-Client ist ein Programm, welches auf die EJB-Objekte zugreift und diese nutzt.
Hierbei wird zwischen einem so genannten Remote- und einem Local-Client
unterschieden. Der Remote-Client befindet sich außerhalb der Java-Virtual-Machine, in
dem die EJB-Objekte installiert sind, der Local-Client im Gegensatz dazu in dieser VirtualMachine. Diese Nähe zu den benutzten EJB-Objekten bietet starke Vorteile in Bezug auf
die Ausführungsgeschwindigkeit. Sie ergeben sich zum einen durch entfallende Zugriffe
über lokale Netzwerke, zum anderen aber auch daraus, dass bei rein lokalem Zugriff auf ein
EJB-Objekt die Verwendung des zeitlich kostspieligen RMI- oder RMI-IIOP-Protokolls
vermieden werden kann (Local interfaces).
Ein typisches Beispiel für einen EJB-Client ist eine JSP-basierte Webapplikation, die auf die
Geschäftslogik der EJB-Objekte zugreift, die im EJB-Container installiert sind.
4.3 Die EJB-Persistenz-Schnittstellen
Das Persistenzmanagement ist eine der Hauptaufgaben der Enterprise JavaBeansArchitektur. Dieses Kapitel will vor allem die Schnittstellen betrachten, die durch die
Verwendung von EJBs vorgegeben werden.
33
Über den Erfolg oder Misserfolg der EJB-Architektur entscheiden in erster Linie nicht die
aktuellen verfügbaren Implementierungen der EJB-Container, sondern die spezifizierten
Schnittstellen. Diese müssen
·
übersichtlich gestaltet und ohne großen Lernaufwand zu benutzen sein
·
den funktionalen Bedürfnissen großer Projekte genügen und
·
es erlauben, dass die darunterliegenden Implementierungen ausgetauscht werden
können.
Die EJB-Spezifikation in den ersten Versionen (1.0 und 1.1) hatte im Bereich der
Schnittstellendefinition des Persistenzmanagement große Defizite. Die für die persistenten
Objekte vorgesehenen Entity-Beans waren in der Praxis teils nicht oder nur eingeschränkt
nutzbar, so dass Entwickler dazu übergegangen sind Sessionbeans zu nutzen, um von dort
‘manuell’ auf externe Datenbanksysteme zuzugreifen. Es entstanden Hilfsmittel, die diese
Vorgehensweise vereinfacht und erleichtert haben. Dies war ein gefährlicher Schritt für den
Erfolg von Enterprise JavaBeans und mit Sicherheit nicht im Sinne des Erfinders Sun
Microsystems. Die aktuelle Version 2.0 der Spezifikation behebt jedoch einen großen Teil
der Mängel. Diese Arbeit geht auf die alte Spezifikation in der Version 1.0 und 1.1 nicht
ein, hierzu gibt [Mon01] eine gute Einführung.
Enterprise JavaBeans bietet zwei Persistenzschnittstellen an:
·
BMP (Bean Managed Persistence) und
·
CMP (Container Managed Persistence).
Bevor auf die Unterschiede dieser beiden Persistenzarten eingegangen wird, folgt ein kurzer
Blick in den Aufbau eines Entity-Beans, seiner Konfiguration und seinen Platz im EJBContainer.
34
Client
Home Interface
Remote Interface
Applikationsserver
EJB Container
Home Interface
Remote Interface
XML DeploymentDeskriptor
Bean Klasse
DB
Abbildung 5: Definition eines Enterprise JavaBeans
Abbildung 5 stellt die wichtigsten Komponenten eines Enterprise JavaBeans dar:
·
Das Remote Interface beschreibt alle Methoden des Entity-Beans, welche von
einem Client aus zugreifbar sein sollen und die ausschliesslich mit der aktuellen
Instanz arbeiten (also z.B. Product.getName() )
·
Im Home Interface werden die Methoden definiert, mit denen neue Instanzen
angelegt oder gelöscht werden können und einzelne Instanzen in der Datenbank
gesucht werden.
·
Die Bean Klasse letztendlich implementiert die Methoden des Remote und Home
Interface. Ein Client sieht die Bean Klasse nicht, der Zugriff erfolgt immer über das
Remote- oder Home Interface.
·
Der XML Deployment-Deskriptor beschreibt den Namen, die Klassen, persistente
Felder und weitere Eigenschaften des Entity-Beans.
Die Interfaces, Klassen und XML-Deskriptoren werden nach einem bestimmten Verfahren
zu einem Java Archive-File (JAR-File) gepackt und können so in einem EJB-Container
deployed werden. Die Geschäftslogik, die durch die Remote- und Home Interfaces
definiert wird, ist über den EJB-Server von einem Client aus zugänglich, wogegen die Bean
Klasse innerhalb des Containers liegt und von dort eigenständig (BMP) oder mit Hilfe des
Containers (CMP) auf die Datenbank zugreift.
35
Der Aufbau des Deskriptors wie auch die Implementierung der Bean Klasse unterscheiden
sich je nach Wahl der Persistenzschnittstellen Container Managed Persistence oder Bean
Managed Persistence. Die Vor- und Nachteile und die sich daraus ergebenden
Einsatzgebiete werden in den folgenden Unterkapiteln genauer erläutert.
4.3.1 CMP
Bei der Nutzung von Container Managed Persistence übernimmt der EJB-Container das
komplette Persistenzmanagement. Der Container weiss, wie es die Instanz eines Beans in
der Datenbank ablegen muss und kümmert sich selbständig um das Anlegen, Löschen,
Lesen und Schreiben der Daten. Die für den Container nötigen Informationen werden im
Deployment-Deskriptor für jedes Bean angegeben. Dazu zählen:
·
Klassennamen des Home- und Remote Interfaces und der Bean-Implementierung.
·
Namen der einzelnen Attribute, die persistent gemacht werden sollen.
·
Name und Typ des Primary-Key Feldes.
·
Die Definition der Finder-Methoden mittels EJB QL (EJB Query Langage).
Eine genaue Beschreibung der Attribute findet sich beispielsweise in [NBW+01].
Untenstehend sehen Sie einen Auszug aus dem Deployment-Deskriptor zum Prototyp
dieser Arbeit, in dem ein CMP Entity-Bean beschrieben wird.
<entity>
<ejb-name>Product</ejb-name>
<home>de.tum.in.j2eedemo.ProductHome</home>
<remote>de.tum.in.j2eedemo.ProductRemote</remote>
<ejb-class>de.tum.in.j2eedemo.ProductEJB</ejb-class>
<persistence-type>Container</persistence-type>
<prim-key-class>java.lang.String</prim-key-class>
<primkey-field>code</primkey-field>
...
<cmp-field><field-name>name</field-name></cmp-field>
<query>
...
<ejb-ql>
SELECT DISTINCT OBJECT(p) FROM Product p WHERE p.name LIKE '%?1%'
</ejb-ql>
</query>
...
</entity>
Durch diese Art Meta-Beschreibung [FY98] ist es dem Container möglich, die
nachfolgenden Aufgaben selbst zu implementieren:
36
1. Das Auslesen und Schreiben einzelner Attribute. Damit der Container die
Implementierung übernehmen kann, werden die Bean-Klasse und die
Zugriffsmethoden auf die einzelnen Attribute als abstrakt definiert:
public abstract class ProductEJB implements EntityBean
{
public abstract String getName();
public abstract void setName( String name );
...
}
Es ist nun möglich eine Klasse zu generieren, die von der Bean-Klasse erbt und in
der die abstrakten get- und set-Methoden implementiert sind.
2. Das Anlegen, Löschen, Lesen und Schreiben einer Bean-Instanz. Die nach
der Spezifikation angebotenen Methoden ejbCreate(), ejbRemove(),
ejbLoad() und ejbStore() bleiben leer bzw. enthalten keinen Code für die
Persistenzhaltung. Somit können diese Methoden durch den Container
überschrieben werden.
3. Die Implementierung von Suchanfragen (Findern). Die eigene Implementierung geschieht bei CMP nicht. Diese Aufgabe wird vom Container übernommen.
Die im Home Interface angegebenen Suchmethoden werden ebenfalls automatisch
durch den Container mit Hilfe der Definition der EJB QL-Query im DeploymentDeskriptor in entsprechenden Datenbankzugriffscode umgesetzt.
Der Bean-Entwickler kann sich bei Nutzung von CMP somit vollständig auf die zu
implementierende Geschäftslogik konzentrieren.
4.3.2 BMP
Bei Bean-Managed-Persistence muss der Bean-Entwickler die komplette Persistenzhaltung
selbständig implementieren.
Der EJB-Container nimmt dem Entwickler in diesem Bereich keine Arbeit ab, die BeanKlassen werden nicht wie bei CMP abstrakt definiert. In den Methoden ejbCreate(),
ejbRemove(), ejbLoad() und ejbStore() müssen die entsprechenden Zugriffe auf die
Datenbank erfolgen.
Dazu ist es ist möglich, vom Container eine JDBC Datenbank-Connection zu erhalten, die
für die Anfragen benutzt werden kann. Dies wird im nachfolgenden SourcecodeAusschnitt demonstriert:
37
public String ejbCreate( String code, String name )
{
DataSource ds =
(DataSource)new InitialContext().lookup( "jdbc:DefaultDS" );
Connection conn = ds.getConnection();
Statement stmt = conn.createStatement();
stmt.executeQuery( "INSERT INTO Product (pk, code, name ) .." );
stmt.close();
conn.close();
...
}
Bei Nutzung von BMP werden im Deployment-Deskriptor die Finder-Methoden nicht
mittels EJB QL definiert. Auch hier ist es Aufgabe des Bean-Entwicklers, die nötigen
ejbFindXY()-Methoden in der Bean-Klasse zu implementieren.
4.3.3 EJB QL
Die Definition und Implementierung von Finder-Methoden wurde in der EJB1.1
Spezifikation von Sun in keinster Weise festgelegt. Es wurde dem Applikationsserver oder
Container-Hersteller überlassen, wie in Einzelfall das Mapping von den Signaturen der
Findermethoden auf wirkliche Datenbankabfragen vom Entwickler anzugeben war. Dies
geschah bei einigen Applikationsservern durch spezielle Kommentare im Sourcecode oder
durch eigene XML-Konfigurationsdateien.
Durch die Tatsache, dass sich in diesem Bereich keinerlei Standard durchsetzen konnte,
wurde die Portabilität der ganzen EJB-Applikation auf andere Applikationsserver stark
eingeschränkt. Auch die Migration auf andere Datenbanksysteme wurde teilweise
aufwendiger, da auch hier die feinen Unterschiede in der Interpretation von SQL komplett
an den Entwickler abgegeben wurden und so die Applikationen beim Umstieg angepasst
werden mussten. Sun hat diesen Missstand erkannt und in der EJB 2.0 Spezifikation eine
einheitliche Konfigurationsmöglichkeit vorgesehen. Diese besteht aus einem zusätzlichen
Tag im standardisierten Deployment-Deskriptor, in dem mit Hilfe der SQL-ähnlichen
Abfragesprache EJB QL die Semantik der Findermethoden festgelegt wird.
Nachfolgend ein Beispiel, welches die Finder-Methode findByCode implementiert, welche
alle Objekte zurückliefert, die ein Attribut code mit dem übergebenen Wert besitzen:
<query>
<query-method>
<method-name>findByCode</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</query-method>
<ejb-ql>
SELECT DISTINCT OBJECT(p) FROM Product p WHERE p.code = ?1
</ejb-ql>
</query>
38
Diese Spezifikation ist ein großer Schritt in die richtige Richtung. Applikationen, die EJB
QL nutzen, sind einfacher auf andere Datenbanken und Applikationsserver zu migrieren.
Leider enthielt die erste Version von EJB QL aber noch einige gravierende Mängel, doch
sieht es so aus, dass Sun hier in Zusammenarbeit mit der einsetzenden Industrie einen
Großteil dieser meist fehlenden Funktionalität aufgenommen hat.
Neu hinzugekommen seit EJB2.1 ist die in SQL sehr oft eingesetzte COUNT()-Funktion.
Mit ihr lassen sich die Anzahl bestimmter Resultate zählen. Als Beispiel zählt die folgende
Query alle Produkte mit einem Preis größer 1000.
SELECT COUNT( p ) FROM Products AS p WHERE p.price > 1000
Durch die neuen Aggregierungsfunktionen MAX(), MIN(), AVG() und SUM() wird es
möglich, die Berechnung von Maximal/Minimalwerten, Durchschnittswerten oder
Summen direkt durch Abfragen auszudrücken. Dies bedeutet eine erhebliche Entlastung
des Entwicklers und einen möglichen Performancevorteil.
Beispiel:
SELECT MAX( o.totalprice ) FROM Orders o WHERE o.type = 'Processed'
Eine der wichtigsten Änderungen in der EJB2.1 Spezifikation aber ist die neue ORDER BY Klausel in EJB QL.
Mit ORDER BY kann man das Resultat einer Findermethode sortiert zurückliefern lassen.
Der Grund, warum EJB2.0 die ORDER BY-Klausel nicht enthielt, war ein sehr einfacher und
hängt mit den unterschiedlichen Verfahren, Strings zu sortieren, zusammen.
Folgender Auszug erläutert das Problem sehr anschaulich:
According to Sun, the ORDER BY clause was not included in EJB 2.0 because of
problems dealing with the mismatch in ordering behavior between the Java language
and databases. The example they gave had to do with string values. The semantics of
ordering strings in a database may be different from those of the Java language. For
example, Java orders String types according to character sequence and case
(uppercase vs. lowercase). Different databases may or may not consider case while
ordering, or they may not consider leading or trailing whitespace. In light of these
possible differences, it seems as though Sun has a reasonable argument, but only for
limiting the portability of ORDER BY, not for eliminating its use all together. EJB
developers can live with less than perfect portability of the ORDER BY clause, but they
cannot live without the ORDER BY clause altogether. [Mon01]
39
Dieses Problem wird sich auch in absehbarer Zeit nicht lösen. Der Unterschied in EJB 2.1
ist, dass Sun Microsystems es aufgrund der dringlichen Forderungen akzeptiert hat, auch
eine nicht perfekte Lösung umzusetzen.
Weitere Features, die helfen würden, diese Query-Sprache auch in großen, komplexen
Projekten einsetzen zu können, sind leider nicht in der EJB2.1-Spezifikation enthalten.
·
Subqueries fehlen (SELECT .... IN ( SELECT ... ))
·
Es gibt noch keinen Date-Datentyp.
·
Fehlende oft genutzte Funktionen wie UPPER() oder LOWER().
·
fehlende GROUP BY und HAVING - Unterstützung
Nichtsdestotrotz ist EJB QL ist ein sehr wichtiger Bestandteil der EJB Spezifikation. Die
Möglichkeit, datenbankunabhängig auch komplexe Abfragen zu bauen, ist entscheidend für
die Portabilität von J2EE-Applikationen. Wie bei vielen neu eingeführten Standards sind
die ersten Versionen oft teils noch nicht ausgereift und lassen Ausrichtungen auf praktische
Anforderungen vermissen. Durch die Änderungen in EJB 2.1 zeigt sich aber, das EJB QL
auf dem richtigen Weg ist. Sollten darüber hinaus in einer weiteren Version die letzten
großen Probleme behoben werden, hat EJB QL die Chance, eine universelle
Abfragesprache zu werden.
4.4 Transaktionen in EJB-Architekturen
Da J2EE-Architekturen auf transaktionale relationale Datenbanksysteme zugreifen können,
ist es eine Aufgabe der EJB-Spezifikation zu definieren, wie die Transaktionsverwaltung
auszusehen hat. Es wurden durch Sun Microsystems hier zwei Möglichkeiten
unterschiedlicher Komplexität geschaffen:
1. Transaktionen können unter der Nutzung der JTA (Java Transaction API) direkt
aus dem Sourcecode eines Enterprise JavaBeans gestartet und beendet werden.
Diese Möglichkeit nennt sich BMT (Bean Managed Transactions).
2. Durch Angabe von bestimmten Transaktionsattributen im Deployment-Deskriptor
kann entschieden werden, ob und wie die entsprechende Methode in einen
Transaktionskontext eingebettet wird. Diese Möglichkeit nennt sich CMT
(Container Managed Transactions).
Grundsätzlich können Transaktionen sowohl aus Entity-Beans als auch aus Stateful- und
Stateless Sessionbeans verwendet werden. Bei letzteren müssen Transaktionen allerdings in
ein und derselben Methode begonnen und wieder beendet werden, da die entsprechenden
Bean-Instanzen nach Rückkehr einer Methode vom Container für andere Clients
verwendet oder sogar vernichtet werden können.
40
4.4.1 CMT
Mit CMT (Container Managed Transactions) können Transkationsattribute für einzelne
Methoden eines EJBs gesetzt werden. Durch das Vergeben dieser Attribute wird der BeanEntwickler davon entbunden, selbst die Transaktionssteuerung zu übernehmen.
Um CMT für ein Enterprise JavaBean zu aktivieren, muss im Deployment-Deskriptor die
Einstellung
<transaction-type>Container</transaction-type>
vorgenommen werden.
Nun hat man die Möglichkeit, auf Methodenebene einzelne Transaktionsattribute zu
vergeben, die darüber entscheiden, ob die entsprechende Methode in einem
Transaktionskontext abläuft und ob sie berechtigt ist, neue Transaktionen zu beginnen:
·
Required: Die Methode muss im Kontext einer Transaktion ausgeführt werden,
ein eventuell schon bestehender Transaktionskontext wird übernommen, ansonsten
wird eine neue Transaktion begonnen.
·
Supports: Die Methode kann im Kontext einer Transaktion ausgeführt werden, ein
eventuell schon bestehender Transaktionskontext wird übernommen, ansonsten
läuft die Methode ohne Transaktionsklammern.
·
NotSupported: Die Methode wird nie im Kontext einer Transaktion ausgeführt,
eine eventuell existierender Transkationskontext wird nicht übernommen und nach
Abarbeitung der Methode fortgesetzt (Transaction Suspension).
·
RequiresNew: Die Methode muss im Kontext einer neuen Transaktion ausgeführt
werden.
·
Mandatory: Die Methode muss im Kontext einer bestehenden Transaktion
ausgeführt werden (bei nicht bestehender Transaktion wird eine TransactionRequiredException geworfen).
·
Never: Die Methode wird nie im Kontext einer Transaktion ausgeführt, falls ein
Transaktionskontext besteht, wird eine Exception geworfen.
Die Wahl eines Transaktionsattributs erfolgt über entsprechende Einträge im DeploymentDeskriptor. Somit ist der eigentliche Sourcecode komplett von der Transaktionssteuerung
befreit. Weiterführende Informationen hierzu bietet beispielsweise [Mon01].
41
4.4.2 BMT
Falls dennoch die komplette Transaktionssteuerung vom Bean-Entwickler übernommen
werden soll, bietet es sich an, Bean Managed Transactions zu verwenden. Dies wird durch
einen entsprechenden Eintrag im Deployment-Deskriptor konfiguriert:
<transaction-type>Bean</transaction-type>
Der Java Transaction Service (JTS) spezifiziert die Funktion eines Transaktionsmanagers
für die Anwendungsentwicklung in Java. Die zugehörige Programmierschnittstelle heißt
Java Transaction API (JTA) und wird im Folgenden beschrieben. Beide APIs sind Teil von
J2EE und damit nicht in der Java 2 Standard Edition enthalten. Sie müssen aber von J2EEkonformen Applikationsservern angeboten werden und bieten damit eine einheitliche
Schnittstelle, um feingranulare Transaktionssteuerung aus Enterprise JavaBeans zu
realisieren.
Eine Anwendung kann auf mehrere transaktionsunterstützende Ressourcen, typischerweise
Datenbanken, zugreifen. Die Koordination von Transaktionen wird durch einen
Transaktionsmanager (TM) realisiert. Er initiiert Start, Abbruch und Abschluß von
Transaktionen.
Durch das Interface javax.transaction.UserTransaction wird eine Schnittstelle für
den direkten Zugriff von Anwendungen geschaffen. Die wichtigsten Methoden dieser
Schnittstelle sind die folgenden:
·
Das Starten einer Transaktion kann mittels begin() erfolgen.
·
Mittels rollback() wird eine Transaktion abgebrochen.
·
Der erfolgreiche Abschluß wird mittels commit() initiiert.
·
getStatus()
·
setTransactionTimeout()
·
Durch das Kommando setRollbackOnly() ist nachfolgend nur ein Beenden
mit Rollback möglich, ein erfolgreicher Abschluss der Transaktion wird
verhindert.
erlaubt die Abfrage des aktuellen Status einer Transaktion.
setzt ein Transaktions-Timeout.
Der Applikationsserver bietet meist zwei Möglichkeiten, um an die UserTransactionSchnittstelle zu gelangen. Die erste Möglichkeit ist der Zugriff über das EJB-Interface
javax.ejb.EJBContext :
UserTransaction ut = ejbContext.getUserTransaction();
42
Da diese Möglichkeit allerdings nur aus Bean-Instanzen heraus funktioniert, bieten
Applikationsserver eine zweite Möglichkeit über das Lookup einer Implementierung der
UserTransaction-Schnittstelle im JNDI-Namespace an, dies geschieht ähnlich wie für den
Zugriff auf eine Datenbank-Connection bei Bean Managed Persistence (siehe Kapitel 4.3.2)
Aus einem Enterprise JavaBean sollte allerdings die Methode über den EJBContext
bevorzugt werden, da dies der durch die Spezifikation vorgegebene Weg ist.
43
44
5 O/R-MAPPING
5.1 Übersicht und Gliederung
Eine der größten Herausforderungen für jedes Projekt, im dem die Geschäftslogik in einer
objektorientierten Programmiersprache verfasst ist und welches auf große Datenvolumen
in einer relationalen Datenbank zugreifen muss, ist die Kommunikation zwischen der
Geschäftslogik- und Datenschicht. Dieses Kapitel versucht hier Lösungen aufzuzeigen und
folgende Fragestellung genauer zu durchleuchten:
„Wie können Daten in einem relationalen Datenbanksystem abgelegt werden und wie
kann aus objektorientierten verteilten Systemen performant auf diese Daten zugeriffen
werden?“
Die Bearbeitung dieser Frage wird in die folgenden Bereiche unterteilt:
Kapitel 5.2, Datenhaltung, versucht Lösungen auf die Frage zu finden, wie die Daten in
einem relationen Datenbanksystem abgelegt werden. Es werden einzelne Entwurfsmuster
[BMR+96] vorgestellt und Optimierungsmöglichkeiten der Datenbankstruktur besprochen.
Kapitel 5.3, Datenzugriff, widmet sich der Fragestellung, wie aus einer beliebigen objektorientierten Applikation auf Daten zugegriffen wird.
Kapitel 5.4, Caching, beschreibt die Notwendigkeit der Zwischenspeicherung (engl.
Caching), um performant unter Berücksichtigung der Skalierbarkeit auf die Daten zugreifen
zu können.
Aufgrund der großen Komplexität des Themas O/R-Mappings erhebt diese Arbeit keinen
Anspruch darauf, eine vollständige Übersicht aller für diesen Bereich bekannten
Architektur- und Entwurfsmuster zu geben. Hier sei auf weiterführende Literatur (z.B.
[Kel03]) verwiesen.
5.2 Datenhaltung
Entscheidend für den Erfolg eines Projektes, welches seine Daten in einer relationen
Datenbank ablegt, ist die Strukturierung der einzelnen Datenbanktabellen und -spalten. Sie
entscheidet über die Wartbarkeit, die Integrität und Performanz der gesamten Anwendung.
Bei der Wahl der richtigen Datenhaltungsstrategien muss im Blick behalten werden, dass
der Zugriff auf die Daten aus einer objektorientieren Schicht erfolgen wird.
Die Aufgabe der Applikationsserver- und O/R-Mapping-Tool-Hersteller muss es sein,
genügend Konfigurationsmöglichkeiten zu bieten, um Objekte unter Ausnutzung
verschiedenster Strategien persistent zu machen. Diese Aufgabe darf nicht dem Bean-
45
Entwickler überlassen werden, die Logik sollte nicht in der Geschäftslogikschicht
implementiert werden.
Es gibt kein Vorgehen, welches für alle Situationen die bestmöglichste Eignung bietet. Alle
im Folgenden vorgestellten Muster und Strategien haben in der Praxis ihre berechtigten
Anwendungsgebiete.
5.2.1 One Class/One Table Pattern
Die wohl eingängigste, einfachste und intuitivste Idee des O/R-Mappings ist das One
Class/One Table Pattern. Hierbei wird der Aufbau der Datenbank, wie in Tabelle 3
dargestellt, stark an der Klassenstruktur der objektorientierten Schicht ausgerichtet.
Relationale Sicht
Objektorientierte Sicht
1 Tabelle
1 Klasse
1 Zeile
1 Instanz (1 Objekt)
1 Feld
1 Attribut
Tabelle 3: Einfaches O/R-Mapping nach [CK96]
Die Definition einer Klasse entspricht der Definition einer Tabelle. Es wird festgelegt,
welche Attribute (der Klasse) bzw. Felder (der Tabelle) vorhanden sind und von welchem
Typ sie sind. Die einzelnen Instanzen dieser Definitionen nennt man in objektorientierten
Programmiersprachen Objekte der Klasse, in relationalen Datenbanken sind dies einzelne
Zeilen in der Tabelle.
Die entsprechende Implementierung dieses Mappings ist sehr einfach. Alle dem Autor
bekannten J2EE-basierten Applikationsserver unterstützen dieses Pattern und es ist für
kleine Projekte durchaus ausreichend.
Ein Problem in der Vergangenheit war allerdings die zu starke Fixierung auf dieses
Entwurfsmuster zur Datenhaltung. Applikationsserver unterstützten keine weitergehenden
Möglichkeiten und so sahen sich die Enterprise JavaBeans-Entwickler gezwungen, ihr
Objekt-Modell so anzupassen, dass man weiterhin diese Art der Abbildung von einer
Tabelle auf eine Klasse aufrechterhalten konnte. Ein Beispiel hierfür ist die
Implementierung eines Entity-Beans, dessen einziger Zweck es ist, eine m-zu-nVerknüpfung zweier anderer Beans herzustellen. Das Ergebnis auf relationaler Ebene (eine
Zwischentabelle) mag korrekt sein, doch sollte es nicht Aufgabe der Geschäftslogikebene
sein, dies durch Implementierung eines eigenen Entity-Beans zu erreichen.
46
Weitere Nachteile des One Class/One Table Pattern:
·
Es besteht keine Datenbankstabilität im Bezug auf Strukturänderungen. Bei
Definition neuer Klassen oder Änderung von Klassendefinitionen muss die
Datenbankstruktur angepasst werden. Eine lästige und teure Datenmigration ist die
Konsequenz.
·
Es muss für alle nicht abstrakten Klassen, die zur Laufzeit mindestens eine Instanz
besitzen können, genau eine entsprechende Tabelle existieren; es ist weder möglich
Instanzen unterschiedlicher Klassen in einer gemeinsamen Tabelle zu speichern,
noch die Instanzen einer Klasse auf mehrere Tabellen aufzuteilen.
5.2.2 Serialized LOB Pattern
Die dargestellten Probleme des One Class/One Table Patterns kann man mit dem
sogenannten Serialized LOB Pattern [Fow02] lösen.
Wie in Abbildung 6 dargestellt ist, wird hierbei von einer nicht änderbaren
Datenbankstruktur ausgegangen. In einer Tabelle wird in einem LOB (Large Object)-Feld
ein komplettes serialisiertes Java-Objekt abgelegt. Durch diese Speichertechnik wird
sichergestellt, dass Änderungen an Klassendefinitionen nicht zu einer Änderung der
Tabellenstruktur führen. Die im One Class/One Table-Pattern fehlende Datenbankstabilität ist in diesem Bereich also gegeben. Auch wird es möglich, Objekte
unterschiedlichen Typs in einer gemeinsamen Tabelle abzulegen.
Allerdings erkauft man sich diese Vorteile mit einigen gravierenden Nachteilen:
·
Es ist nicht mehr möglich, die Daten direkt in der Datenbank zu ‚sehen’, also ohne
Deserialisierung den Inhalt der Objekte zu bestimmen oder zu ändern. In der
Praxis ist dies aus der Erfahrung des Autors ein gravierender Nachteil. Für das
Debuggen von Fehlern und Nachvollziehen von Programmabfolgen ist es oftmals
wichtig, die enthaltenen Daten über ein normales SQL-Abfrageprogramm
bearbeiten zu können.
·
Es ist schwierig bis unmöglich Ad hoc-Queries direkt von der Datenbank ausführen
zu lassen, da die Daten nicht normalisiert (Kap. 5.2.5 oder [Kel98]) vorliegen und
Datenbanken die Suche auf LOB-Feldern oft nicht oder nur sehr unperformant
erlauben.
·
Es kann immer nur ein ganzes Objekt gespeichert werden, es ist nicht möglich,
einzelne Attribute zu ändern. Dies kann in Verbindung mit dem allgemein
langsamen Zugriff auf LOB-Felder ein weiteres Performanceproblem darstellen.
47
User
+firstname:String
+lastname:String
Customer
+mail:String
Employee
+phoneextension:String
PK
Content as LOB
#1
SerializedByteArray (Class:Customer,Frank,Binder,[email protected])
#2
SerializedByteArray (Class:Employee,Christine,Boss,-533)
#3
SerializedByteArray (Class:Customer,August,Meier,[email protected])
#4
..
Abbildung 6: Anwendung des Serialized LOB Patterns
5.2.3 Single Interitance Table Pattern
Falls sowohl das One Class/One Table Pattern mit seinem strikten 1:1-Mapping von
Tabellen auf Klassen als auch das Serialized LOB Pattern, bei dem beliebige Klassen in eine
einzige Tabelle abgebildet werden, nicht eingesetzt werden können, bietet sich das Single
Inheritance Table Pattern [Fow02] an. Es stellt einen Kompromiss zwischen beiden
Mustern dar.
Man könnte die Anwendung des Single Inheritance Table Pattern als Vereinigung ( UNION)
aller Attribute mehrer Klassen aus einem Vererbungsbaum in einer Tabelle bezeichnen.
Abbildung 7 verdeutlicht das Vorgehen. Die Typ- oder Markierungsspalte (Type) dient zur
Rekonstruktion der Klasse, die in der entsprechenden Zeile abgelegt ist. ([ Fus97] spricht bei
hier von sogenannten Object Distinguishers als Markierung).
48
User
+firstname:String
+lastname:String
Customer
Employee
+mail:String
PK
Type
#1
+phoneextension:String
First
Name
Last Name
Mail
CUSTOMER Frank
Binder
[email protected]
#2
EMPLOYEE Christine
Boss
#3
CUSTOMER August
Meier
[email protected]
#4
..
..
..
..
PhoneExtension
-533
..
Abbildung 7: Anwendung des Single Inheritance Patterns
Durch dieses Pattern werden die Nachteile des Serialized LOB Patterns gelöst. Zum einen
können die Daten aufgrund der normalisierteren Tabellenstruktur mittels SQL-Anfragen
eingesehen werden, ebenso ermöglicht diese Tabellenstruktur sehr performante Ad hocQueries.
Aber auch dieses Pattern hat in bestimmten Einsatzgebieten seine Nachteile: So kann für
Klassenhierachien, bei denen die beteiligten Klassen nur sehr wenige gemeinsame Attribute
besitzen, unnötig viel Platz verbraucht werden.
Das Single Inheritance Table Pattern beschreibt den Versuch, das Modell der Vererbung
aus objektorientierten Programmiersprachen in die relationale Welt zu übertragen. Es stellt
hierfür eine sehr einfache Möglichkeit dar und ist aus Sicht des Autors in der Praxis sehr
gut einsetzbar. Für weitere Informationen zur Abbildung von Vererbung in relationale
Datenbanken sei auf [Kel97] verwiesen.
49
5.2.4 Overflow Tables
Bei großen Datenmengen kann es die Möglichkeit geben, das Konzept der Overflow Tables
anzuwenden. Dabei wird eine Art Auslagerungtabelle benutzt, um selten benötigte
Datensätze an anderer Stelle zu speichern und damit die Zugriffsgeschwindigkeit auf die
jetzt kleinere Originaltabelle zu erhöhen.
Angewendet werden kann dieses Konzept, falls die Entscheidung, welche Datensätze
selten benötigt werden, sehr einfach getroffen werden kann. Ein Beispiel wäre eine Tabelle
mit Bestellungen, bei der alle Sätze, die älter als ein Jahr sind, in einer anderen Tabelle
‚geparkt’ werden können.
Eine weitere Bedingung für die Anwendung einer Overflow Table ist die Tatsache, dass
keine oder nur wenige Anfragen ausgeführt werden müssen, die einen kompletten
Datenbestand vorraussetzen (z.B. für Statistikberechnungen). Diese Art von Anfragen
müssten im Falle einer vorhandenen Overflow Table gleichzeitig mit zwei Tabellen
arbeiten, was durch dann nötige JOINs zwischen den Tabellen einen Performanceeinbruch
bedeuten würde.
Im optimalen Fall kann das verwendete Datenbanksystem die Auswahl der Datensätze und
das Verschieben dieser in die Überlauftabelle komplett transparent übernehmen. Oracle
bietet mit dem sogenannten Partitioning genau diese Möglichkeit.
5.2.5 Denormalisation und Redundanz
Normalisierung ist die „Überführung komplexer Beziehungen (Tabellen) in einfache
Beziehungen durch Aufteilung der Attribute einer Tabelle auf mehrere Tabellen.“ [Kel98, Kap.
4.1.1]
Durch Normalisierung wird ein System vor Inkonsistenzen geschützt und es wird ein
Datenbestand von möglichst geringer Redundanz geschaffen. Dies spart Platz auf externen
Speichermedien. Leider erreicht man durch Normalisierung oft eine Verlangsamung des
Systems, da Informationen über mehrere Tabellen verteilt sind und damit eine größere
Anzahl von Abfragen nötig ist, um an die entsprechenden Daten zu gelangen.
Es gilt, hier eine Abwägung zwischen Platzverbrauch und Geschwindigkeit zu treffen.
Da externer Speicherplatz in heutigen Systemen oft keine entscheidende Kostenrolle spielt,
kann es sehr sinnvoll sein, Denormalisierung zu betreiben, das bedeutet absichtlich ein
System größerer Redundanz zu schaffen, um performanter auf Daten zugreifen zu können.
Das Denormalisierungs-Pattern ist eng mit dem Controlled Redundancy-Pattern [Kel03]
verwandt.
Eine gute O/R-Mapping-Software muss Möglichkeiten bieten, mit denen Denormalisierungen vorgenommen werden und automatisch Redundanzfelder angelegt und
verwaltet werden können.
50
5.2.6 Primary Keys
Um Objekte eindeutig identifizieren und in der Datenbank wiederfinden zu können,
benötigen sie einen eindeutigen Identifier. Es bietet sich an, jedem Objekt ein zusätzliches
Attribut zu geben, in dem dieser Identifier gespeichert wird (siehe auch das Identity Field
Pattern in [Fow02]).
Auf relationaler Datenbankebene bezeichnet man diese eindeutigen Schlüssel als Primary
Keys. Sie sind meist eindeutig pro Tabelle und können beispielsweise aus einer Zahl
bestehen, welche für jeden neuen Datensatz in dieser Tabelle hochgezählt wird. Die
Tatsache, dass Sätze in anderen Tabellen einen gleichen Primary Key besitzen können,
stört nicht, da der relationale Zugriff immer auch unter Angabe des Tabellennamens
erfolgt:
SELECT * FROM TableName WHERE PKField = xy
In der objektorientierten Welt definieren Klassen den Typ eines Objektes. Durch die
verschiedenen O/R-Mapping-Strategien wird nun nicht immer wie beim One Class/One
Table Pattern (siehe Kapitel 5.2.1) eine 1:1-Abbildung von Klassen auf Tabellen
vorgenommen. Um sich hier ein nötiges Mapping von Primary-Keys zu ersparen, kann es
sinnvoll sein, wenn der Identifier eines Objektes nicht nur tabellenweit eindeutig gehalten
wird, sondern systemweit.
Aber auch in diesem Falle eines systemweit eindeutigen Schlüssels bleibt das Problem, dass
es zu eventuellen Dopplungen kommen könnte, falls Objekte aus einem Fremdsystem
importiert werden. Um sich auch in diesem Fall ein Mapping ersparen zu können, muss ein
Objekt bei seiner Erzeugung einen Identifier erhalten, der ähnlich dem Fingerabdruck eines
Menschen weltweit eindeutig ist und es damit zu keinen Kollisionen kommen kann.
Einen Schlüssel mit diesen Eigenschaften nennt man UUID (Universally Unique IDentifier)
oder GUID (Globally Unique IDentifier).
„A UUID is an identifier that is unique across both space and time […]“ [Int98]
Diese UUIDs sind nach der Spezifikation der Internet Engineering Task Force, aus der
auch obige Aussage entnommen wurde, 128 Bit lang und eignen sich damit nicht mehr zur
Speicherung in gängigen NUMERIC-Datenbankfeldern, die üblicherweise für die
Speicherung von Primary Keys verwendet werden.
Es werden Feldtypen nötig, die größere Datenmengen speichern können. Hier bietet sich
die Verwendung eines String bzw. VARCHAR-Feldes als Primary Key-Feldtyp an. Die
Handhabung und Indizierung von VARCHAR-Felden ist zwar in den meisten
Datenbanksystemen langsamer als die von NUMERIC-Feldern, der entstehende
Performancenachteil ist aber sehr gering und wird durch die leichtere Handhabung mehr
als ausgeglichen.
51
Der Prototyp zu dieser Arbeit verwendet zur Erzeugung eindeutiger Schlüssel die folgende
Methode:
String uuid = new java.rmi.dgc.VMID().toString();
Die Klasse java.rmi.dgc.VMID ist Bestandteil der Java 2 Standard Edition. Durch sie
werden zwar keine UUIDs erzeugt, die exakt obige Spezifikation erfüllen, sie sind aber
dennoch hinreichend eindeutig, dass sie auch in der Praxis eingesetzt werden können. Die
genaue Erklärung und Umsetzung eines weiteren Algorithmus zur Erzeugung einer UUID
für die Verwendung in Enterprise JavaBeans-Umgebungen ist unter [Mar02] zu finden.
Ein Beispiel einer Java-Implementierung, mit der UUIDs nach der IETF Draft
Spezifikation erstellt werden können, bietet [JUG03].
5.3 Datenzugriff
Nachdem Möglichkeiten aufgezeigt wurden, in welcher Form die Daten in einer
relationalen Datenbank abgelegt werden können, stellt sich nun die Frage, wie aus einer
objektorientierten Schicht auf diese Daten zugegriffen werden kann.
5.3.1 Brute Force Zugriff
Die einfachste Möglichkeit ist, direkt aus den Methoden, aus denen heraus auf Daten
zugegriffen werden muss, SQL-Statements über eine Schnittstelle wie JDBC abzusetzen
und auszulesen. Diese Vorgehensweise wird nach [Amb03] auch Brute Force Strategy
genannt. Im Sequenzdiagramm in Abbildung 8 ist der Ablauf aufgezeigt.
Diese Vorgehensweise besitzt allerdings einige entscheidende Nachteile:
52
·
Es besteht eine direkte und unmittelbare Kopplung zwischen der Datenebene und
der Objektebene; die Geschäftslogik ist nicht getrennt von der Datenhaltung.
·
Der Applikationsprogrammierer benötigt sehr gute SQL- und JDBC-Kenntnisse.
·
Der entstehende Sourcecode zum Zugriff auf die Daten ist nur schlecht
wiederverwertbar.
·
Bei nötigen Datenstrukturänderungen ist auf Applikationssebene ein hoher
Migrationsaufwand nötig.
Die Brute Force Strategy mag für sehr kleine Projekte eine aktzeptable Vorgehensweise
sein, die aufgelisteten Nachteile machen einen Einsatz bei mittleren bis großen Projekten
mit einer hohen Anzahl von persistenten Objekten aber nahezu unmöglich.
Customer
Database
1: buildSQL Statement
1.1: select from Database
1.2: set Data Values
Abbildung 8: Brute-Force Datenzugriff nach [Amb03]
5.3.2 DAOs (Data Access Objects)
Datenzugriffsobjekte (engl. Data Access Objects) versuchen die Nachteile der Brute Force
Strategy damit zu addressieren, dass die Datenzugriffs-Logik zentralisiert wird. Diese wird
komplett in die sogenannten Data Access Objects verschoben. Sie verstecken damit die
Datenbankschicht vor den Geschäftsobjekten. Abbildung 9 zeigt die Logik des Zugriffs
über ein DAO.
Sun hat mit JDO (Java Data Objects [JDO03a]) eine Spezifikation geschaffen, die es
Herstellern ermöglicht, Applikationsprogrammierern eine für Java einheitliche Schnittstelle
für Data Access Objects anzubieten. Einen guten Überblick über Java Data Objects bietet
[JDO03b].
53
Customer
CustomerData (DAO)
Database
1: read( Customer )
1.1: getPrimaryKey
1.2: build Select Statement
1.3: select From Database
1.4: setDataValues
Abbildung 9: Datenzugriff mittels Data Access Objects (DAO)
5.3.3 Persistenzframeworks
Ein Persistenzframework entkoppelt den Datenbankzugriff vollständig von der
Geschäftslogik. Es bildet eine Zugriffsschicht (engl. Access Layer, siehe auch Layered
Architecture Pattern in [BMR96+]) zwischen der Applikation und der Datenbank.
Diese Trennnung der Schichten ist in Abbildung 10 schematisch dargestellt.
Die notwendigen Informationen für den Datenzugriff werden dem Persistenzframework
durch sogenannte Metadaten (engl. Metadata, siehe auch Metadata-Pattern in [VCK96] oder
[FY98]) zur Verfügung gestellt. Diese werden entweder in einem Datenbeschreibungsformat wie XML angegeben oder sie können direkt aus den Klassendefinitionen der
Geschäftsobjekte gewonnen werden (z.B. per Java Reflection).
Aufbauend auf diesen Metadaten generiert das Persistenzframework den Code, der auf die
Datenbank zugreift. Diese Generierung kann entweder dynamisch zur Laufzeit geschehen
oder statisch in Form von Data Access Objects (siehe Kapitel 5.3.2), welche zur
Kompilierzeit in die Applikation eingebunden werden.
54
Applikation
O/R-Mapping Schicht
Logische Zugriffsschicht
Physische Zugriffsschicht
Datenbank
Abbildung 10: Trennung von logischer und physikalischer Schicht beim Datenzugriff
Abbildung 11 zeigt den Ablauf eines Datenzugriffes. Es ist zu sehen wie nach der ‘read’Anfrage an das Persistenz-Framework (1) dieses sich selbstständig die nötigen Metadaten
besorgt (1.1 und 1.2), um daraus den SQL-Code zu generieren (1.3), der zur
Datenbankabfrage (1.4) dient.
Customer
Persistence-Framework
Meta-Data
Database
1: read( Customer )
1.1: getDataValue
1.2: getMetaData
1.3: build Select-Statement
1.4: Select from Database
1.5: setDataValues
Abbildung 11: Nutzung eines Persistenzframeworks
55
Tabelle 4 beschreibt die Metadaten, die ein einfaches Persistenzframework mindestens
benötigt, um auf ein einzelnes Java-Objekt zuzugreifen.
MetaData Name
Beschreibung
Inhalt
TABLE_NAME
Name der Tabelle,
in der das Objekte
gespeichert wird.
Der Tabellenname kann aus dem Packageund Klassennamen generiert werden (z.B.
für die Klasse de.tum.in.Test wird der
Tabellenname DE_TUM_IN_TEST
erzeugt).
COLUMN_NAME
für jedes Attribut
Name der Spalte, in
der das
entsprechende
Attribut abgelegt
wird.
Der Attributname kann aus den get- und
set-Methoden für das Attribut gebildet
werden (z.B. erhält das Attribut, welches
durch die Methoden getCode() und
setCode() beschrieben wird, den Namen
code).
COLUMN_TYPE
für jedes Attribut
Der SQL-Type der
Spalte, in der das
Attribut abgelegt
wird.
Der Typ kann durch das TypeMappingPattern (Erklärung siehe Text) aus der
Java Klasse des Objekts bestimmt werden.
Tabelle 4: Benötigte Metadaten zum Zugriff über ein Persistenzframework
5.3.4 Einfache Datenzugriffpatterns
5.3.4.1
CRUD Pattern
7
Das Create, Read , Update & Delete Pattern beschreibt, welche Operationen mindestens
notwendig sind, um ein Objekt persistent zu halten. Die genaue Patterndefinition findet
man beispielsweise unter [YJW98].
Nach diesem Pattern muss jedes persistente Objekt mindestens vier Methoden anbieten:
7
·
Zum Anlegen (Create) eines neuen Objektes in der Datenbank,
·
zum Lesen (Read) von einem Objekt aus der Datenbank,
·
zum Übertragen von Änderungen (Update) eines Objektes und
·
zum Löschen (Delete) eines Objektes.
Statt Des englischen Wortes Read wird im Zusammenhang mit diesem Pattern oft auch Retrieve verwendet.
56
Der Applikationsentwickler, der die Geschäftslogik realisiert, muss sich nicht im Detail um
die Persistenzhaltung kümmern, sondern kann die gewünschte CRUD-Methode als
Blackbox betrachten und diese aufrufen. Der folgende Pseudocode zeigt die beispielhafte
Implementierung der vier Methoden:
create()
executeSQL("INSERT INTO tab (pk,name) VALUES (ownpk,ownname)");
read()
executeSQL("SELECT name FROM tab WHERE pk = ownpk");
update()
executeSQL("UPDATE tab SET name=ownname WHERE pk = ownpk");
delete()
executeSQL("DELETE FROM tab WHERE pk = ownpk");
Dies stellt zwar eine Entlastung des Applikationsentwickler dar, doch muss er weiterhin
entscheiden, wann eine der obigen Methoden aufgerufen werden soll. Beispielsweise muss
er das Wissen darüber besitzen, ob beim Zeitpunkt einer gewünschten Speicherung eines
Objektes dieses schon in der Datenbank existiert (Aufruf der update()-Methode) oder
neu angelegt werden muss (Aufruf der create()-Methode).
Eine Verbesserung des CRUD-Patterns legt diese beiden Methoden zu einer save()Methode zusammen. Hierdurch wird dem Applikationsentwickler die Entscheidung der
Auswahl der update()- oder create()-Methode abgenommen.
save()
if( objectIsAlreadyPersistent )
update();
else
create();
5.3.4.2 Generate-SQL Pattern
Das Generate-SQL Pattern [YJW98] dient dazu, die Erzeugung von SQL-Code durch die
Bereitstellung einfacher Methoden zu zentralisieren und modularisieren. Diese Methoden
können dann beispielsweise für die Implementierung der CRUD-Methoden herangezogen
werden. Dies entkapselt die Logik der Erstellung von SQL-Code von der Logik des
Ausführens der SQL-Statements.
SQL-Statements, die zur Persistenzhaltung in typischen CRUD-Methoden dienen, besitzen
immer einen sehr ähnlichen Aufbau, auch wenn sie zum Zugriff auf verschiedene Objekte
dienen:
57
INSERT
SELECT
UPDATE
DELETE
INTO table_name ( column_name ) VALUES ( values )
* FROM table_name WHERE key_value
table_name SET column_name = xyz WHERE key_value
FROM table_name WHERE key_value
Es können nun Methoden angeboten werden, die aus diesem Grundgerüst, dem Objekt
und den vorhandenen Metadaten (wie z.B. der Name der Tabelle, in der das Objekt
abgelegt werden soll), ein fertiges SQL-Statement erzeugen.
5.3.4.3 Type-Mapping Pattern
Durch das TypeMapping-Pattern [Kel03] wird eine zentrale Stelle geschaffen, über die
Attributstypen der objektorientierten Klassen auf SQL-Typen gemappt werden. Ein
einfaches Mapping für die Programmiersprache Java ist in Tabelle 5 dargestellt.
Javaklasse
SQL-Typ
java.lang.String
VARCHAR
java.lang.Integer
NUMERIC
java.lang.Serializable
BLOB
Tabelle 5: Beispielhaftes Typ-Mapping von Java zu SQL
Die entsprechende Implementierung könnte folgendermassen aussehen: Einer statischen
Methode getSQLType(Class) wird die Klasse des zu wandelnden Objektes übergeben,
und diese liefert aus dem angegebenen Mapping einen SQL-Typ als int-Wert zurück, der
benutzt werden kann, um das Objekt mittels JDBC-Befehl
pstmt.setObject( INDEX, object, getSQLType( object.getClass() );
zu setzen.
58
5.4 Caching
Caching bezeichnet das Zwischenspeichern in einer nicht persistenten Schicht zum
schnellen Zugriff auf gewünschte Daten.
Datenbanksysteme bieten oft konfigurierbare Caches an (Oracle bietet einen Cache um
Tabellen und Indizes im Hauptspeicher zu halten (Buffercache), MySQL ab der Version 4
sogar einen Query-Cache, der Resultate komplexer Suchanfragen zwischenspeichert). Diese
Caches sind sinnvoll und nötig, doch reichen sie für große Systeme mit hoher Last oft nicht
aus. Es bleibt die Notwendigkeit eines Caches in der gleichen Netzwerkschicht, in der sich
die Geschäftslogik befindet, da jeder Netzwerkzugriff verglichen mit lokalen
Methodenaufrufen sehr langsam ist.
Tests mit einer Oracle 9i Release 2 Datenbank (Version 9.0.2) haben ergeben, dass die
Ausführungszeit einer einfachen Anfrage (beispielsweise ein SELECT auf eine leere Tabelle)
fast unabhängig von der eingesetzten Hardware im Bereich 2-5 Millisekunden (ms) liegt. Es
stellte sich heraus, dass die Netzwerklatenz eines 100Mbit- oder 1GB-Netzwerkes für einen
Großteil dieser Zeit verantwortlich gemacht werden kann, nicht etwa die Prozessor- oder
Festplattengeschwindigkeit.
Die Realisierung einer typischen E-Commerce Webseite, bei der auf einer Produktseite 20
Produkte mit jeweils vier Attributen angezeigt werden sollen, würde ohne Caching in einer
objektorientierten Implementierung dazu führen, dass mindestens
1 + ( 20 × 4) = 81
Datenbankanfragen abgesetzt werden. Als erstes werden die 20 Produkte ausgewählt, und
dann wird für jedes Produkt jeweils 4 Mal ein Attribut gelesen. Bei angenommenen 5 ms
Bearbeitungszeit für jede einzelne Anfrage würde der Aufbau dieser Seite mehr als 400 ms
benötigen. Dies ist nicht akzeptabel, wenn man bedenkt, dass eine grundlegende
Verbesserung dieser Werte auch mit hochwertiger Hardware nicht erreicht werden kann.
In einer klassischen Two-Tier-Architektur, in dem direkt aus dem Applikationscode auf die
Datenbank zugegriffen wird, kann eine Performancesteigerung des gesamten Systems
dadurch erreicht werden, dass versucht wird, möglichst wenig Anfragen an die Datenbank
zu stellen. Oftmals ist es möglich, mit einigen komplexen Anfragen alle benötigten Daten
für die Darstellung einer Webseite zu erhalten. Durch Einzug einer O/R-Mapping-Schicht
in einer Multi-Tier-Anwendung kann und sollte diese Art der Optimierung aber nicht mehr
vom Applikationsprogrammierer erledigt werden.
Durch die Anwendung von speziellen Entwurfsmustern in J2EE-Systemen (z.B. SessionFacade oder Value-Objects [Mar02]) kann die Anzahl der Anfragen reduziert werden, doch
belasten diese in gewisser Weise immer den Applikationsprogrammierer, da er Wissen
darüber haben muss, welche Methoden auf der Geschäftslogikschicht Datenbankzugriffe
enthalten und dadurch potenziell langsam sein könnten und wann er diese aufrufen darf.
59
Es bietet sich an, darüber nachzudenken, ob es möglich ist, zwischen der Applikationslogik
und der Datenbank eine transparente Schicht einzuziehen, welche teure Methodenaufrufe
(wie z.B. Netzwerkzugriffe) durch Zwischenspeichern von Ergebnissen entschärfen kann.
Die Anforderungen an eine solche Cache-Schicht sind vielfältig:
·
Möglichst hohe Transparenz. Die Transparenz ist wichtig, um die Businesslogik
vollständig von anderen Diensten wie Datenhaltung, O/R-Mapping oder
Skalierung zu kapseln. Eine optimale Lösung wäre eine, bei der der Sourcecode der
Geschäftslogik durch den Einsatz eines Cache-Systems nicht angepasst werden
müsste.
·
Skalierbarkeit. Das Cachesystem muss sich auf beliebig viele Server verteilen
lassen. Die entsprechend nötige Synchronisation muss dafür sorgen, dass alle
Knoten bei Anfragen an den Cache gleiche Ergebnisse zurückliefern und einen
konsistenten Datenstand repräsentieren [CDK01].
·
Gute Verdrängungsstrategien. Da es oftmals nicht möglich ist, alle Daten im
Zwischenspeicher zu halten, wird eine Strategie benötigt, nach der neue Einträge
aufgenommen und alte Einträge gelöscht werden. Dies entscheidet mit über die
Performanz des gesamten Cachesystems.
·
Transaktionssicherheit im Zusammenspiel mit Datenbanken. Falls Daten aus
einem relationalen Datenbanksystem zwischengespeichert werden, ist entscheidend,
dass der Cache den von der Datenbank benutzten Isolationslevel (siehe Kapitel 3.3)
bei
Transaktionen
unterstützt.
Beispielsweise
dürfen
nach
einem
Transaktionsrollback keine in der Transaktion geänderten Werte im Cache
verbleiben.
Die folgenden Unterkapitel behandeln diese zentralen Punkte detaillierter und stellen
mögliche Lösungen dar.
Der im Rahmen dieser Diplomarbeit entworfene Prototyp enthält unter anderem die
Komponente DCache, ein nutzbares Java-Framework für verteilte, transaktionssichere
Caches, welches Lösungen für diese Anforderungen anbietet. Für eine genaue
Architekturbeschreibung wird auf Kapitel 6 verwiesen.
5.4.1 Transparenz
Um mit einem Cache-Framework eine möglichst hohe Transparenz in komplexen
Systemen zu erhalten, ist es sinnvoll, nach Einsprungpunkten in der Architektur zu suchen,
in die sich ein Cache-Framework beispielsweise mittels Proxy-Pattern [GHJ+95] einbetten
lässt, um Ergebnisse von Methodenaufrufen oder Datenzugriffen möglichst transparent
zwischenzuspeichern.
60
Für eine mögliche komplett transparente Integration eines verteilten Cachesystems in eine
EJB-Architektur wird auf Kapitel 6 verwiesen.
In großen Applikationen ist es oft sinnvoll, diese Proxyfunktionalität als eine Art zentrale
Verwaltung für schreibende und lesende Zugriffe auf den Cache zu modellieren. Eine
Aufteilung auf mehrere Cache-Frameworks und damit das Fehlen dieser zentralen
Verwaltung würde zu einigen Implikationen führen:
·
Falls Verdrängungsstrategien verschiedener Cache-Frameworks sich an der Größe
des noch verfügbaren Speichers der Java-Virtual-Machine orientieren, kann es hier
zu schweren Zusammenstössen kommen. Die einzelnen Cache-Frameworks
versuchen nicht synchronisiert, Speicher für neue Cacheeinträge zu allokieren oder
durch Invalidierung bestehender Einträge Speicher freizugeben. Dies kann zu
einem undeterministischen Verhalten führen, in dem einer der Caches komplett
geleert wird, weil er davon ausgeht, dass kein Speicher mehr verfügbar ist, wogegen
ein anderer diesen freiwerdenden Speicher direkt für die Allokation neuer Einträge
benutzt.
·
Durch sogeannntes Cache Bypassing [PJ03, Kap.4.3] kann es ohne zentrale
Verwaltung für alle schreibenden Zugriffe zu Inkonsistenzen im Cache kommen,
da auf Grund von fehlendem Wissen über eine Datenänderung das CacheFramework eine Invalidierung nicht anstossen kann. Dieses Problem besteht auch
dann, wenn mehrere Cache-Frameworks genutzt werden, deren benutzte
Datenmengen sich überschneiden, oder falls Daten direkt im Datenbanksystem
geändert werden.
·
Es wäre schwieriger bis unmöglich, korrekte Abhängigkeiten zwischen einzelnen
Cacheeinträgen automatisch zur Laufzeit zu modellieren (siehe Kapitel 5.4.5,
Automatisches Auffinden von Abhängigkeiten).
·
Durch die Nutzung mehrerer Cache-Frameworks wird die Notwendigkeit
geschaffen, eine Instanz zu bestimmen, die entscheidet, welches der CacheFrameworks für bestimmte Anfragen zu verwenden ist. Falls diese Entscheidung
zur Laufzeit durch eine automatisierte programmierte Strategie getroffen wird,
existiert an genau dieser Stelle doch der gewünschte zentrale Zugriffspunkt, falls die
Aufgabe aber dem Applikationsentwickler zugesprochen wird, ist dies ein Bruch
des Grundsatzes der Wahrung der Transparenz.
Die Schaffung einer zentralen Verwaltungsstelle für das komplette Cachehandling macht es
also einfacher, für eine Wahrung der Transparenz zu sorgen, da der Applikationsentwickler
von Aufgaben und Entscheidungen im Bereich des Cachehandlings entlastet wird.
61
5.4.2 Skalierbarkeit
Falls Zwischenspeicher in verteilten skalierten Systemen eingesetzt werden müssen, ist eine
entscheidende Anforderung die korrekte Synchronisation der einzelnen Cacheknoten.
Änderungen müssen nahezu gleichzeitig auf allen Servern ausgeführt werden oder
zumindest dürfen Cacheanfragen zu einer vorgegebenen Zeit auf verschiedenen Servern
keine unterschiedlichen Ergebnisse liefern. Abbildung 12 zeigt den Ablauf einer möglichen
verteilten Cache-Synchronisation.
Server 1
1
Notifier
3
Cache
4
2
Server N
5
Receiver
6
Cache
7
DB
Abbildung 12: Synchronisation in verteilten Caches
1. Der Client beabsichtigt einen Wert zu ändern und teilt dies dem Cache-Framework
mit.
2. Das Framework schreibt den geänderten Wert in die Datenbank.
3. Der Notifier wird damit beauftragt, die anderen Knoten über diese Änderung zu
informieren.
4. Der Notifier versendet über ein ausgewähltes Kommunikationsprotoll eine
entsprechende Nachricht.
5. Die Receiver der anderen Knoten empfangen diese Nachricht.
6. Sie teilen dem jeweils lokalen Cache-Framework die Nachricht mit.
7. Das Framework sorgt dafür, dass die jeweiligen lokalen Caches wieder in einen
konsistenten Zustand versetzt werden.
62
Für eine entsprechende Implementierung dieser Synchronisation gilt es die folgenden
Fragen zu klären:
·
Wie sieht ein geeignetes Kommunikationsprotokoll zwischen lokaler Cache-Instanz
und den restlichen Knoten aus?
·
Wie werden die entfernten Cache-Instanzen in einem konsistenten Zustand
gehalten? Geschieht dies durch Objektreplikation oder durch Invalidierung?
Auswahl des Kommunikationssprotokolls
Ein gutes Cache-Framework erlaubt den Austausch des Kommunikationsprotokolls
zwischen den einzelnen Knoten. Es werden hier drei Möglichkeiten vorgestellt, wie diese
Kommunikation aussehen kann und wo die jeweiligen Vor- und Nachteile liegen.
Verwendung von UDP Multicast:
Die Verwendung des UDP (User Data Protocol) Multicast Protokolls ist eine sehr
performante Möglichkeit, die Kommunikation von verteilten Caches zu implementieren.
Allerdings ist auf Grund der Architektur des UDP-Protokolls Integritätssicherheit nicht
gegeben, da der Sender nicht über Erfolg oder Misserfolg der Nachrichtenübermittlung
informiert wird. Auch ist die Reihenfolge der Pakete nicht fixiert, es kann also nicht davon
ausgegangen werden, dass die Nachrichten in der Reihenfolge beim Empfänger eintreffen,
in der sie versendet wurden.
Ein Vorteil der Verwendung eines Multicast Protokolls ist, dass weder auf allen beteiligten
Knoten die Adressen der jeweils anderen möglichen Nachrichtenempfänger bekannt sein,
noch eine zentrale Stelle alle beteiligten Knoten kennen muss. Durch Multicast werden die
Nachrichten über das gesamte Netzwerk ausgesendet (Broadcast) ohne zu wissen, wer die
Daten empfängt.
Die einfache Handhabung und der bei guter Konfiguration in einem lokalem Netzwerk
unwahrscheinliche Verlust von Paketen macht das UDP-Protokoll für den Praxiseinsatz
sehr interessant.
Verwendung von TCP:
Da auf Grund der Architektur von TCP (Transmission Control Protocol, [Com93])
festgestellt werden kann, ob gesendete Daten korrekt empfangen wurden, ist dieses
Kommunikationsprotokoll für Szenarien geeignet, bei denen es auf absolute Sicherheit der
Integrität des Caches ankommt.
Diese Sicherheit erkauft man sich allerdings durch einen gravierenden Performanceverlust
gegenüber der UDP Multicast Lösung, da auf die Rückmeldung aller beteiligter Knoten
gewartet wird, bevor die Programmausführung fortgesetzt werden kann.
63
Da TCP keine direkte Möglichkeit eines Multicast bietet, bedeutet dies einen Mehraufwand
in der Verwaltung der einzelnen Cache-Knoten. Die Implementierung dieser Aufgabe kann
beispielsweise mittels geeignetem Publish/Subscribe-Pattern [OAA+00] vorgenommen
werden.
Verwendung von JMS:
Mit der JMS (Java Messaging System) API wird eine einheitliche Schnittstelle zur Verfügung
gestellt, die auch von Cachesystemen für die Kommunikation zwischen einzelnen Knoten
zu nutzen ist.
Durch die JMS-Implementierungen sind sowohl eine asynchrone Übermittlung von
Nachrichten wie bei UDP Multicast als auch eine synchrone transaktionsgeschütze wie
durch Einsatz von TCP-Streams möglich.
Der Einsatz von JMS eignet sich besonders beim Einsatz in EJB-Umgebungen, da alle
J2EE-basierten Applikationsserver laut Spezifikation verpflichtet sind, JMS anzubieten. Es
werden allerdings von keinem dem Autor bekannten JMS-System die Performancewerte
eines einfachen UDP-Multicast-Algorithmus wie im DCache-Framework des Prototyps zu
dieser Arbeit (siehe Kapitel 6) erreicht. Für den Einsatz in hochskalierten Systemen mit
wenig Anforderungen an das Messaging-System ist eine JMS-Lösung nicht immer zu
empfehlen.
Objektreplikation oder -invalidierung?
Es existieren zwei unterschiedliche Arten, eigene Einträge oder Einträge in anderen
Knoten über Änderungen zu informieren:
1. Man überschreibt den Wert im Cache durch den neuen Wert bzw. schickt ihn an
andere Server, wo dieser gesetzt wird. (Object Replication, siehe auch [BP96]).
2. Durch Senden eines Invalidierungsrequests mit der Intention, den Eintrag auf den
anderen Servern zu invalidieren - dies bedeutet ihn aus dem Cache zu entfernen.
Der neue Wert kann in diesem Fall entweder durch explizites unmittelbares Lesen
aus der Datenbank oder durch ein sogenanntes Lazy-Loading gesetzt werden. In
diesem Fall würde erst durch ein Cache-Miss (siehe Kapitel 5.4.3) beim nächsten
Zugriff ein Neulesen aus der Datenbank erzwungen werden. Ein Pattern, welches
dieses Vorgehen beschreibt ist das Propagate Cache Updates Pattern in [Bro02]).
Auf den ersten Blick erscheint es effizienter und sinnvoller, den neuen vorliegenden Wert
direkt in den Cache zu schreiben (man nennt dieses Vorgehen write through) statt ihn nur
zu invalidieren. Es hat den Vorteil, dass bei einem erneuten Zugriff dieser direkt verfügbar
ist und nicht neu geladen werden muss.
64
Allerdings erkauft man sich diesen Vorteil in der Praxis durch Probleme, die zu lösen sind:
·
Die Größe der zu setzenden Daten variiert und ist nicht bekannt, ab einer gewissen
Größe kann sich ein Versenden im Netzwerk aufwendig und unperformant
gestalten. Als Beispiel sei das Setzen von Bildern in LOB-Feldern genannt.
Dagegen besitzen Invalidierungspakete immer eine feste Größe, sie enthalten nur
den eindeutigen Schlüssel des Cacheeintrages, nicht dessen Inhalt.
·
Es ist nicht möglich, nicht serialisierbare Objekte zu cachen. Diese müssen über das
Netz transportierbar und nicht abhängig von der gerade laufenden Virtual-Machine
sein. Dies macht es unmöglich, ohne Sonderbehandlung Objekte wie FileDeskriptoren oder Datenbankverbindungen durch das Cache-Framework
zwischenzuspeichern.
·
Es ist möglich, dass die an andere Server versendeten Objekte von dort laufenden
Transaktionen noch nicht eingesehen werden dürfen (beispielsweise durch eine
READ_COMMITTED Isolation, siehe dazu Kapitel 3.3). Es kann also zu
ungültigen Einträgen im Cache kommen. Im Falle einer einfachen Invalidierung
lässt sich dieses Problem einfacher lösen (siehe Kapitel 5.4.4)
Auch wird der obengenannte Vorteil der direkten Verfügbarkeit neuer Cacheeinträge
verschwindend klein, wenn der Cache in Applikationen einsetzt wird, auf die überwiegend
lesend zugegriffen wird.
5.4.3 Performanz und Verdrängung
Cachesysteme für verteilte objektorientierte Anwendungen halten ihre Daten meist für
schnellen Zugriff im Hauptspeicher der lokalen Umgebung. Warum dies so ist, zeigt
Abbildung 13: Die Zugriffsgeschwindigkeit auf lokale Daten ist um ein vielfaches höher als
bei Zugriff auf externe Speichermedien wie Festplatten oder Netzwerkresourcen.
Ein limitierender Faktor für die Haltung der Daten im Hauptspeicher ist die Datenmenge.
Es ist selten der Fall, dass die aufsummierte Größe aller jemals benötigten Daten die
verfügbare Hauptspeichergröße unterschreitet.
Diese verfügbare Größe wird bei Nutzung von Java durch den sogenannten Maximum
Heap angegeben, dies ist die für das Javaprogramm zu nutzende Menge des vorhandenen
Hauptspeichers. Der Maximum Heap war lange Zeit aufgrund der benutzten 32bitArchitektur der Betriebssysteme und der Java-Virtual-Machine auf einen Wert unter 4GB
beschränkt. Durch 64-bit Betriebssysteme und eine 64bit-Version der Java-Virtual-Machine
in der Version 1.4.1 (z.B. unter HP-UX11i RISC [Hew03b]) ist diese Grenze zwar gefallen,
aber dennoch ist es (oft auch aus Kostengründen) nicht möglich, genug Hauptspeicher zur
Verfügung zu stellen.
65
Sehr gut
Lokale Daten im Hauptspeicher
Zugriffsgeschwindigkeit
schlecht
Lokale Daten in Dateien/Datenbanksysteme
Entfernte Daten im Hauptspeicher
Entfernte Daten in Dateien/Datenbanksystemen
Abbildung 13: Zugriffszeit nach Ort der Datenhaltung
Man benötigt also eine Strategie, nach der ausgewählt werden kann, welche Einträge
gecacht werden sollen und welche nicht.
Eine Möglichkeit ist es, dem Applikationsentwickler diese Aufgabe zuzuteilen. Er kann
entscheiden, welche Daten er über das Cache-Framework liest und welche er unter
Umgehung des Caches liest. Diese Lösung verstösst allerdings gegen folgende Grundsätze:
·
Transparenz (siehe Kapitel 5.4.1).
·
Zentralisierte Architektur. Alle Leseanfragen sollten über genau ein CacheFramework laufen.
·
Modularisierung. Die Geschäftslogik soll von anderen Diensten entkoppelt werden.
Zusätzlich ist es oft zur Entwicklungszeit nicht möglich, abzusehen, wie viele und welche
Einträge am sinnvollsten zwischengespeichert werden können.
Die bessere Lösung ist es, alle Anfragen die zentrale Cachezugriffsschicht durchlaufen zu
lassen. Eine zentrale automatisierte Logik entscheidet nach einer bestimmten Strategie,
welche Einträge bei gefülltem Cache, also Erreichen der Speicherobergrenze entfernt
werden können, um Platz für neue Einträge zu schaffen.
Mögliche Strategien hierzu sind die folgenden:
·
66
FIFO (First In First Out). Bei Verwendung des FIFO-Algorithmus fallen diejenigen
Cacheeinträge, die die längste Verweildauer im Cache besitzen, als erstes wieder
heraus. Diese Strategie ist sehr einfach durch eine Art Zeitstempel an jedem
Cacheeintrag zu realisieren, hat aber den Nachteil, dass sie nicht berücksichtigt, wie
häufig ein Cacheeintrag abgefragt wurde. Dies führt dazu, dass häufig frequentierte
Einträge unnötigerweise oft aus- und wieder eingelagert werden.
·
LRU (Least Recently Used). Hierbei fallen nicht wie bei FIFO diejenigen Einträge
heraus, die die längste Verweildauer, sondern diejenige, die die längste Inaktivität
im Bezug auf Zugriffe besitzen. Auch diese Strategie ist einfach umzusetzen. Die
Cacheeinträge werden in einer verketteten Liste gehalten und bei jedem Zugriff
wird ein Eintrag an den Anfang dieser Liste gestellt. Einträge am Ende der Liste
werden zuerst entfernt.
Eine Abwandlung des LRU-Algorithmus stellt der LRU-K Algorithmus [NNW93]
dar, der gerade bei kleinen Cachegrößen Vorteile gegenüber dem Standard-LRUVerfahren aufweisen kann.
Weitaus komplexere Algorithmen nehmen in die Entscheidung weitere Kriterien wie den
Speicherverbrauch oder die mittlere Berechnungszeit des Eintrages auf. Je nach
Gewichtung der einzelnen Kriterien ergeben sich unterschiedliche Formeln, mit denen die
‚Wichtigkeit’ bestimmt und letztendlich über den Verbleib des Eintrages im Cache
entschieden wird.
In der Praxis zeigt sich, dass Verdrängungsstrategien, die auf LRU-Algorithmen aufbauen,
sehr gut eingesetzt werden können. Sie bieten eine hinreichend gute Balance zwischen
einfacher und performanter Implementierung und Güte der Auswahl der zu entfernenden
Cacheeinträge.
Diese Güte der Auswahl oder die Effektivität des Caches kann man mit der sogeannten
Cache-Hit-Ratio bestimmen. Sie bestimmt im Gegensatz zu der Cache-Miss-Ratio den
Prozentsatz, mit der Anfragen an Daten über den Cache beantwortet werden konnten.
In den meisten Anwendungen ist zu beobachten, dass die Anzahl der Zugriffe auf einzelne
Daten nicht gleichverteilt ist, das bedeutet, dass bestimmte Datensätze viel häufiger gelesen
werden als andere. Der Autor kann aus der Erfahrung mit großen e-commerce-Systemen
mit mehr als 100.000 angebotenen Produkten die folgende Aussage treffen:
Die 10% meistfrequentierten Produkte sind für mindestens 75% der Produkt-Zugriffe
verantwortlich.
Falls also der Cache nur 10% der Produkte halten könnte, würde bei Verwendung einer
LRU-Verdrängungsstrategie die Cache-Hit-Ratio annähernd 75% betragen. Siehe dazu auch
[PJ03, Abb.18].
67
5.4.4 Transaktionssicherheit
Die Transaktionssysteme moderner relationaler Datenbanken sind robust, sicher und
performant implementiert. Sie haben ihre Einsatzfähigkeit in der Praxis in den letzten
Jahren in großen Projekten unter Beweis gestellt.
Falls Caches in Verbindung mit relationalen Datenbanksystemen eingesetzt, und
Transaktionen verwendet werden sollen, ist es entscheidend, dass auch das CacheFramework transaktionssicher agiert und die ACID-Kriterien (siehe Kapitel 3.3) erfüllt.
Um von der Implementierung der Datenbankhersteller profitieren zu können, ist es
sinnvoll zu versuchen, möglichst viel transaktionale Funktionalität an die Datenbank zu
übertragen. Dies ist allerdings nicht vollständig möglich, so dass auch geeignete
Algorithmen und Strategien im Cache-Framework umgesetzt werden müssen. Dieses
Kapitel versucht hier die Grundsätze und Ideen zu präsentieren.
Das DCache-Framework (siehe Kapitel 6.1) enthält eine mögliche Implementierung eines
transaktionssicheren Caches und kann als zusätzliche Informationsquelle herangezogen
werden.
Beim transaktionssicheren Zwischenspeichern von Daten muss zwischen den folgenden
beiden Arten unterschieden werden [FCL97, Kap. 1.2]:
·
Intra-Transaction Caching, welches Daten nur innerhalb einer Transaktion
zwischenhält, nachfolgende oder gleichzeitig laufende andere Transationen haben
keinen Zugriff mehr auf diese Daten.
·
Inter-Transaction Caching, bei dem gecachte Daten auch zwischen einzelnen
Transaktionskontexten übertragen werden können.
Ziel der folgenden Ausführungen sind die komplexeren und schwieriger zu realisierenden
Inter-Transaction Caches, da erst diese bei den in J2EE Architekturen auftretenden
typischen zeitlich kurzen Transaktionen den gewünschten Performancevorteil gegenüber
einer vollständig ungecachten Lösung bringen.
Die unterschiedlichen Transaktionsisolationsmodi Read Uncommitted, Read Committed und
Repeatable-Read erfordern auf Seite des Caches Implementierungen unterschiedlicher
Komplexität, die jeweils aufeinander aufbauend umgesetzt werden können. Die
Betrachtung der Serializable-Isolation erfolgt im Rahmen dieser Arbeit nicht.
Durch die Verwendung eines geeigneten Patterns (z.B. Strategy-Pattern [GHJ+95]) ist es
möglich, diese unterschiedlichen Implementierungen in einem Cache-Framework zu
vereinen. Durch ein Abfragen des aktiven Datenbankisolationsmodus (dies ist
beispielsweise über die JDBC-Schnittstelle möglich) kann eine automatische Adaption
erfolgen. Es wird im Folgenden davon ausgegangen, dass als Strategie für Cache-Updates
keine Object-Replication oder Write-Through, sondern einfache Invalidierung verwendet
wird (siehe Kapitel 5.4.2).
68
5.4.4.1
Cache mit Read-Uncommitted Isolierung
Die Verwendung einer Read-Uncommitted Isolierung bedeutet, dass jegliche Änderungen
aus anderen Transaktionen sofort in der eigenen Transaktion gesehen werden können.
Dies ist das Standardverhalten eines nicht transaktionalen Caches mit korrekten
Invalidierungen. Aus diesem Grund muss zur Erfüllung des Isolations-Kriteriums keine
weitere Anpassung erfolgen.
Anpassungen sind allerdings für die Wahrung des Atomitätskriteriums nötig, wonach bei
Abschluss einer Transaktion entweder alle Änderungen (erfolgreicher Abschluss,
COMMIT) oder keine Änderungen (nicht erfolgreicher Abschluss, ROLLBACK)
übernommen werden.
Der COMMIT Fall: Da im Zuge der Transaktionsabarbeitung jegliche Updates durch
eine Invalidierung auf einen konsistenten Stand gebracht wurden, ist sichergestellt, dass
dies zum Abschluss der Transaktion für die Summe aller Änderungen geschehen ist. Es ist
keine zusätzliche Anpassung des Caches nötig.
Der ROLLBACK Fall: Nachdem die Datenbank ein Rollback ausgeführt hat und damit
alle während der Transaktion geänderten Daten auf ihren Ursprungswert zurückgesetzt hat,
muss nun auch sichergestellt werden, dass die entsprechenden Daten im Cache nicht mehr
vorhanden sind, da sie inkonsistente Werte enthalten könnten. Ein Ändern von Daten
während der Transaktion führt zwar automatisch schon zu einer Invalidierung des
entsprechenden Cacheeintrages, doch ist es möglich, dass dieser noch während der
Transaktionslaufzeit durch einen Zugriff neu geladen wird. Er hätte dann nach einem
Datenbank-Rollback einen inkonsistenten Wert.
Eine mögliche Implementierung sieht wie folgt aus: Der Cache registriert jede
Invalidierung während der Transkation in einer Liste und führt am Ende der Transaktion
nach dem Datenbank-Rollback diese Invalidierungen erneut aus, um sicherzustellen, dass
die entsprechenden Objekte nicht mehr im Cache verfügbar sind.
5.4.4.2 Caches mit Read-Committed Isolierung
Die Umsetzung eines Inter-Transaction Caches mit Read-Committed Isolierung baut auf
der vorgestellten Lösung für die Read-Uncommitted Isolierung auf.
Zusätzlich muss sichergestellt werden, dass beim lesenden Zugriff keine Daten aus nicht
abgeschlossenen Transaktionen gelesen werden können.
Hier gilt es zwei Fälle zu unterscheiden
1. Der lesende Zugriff greift auf die Datenbank durch, da der entsprechende Wert
nicht im Cache verfügbar war: In diesem Fall sorgt die Datenbankisolierung dafür,
dass ein konsistenter Wert zurückgeliefert wird. Es ist keine Anpassung am CacheFramework nötig.
69
2. Der lesende Zugriff greift nicht auf die Datenbank durch, der entsprechende Wert
ist im Cache verfügbar: Nun besteht das Problem, dass dieser Wert inkonsistent
sein kann, da er während einer noch laufenden Transaktion in den Cache geladen
worden sein könnte. Es besteht die Möglichkeit, dass dieser Wert von der eigenen
Transaktion nicht gelesen werden darf.
Eine mögliche Lösung für dieses Problem ist die folgende: Nachdem ein Objekt in einer
Transaktion invalidiert wurde, wird es für die Dauer der Transaktion nicht mehr in den
Cache geladen. Gewünschte Zugriffe greifen immer auf die Datenbank durch. Am Ende
der Transaktion ist keine weitere Aktion des Cache-Frameworks nötig.
Damit sind auch die Bedinungen für eine Read-Committed Isolierung erfüllt.
Die Implementierung bedeutet allerdings einen gewissen Performanceverlust, da in einer
Transaktion geänderte Daten für die gesamte Dauer dieser Transaktion nicht mehr gecacht
werden können.
5.4.4.3 Cache mit Repeatable-Read Isolierung
Die zusätzliche Anforderung an einen Cache mit Repeatable-Read Isolierung ist, dass bei
aufeinander folgenden Zugriffen innerhalb einer Transaktion keine unterschiedlichen
Werte gelesen werden dürfen, auch wenn zwischenzeitlich eine andere abgeschlossene
Transaktion diesen Wert geändert hat.
Wir kommen nun an den Punkt, an dem die Strategie des Einsatzes eines einzelnen
gemeinsamen Caches für alle Clients und Transaktionen abgeändert werden muss. Um
sicherzustellen, dass eine Transaktion nur ihre Werte lessen kann, wird ein zusätzlicher
Intra-Transaction Cache eingeführt, der bei lesenden Zugriffen vor dem globalen Cache
abgefragt wird. Dieser Intra-Transaction-Cache wird durch erfolgreichen oder nicht
erfolgreichen Abschluss der Transaktion komplett verworfen.
Es gilt ein geeignetes Speichermanagement und eine geeignete Verdrängungsstrategie zu
finden, die im Zusammenspiel mit dem globalen Cache nicht zu einem zu hohen
Speicherverbrauch führt.
Abbildung 14 zeigt einen Lesezugriff und die drei möglichen Wege durch den Intra- und
Inter-Transaction Cache:
1. Der lokale Intra-Transaction Cache enthält den gewünschten Cachewert und liefert
ihn direkt an den Client zurück.
2. Der Intra-Transaction Cache enthält die Daten nicht und leitet auf den globalen
Cache weiter. Dieser liefert die Daten zurück, sie werden im Intra-Transaction
Cache gesetzt und an den Client geliefert.
70
3. Weder der Intra-Transaction Cache noch der globale Inter Transaction Cache
können das gewünsche Cacheelement liefern. Es wird berechnet (z.B. durch
Zugriff auf ein externes Datenbanksystem) und zum Client geliefert, nachdem es in
beiden Cacheschichten gesetzt wurde.
1
get
IntraTransaction
Cache
2
return
get
available
return
get
n/a set
get
InterTransaction
Cache
3
return
n/a set
get
available
return
n/a set
get
Database
return
return
available
Abbildung 14: Wege durch den Intra- und Inter-Transaction Cache
Der globale Cache dient nunmehr nur noch zum erstmaligen Abfangen eines Zugriffes
innerhalb einer Transaktion, alle weiteren Zugriffe werden zur Wahrung des RepeatableRead-Kriteriums aus dem Intra-Transaction Cache beantwortet.
5.4.5 Automatisches Auffinden von Abhängigkeiten
Falls der Cachezugriff zentralisiert abläuft und damit das Cache-Framework über jeden
Zugriff informiert wird, bietet sich die Möglichkeit, Abhängigkeiten zwischen
Cacheeinträgen zu lokalisieren und diese zu nutzen, um ein konsistentes System mit
möglichst geringer Invalidierungsquote zu schaffen.
Wir betrachten für die folgenden Ausführungen einen Cacheeintrag als einen Container,
welcher entweder atomare zwischengespeicherte Daten oder wiederum Cacheeinträge
enthält. Dieser Aufbau entspricht dem Composite Pattern [Wuy98].
Ein Cacheeintrag A ist von einem Cacheeintrag B abhängig im Bezug auf eine
Invalidierung, wenn die Möglichkeit besteht, dass bei Änderung (Invalidierung) von Wert B
71
sich ebenfalls Wert A gändert hat und aus diesem Grund zur Aufrechthaltung der
Konsistenz sofort invalidiert werden muss. Auf die Mengenlehre bezogen kann man diese
Abhängigkeit von A zu B ausdrücken als: „B ist Teilmenge von A.“
Die Invalidierungsabhängigkeit wird in einfachen Cachemodellen durch den
Applikationsentwickler von außen festgelegt. Dies kann beispielsweise durch das Aufbauen
einer Baumstruktur wie in Kapitel 6.1.1 erläutert, geschehen. Um nun die Bestrebung nach
weitgehender Transparenz weiter voranzutreiben, wird versucht, auch diese Aufgabe dem
Entwickler abzunehmen und automatisiert zu beschreiben.
Aufgrund der Tatsache, dass die Invalidierungsabhängigkeit von Eintrag A zu B immer
genau dann besteht, wenn zur Berechnung von A der Wert von B benötigt wird, lässt sich
dieses gewünschte automatisierte System aufbauen, welches die Abhängigkeiten modelliert.
Das Cache-Framework kann beim Erzeugen von Cacheeinträgen Daten über die
Abhängigkeiten sammeln, speichern und im Falle von Invalidierungsanfragen benutzen,
um alle abhängigen Einträge ebenfalls zu invalidieren.
Anhand von Abbildung 15 wird dieser Ablauf graphisch dargestellt.
Client 1
1
Client 2
berechnet
PageCacheUnit
Webseite
2
7
berechnet
invalidiert
ProductCacheUnit
Produkt
4
berechnet
NameCacheUnit
Produktname
5
berechnet
6
Abhänging
von ‘Produkt’
invalidiert
8
Abhängig
3 von ‚Webseite‘
invalidiert
9
DB
Abbildung 15: Automatisches Auffinden von Abhängigkeiten
72
Füllen des Caches
1. Der komplette Inhalt einer Webseite wird über das Cache-Framework berechnet
und in einer Instanz der Klasse PageCacheUnit abgelegt. Der Client greift erstmalig
darauf zu, der Wert befindet sich nicht im Cache und wird mittels compute()Methode berechnet.
2. Auf dieser Webseite soll unter anderem in einem bestimmten Bereich ein Produkt
dargestellt werden. Die Berechnungsmethode für die komplette Seite holt sich also
über das Cache-Framework die HTML-Fragmente zur Darstellung des Produktes.
Intern wird die entsprechende Berechnungsmethode angestossen.
3. Das Cache-Framework merkt sich, dass die compute()-Methode für die
ProductCacheUnit aus der compute()-Methode der PageCacheUnit aufgerufen
worden ist.
4. Um das Produkt darzustellen, wird unter anderem der Name des Produkts
benötigt.
5. Das Cache-Framework merkt sich die Abhängigkeit der ProductCacheUnit von der
NameCacheUnit.
6. Die NameCacheUnit holt sich die benötigten Daten aus dem Datenbanksystem
Die gesamte Seite kann nun angezeigt werden, der Cacheeintrag für die komplette Seite
liefert bei erneutem Zugriff direkt den gerenderten HTML-Code zurück.
Invalidieren
7. Nun ändert ein zweiter Client den Namen des Produkts. Die NameCacheUnit wird
invalidiert.
8. Das Cache-Framework überprüft, ob der Name in anderen Cacheeinträgen
verwendet wurde. In diesem Fall wurde er zur Berechnung der ProductCacheUnit
benutzt. Diese wird ebenfalls invalidiert.
9. Das Cache-Framework stellt fest, dass die Berechnung der PageCacheUnit von der
ProductCacheUnit abhängt. Auch diese muss invalidiert werden.
73
74
6 CCMP – CACHED CMP
CCMP (Cached Container Managed Persistence) stellt den im Rahmen dieser Diplomarbeit
entwickelten Prototyp dar. Es ist ein mit den Technologien Java, EJB, und XSLT
(Extended Stylesheet Language Transformations) [W3C03] entwickeltes Framework,
welches die Machbarkeit zeigen soll, eine hochskalierbare, performante Persistenzschicht
für Entity-Beans zu entwickeln. Der Prototyp erhebt in keinem Fall Anspruch darauf, die
komplette Funktionalität eines EJB-Containers nachzustellen, doch zeigt er eine
funktionsfähige Möglichkeit, mit der Entity-Beans auf mehrere Applikationsserver verteilt
werden können. Durch die benutzten verteilten Caches wurde in Lasttests eine deutliche
Performancesteigerung erzielt (siehe Kapitel 6.4).
CCMP versucht, viele der in den vorigen Kapiteln vorgestellten Architektur- und
Entwurfsmuster durch deren Zusammenfügen zu einem in der Praxis einsatzfähigen
Framework weiter zu veranschaulichen. Zusätzlich werden weitere Idiome speziell für die
objektorientierte Programmiersprache Java vorgestellt.
Durch das Design und die Implementation von CCMP wurde versucht die folgende auf
technischer Ebene formulierte Frage zu beantworten:
Wie können die vorgestellten Datenzugriffs- und Cachepatterns so in die J2EE-Umgebung
integriert werden, dass sie für den Entwickler von Enterprise Java Beans möglichst
unsichtbar und transparent arbeiten und damit eine performante, skalierbare und
ausfallsichere Anwendung geschaffen werden kann?
Der Prototyp ist sehr modular aufgebaut. Dies ermöglicht eine hohe Wiederverwertbarkeit
und erhöht die Übersichtlichkeit der Implementierung [Col96]. Die einzelnen
Komponenten von CCMP sind in Abbildung 16 dargestellt.
·
DCache (Lightweight Distributed Cache) stellt einen transparenten verteilten
transaktionssicheren Cache für Java-Objekte dar.
·
BMP2CMP (BMP to CMP Converter) generiert aus CMP Beans unter Nutzung des
DCache-Frameworks BMP Beans. Die komplette Persistenzschicht wird
automatisch generiert und ermöglicht durch das Deployment der Enterprise
JavaBeans auf mehrere Applikationserver die Schaffung eines verteilten skalierten
Systems.
·
J2EEDemo: Eine J2EE Beispielapplikation, bestehend aus CMP Entity-Beans,
Sessionbeans und einer Web-Applikation. Diese Applikation kann in ihrem
Ursprungszustand als CMP-basierte Applikation oder nach Wandlung durch
BMP2BMP in eine BMP-basierte Applikation in jedem J2EE 1.3 kompatiblen
Applikationsserver deployed werden.
75
In den nachfolgenden Unterkapiteln werden diese einzelnen Komponenten detaillierter
vorgestellt und ihre Abhängigkeiten untereinander erläutert. Eine ausführliche Installationsund Bedienungsanleitung finden Sie im Anhang 1 (Nutzung des Prototyps).
EJB-Container
J2EEDemo
CMP Applikation
nutzt
erbt von
CCMP (BMP Generator)
generiert
BMP-Applikation
nutzt
DCache
Abbildung 16: Die einzelnen Komponenten des CCMP-Frameworks
6.1 DCache
Eine wichtige Komponente des Prototyps ist das Lightweight Distributed Cache Framework
(DCache). Es stellt einen transparenten verteilten transaktionssicheren Cache für JavaObjekte dar und ist vollständig unabhängig vom Rest des Prototyps, also nicht abhängig
oder speziell abgestimmt auf den Einsatz mit den anderen Komponenten und somit auch
für andere Aufgaben nutzbar.
Die wichtigsten Funktionalitäten sind nachfolgend aufgelistet:
76
·
Möglichkeit einer transparenten Nutzung in Java-basierten Applikationen.
·
Verdrängungsstrategie nach LRU (Least Recently Used) Algorithmus und
konfigurierbare Maximalanzahl von Cacheeinträgen.
·
Hierarchische Anordung der Cacheeinträge in einer Baumstruktur, dadurch
Möglichkeit zum Invalidieren ganzer Teilbäume.
·
Bei Nutzung einer relationen Datenbank Transaktionssicherheit bis zur ReadCommitted Isolierung.
·
Austauschbare Implementierung des verwendeten Protokolls für verteilte
Invalidierungen in verteilten Systemen. Eine Lösung basierend auf UDP Multicast
ist integriert.
The DCache Framework - UML class diagram
de.tum.in.dcache.local.LocalInvalidator
interface
Invalidator
Helper packages:
+sendInvalidate:void
de.tum.in.dcache.util.Log
+sendInvalidate:void
de.tum.in.dcache.udp.UDPInvalidator
+info:void
+debug:void
+severe:void
+throwing:void
+throwing:void
+ID_DELIMETER:String
+KEY_DELIMETER:String
-multicastSocket:MulticastSocket
-port:int
-multicastaddress:String
Cache
<<use>>
+UDPInvalidator
+sendInvalidate:void
Thread
de.tum.in.dcache.udp.UDPServer
+PACKETSIZE:int
-port:int
-group:InetAddress
+UDPServer
+run:void
-processDatagramPacket:void
<<use>>
-invalidator:Invalidator
-cache:Cache
-vmid:String
-unitMapTree:Map
-cacheUnits:Map
+getID:String
+getInstance:Cache
-Cache
-addUnit:void
-removeUnit:void
-getCacheUnitMap:Map
-getOrCreateCacheUnitMap:Map
-internalGetCacheUnitMap:Map
#getUnit:AbstractCacheUnit
#getOrAddUnit:AbstractCacheUnit
+invalidate:void
+invalidateInternal:void
-invalidateRecursively:void
de.tum.in.dcache.util.Config
-configFile:File
-prefs:Preferences
+get:String
+getKeys:String[]
+put:void
-store:void
AbstractCacheUnit
-theKey:List
-value:Object
-isValueKnown:boolean
-createdTimestamp:long
+createKey:Object[]
+compute:Object
+invalidate:void
+get:Object
#executeInvalidation:void
-privateGet:Object
timeoutMillis:long
key:List
Abbildung 17: Architekturübersicht DCache
77
8
Abbildung 17 zeigt den Klassenaufbau des DCache-Frameworks . Die zentrale Stelle bildet
die Cache-Klasse, welche den Großteil der Logik beinhaltet. Zur Sicherstellung einer
zentralen Zugriffsschicht (siehe Kapitel 5.4.1) wird das Singleton-Pattern [BMR+96]
genutzt. Damit wird verhindert, dass mehrere Cache-Instanzen in einer Java-VirtualMachine existieren können.
Die Instanz der Cache-Klasse enthält den Baum aller im Cache vorhandenen
Cacheeinträge (Cacheunits). Diese werden in einer verschachtelten java.util.Map
gehalten. Zur schnellen Suche und zur Implementierung des Verdrängungsalgorithmus
werden Verweise auf die Cacheeinträge zusätzlich in einer eindimensionalen LRUMap
[Apa03b] gehalten.
Die Cache-Klasse besitzt genau eine Implementierung des Invalidator-Interfaces,
welche durch Angaben in einer Konfigurationsdatei bestimmbar ist. Nach jeder lokalen
Invalidierung von Cacheeinträgen wird die Methode Invalidator.sendInvalidate()
aufgerufen, mit der in einem verteilten System die anderen Cacheknoten informiert werden
können.
Die einzelnen Cacheeinträge werden durch die abstrakte Klasse AbstractCacheUnit
modelliert. Diese müssen durch die Methode computeKey() einen eindeutigen Schlüssel
zurückgeben, der ihre Position in der Baumstruktur bestimmt und durch den sie
wiederzufinden und zu invalidieren sind. Desweiteren ist die eigentliche Berechnung des
CacheUnits in der Methode compute() zu implementieren. Diese wird nur dann einmalig
aufgerufen, wenn der Wert nicht im Cache vorhanden ist. Kapitel 6.2.3 beschreibt dieses
Vorgehen detaillierter.
6.1.1 Baumstruktur
Die einzelen Cacheeinträge werden in einer Baumsstruktur gehalten. Durch diese
hierachische Anordung ist es möglich, einfache Invaldierungsabhängigkeiten zwischen
Cacheeinträgen zu modellieren und den Cache auf diese Weise zu strukturieren (siehe auch
Hierachical Views Pattern in [Kel03])
Jeder Cacheeintrag (CacheUnit) besteht aus
·
einem für den Cache eindeutigen Schlüssel (engl. Key) vom Typ java.lang.
Object[].
·
dem eigentlichen Wert vom Typ java.lang.Object .
Der Key wird als Liste angegeben und bildet in der Baumstruktur den Pfad zu einem Blatt.
Dieses Blatt wird durch das eigentlich zu cachende Objekt gebildet.
8
In der Abbildung fehlen die Klassen des Package de.tum.in.jdbcwrapper, welches gesondert in Kapitel
6.1.4 beschrieben wird.
78
Abbildung 18 zeigt die im Prototyp verwendete Strukturierung bei Nutzung des DCacheFrameworks für das Caching von Entity-Beans. Die erste Ebene des Caches bildet ein
eindeutiger Identifier der Bean-Klasse (Name des Beans). In der zweiten Ebene fällt die
Entscheidung, ob Attribute zu einer bestimmten Bean-Instanz mit einem bestimmten
Primary Key oder Ergebnisse von Findermethoden gecacht werden sollen. An die
entsprechenden genauen Qualifizierungen der Attributsnamen bzw. FinderMethodensignaturen als Schlüssel in Ebene 3 werden die eigentlich zu cachenden Daten
gelegt.
Keys
Values
Entitystate
(PK #nr)
name
name
code
code
findXY
Finder
result
findAB
Finder
result
Entityname
(Product)
Findermethods
Abbildung 18: DCache-Baumstruktur für das Cachen von Entity-Beans
6.1.2 Cachezugriff
Die primitivste Möglichkeit der Datenzwischenspeicherung ist die Nutzung eines typischen
In-Memory Get/Set-Caches wie ihn z.B. die HashMap darstellt.
Die Benutzung eines solchen Caches führt zu den typischen mehrzeiligen Codefragmenten
zum Datenzugriff:
Object value = cache.getValue(XY);
if( value == null )
{
value = <<getNameFromDataBase>>;
cache.putValue( value );
}
79
Dieses Vorgehen verstößt gegen den Transparenz-Grundsatz (Kapitel 5.4.1) und sollte
vermieden werden. Der Entwickler sollte nicht selbst abfragen müssen, ob ein gewünschter
Wert im Cache vorhanden ist.
Das DCache-Framework ist kein typischer Get/Set-Cache. Die Logik des Berechnens eines
Cachewertes wird von der Verwaltung der Einträge getrennt. Eine beispielhafte Anfrage
sieht folgendermassen aus:
Object value = new NameCalculatorCacheUnit().get();
Die Klasse NameCalculatorCacheUnit enthält in der Methode compute() den Code,
der auf die Datenbank zugreift; die hier aufgerufene get()-Methode entscheidet intern, ob
compute() verwendet werden muss (Daten existieren nicht im Cache) oder ob die Daten
direkt zurückgeliefert werden können (Daten existieren im Cache).
Application
1: get
AbstractCacheUnit
Database
Remote Servers
1.1: compute
1.2: add to cache
2: get
3: invalidate
3.1: remove from cache
3.2: broadcastInvalidate
Abbildung 19: Sequenzdiagramm eines Cachezugriffs
80
Abbildung 19 stellt das grundsätzliche Vorgehen des Cachezugriffes beim DCacheFramework anschaulich dar. Im Sequenzdiagramm werden die drei möglichen Abläufe bei
einem Zugriff auf das Cache-Framework dargestellt.
·
Cache-Miss: Der Client erzeugt eine CacheUnit-Instanz, welche von der Klasse
AbstractCacheUnit abgeleitet ist, die die Berechungsroutine für den
Cacheeintrag enthält und fordert mit Aufruf der Methode get() den gewünschten
Wert an (1). Der Wert ist im CacheUnit noch nicht gesetzt, deshalb wird er durch
Aufruf der compute()-Methode berechnet, also beispielsweise aus der Datenbank
gelesen (1.1). Der zurückgelieferte Wert wird im Cacheeintrag gesetzt und dieser
fügt sich in die Baumstruktur des Caches ein (1.2). Der berechnete Wert wird an
den Client zurückgeliefert.
·
Cache-Hit: Falls der angefragte Wert in der CacheUnit-Instanz gesetzt war, wird
dieser sofort an den Client zurückgeliefert (2).
·
Invalidate: Durch Aufruf der Methode invalidate() (3) wird der Cachewert aus
dem AbstractCacheUnit entfernt. Die gesamte CacheUnit wird aus der
Baumstruktur des Caches genommen (3.1) und mittels sendInvalidate() wird
die Invalidierung auf entfernten Cacheknoten angestossen (3.2).
Um Inkonsistenzen mit der Datenbank auszuschliessen, wird keine Möglichkeit angeboten,
den Wert eines CacheUnits explizit zu setzen. Dies muss immer über den internen Aufruf
der compute()-Methode bei einer get()-Anfrage geschehen.
6.1.3 Invalidierung
Um die Skalierbarkeit des DCache-Frameworks zu sichern, werden einfache InvaldierungsRequests benutzt. Es wird keine Object-Replication (Kapitel 5.4.2 oder [BP96]) verwendet.
Durch die Nutzung der Invalidierungsrequests bleibt die Implementierung übersichtlicher
und es ist einfacher, die verschiedenen Transaktions-isolationslevel zu unterstützen
(Kapitel 5.4.4).
Das in Kapitel 5.4.3 (Performanz und Verdrängung) vorgestellte Verfahren der
Invalidierung löst eine solche nur genau dann aus, wenn der zugehörige Wert geändert
wurde. Das DCache-Framework erweitert dieses Vorgehen durch ein zeitbasiertes, vom
Wert unabhängiges Einleiten des Invalidierungsvorganges. Es gibt die zwei folgenden
miteinander kombinierbaren Arten der Entscheidung, wann Objekte aus dem Cache
entfernt werden sollen:
1. Die Time-based Invalidierung - Cacheeinträge werden unabhängig von der Gültigkeit ihres Wertes nach einer bestimmten Aufenthaltszeit aus dem Cache entfernt.
2. Die Modification-based Invalidierung – Cacheeinträge werden unabhängig von der
Aufenthaltszeit im Cache entfernt, sobald sie ungültig werden.
81
Es muss dem Benutzer bewusst sein, dass durch die ausschliessliche Nutzung einer Timebased Invalidierung inkonsistente Caches geschaffen werden können. Da Werte
unabhängig von der Gültigkeit existieren, ist es möglich, dass in der Zwischenzeit der
entsprechende Wert in der Datenbank geändert wurde und dieser nicht mehr dem im
Cache enthaltenen entspricht.
Dennoch hat die Time-based Invalidierung in der Praxis ihre berechtigten
Anwendungsgebiete. Als Beispiel soll die Darstellung von Devisenkursen einer
Börsenapplikation dienen. Die Kurse können sehr kurzfristigen, minütlichen
Schwankungen unterliegen. Falls es aber vollkommen ausreicht, wenn dem Benutzer der
Anwendung Werte präsentiert werden, die maximal 10 Minuten alt sind, ist es durchaus
legitim, die gecachten Werte nicht bei jeder Kursänderung (modification-based) sondern
zeitgesteuert alle 10 Minuten (time-based) zu entfernen.
Die beiden Invalidierungs-Arten und ihre Konfigurationsmöglichkeiten werden im
nachfolgenden ausführlicher behandelt.
Die Time-based Invalidierung
Die Time-based Invalidierung ist ist für jeden einzelnen Cacheeintrag programmatisch
durch Implementierung der Methode AbstractCacheUnit.getTimeoutMillis()
einzustellen. Angegeben wird hier die maximale Verbleibe-Zeit in Millisekunden.
Ein einfaches Beispiel, welches dafür sorgt, dass der Cacheeintrag nach genau 10 Sekunden
invalidiert wird, sieht folgendermassen aus:
public long getTimeoutMillis()
{
return 10000;
}
Es ist durchaus möglich, eigene Programmlogik in die getTimeoutMillis() Methode
einzubauen Dies kann vor allem bei komplexeren CacheUnits sinnvoll sein.
Folgendes Beispiel zeigt eine Implementierung, die die CacheUnit nicht cachen würde, falls
der Key den String "Order" enthält:
public long getTimeoutMillis()
{
if( getKey().contains("Order") )
{
return -1;
}
else
{
return 1000;
}
}
82
Man sollte beachten, dass diese Methode bei jedem get()-Aufruf auf der CacheUnit
aufgerufen wird. Dies birgt auf der einen Seite ein hohes Performancerisiko - es muss vom
Entwickler auf eine entsprechende Implementierung geachtet werden - ermöglicht aber
auch die sehr flexible Ausrichtung des Timeout-Wertes.
Die Modification-based Invalidierung
Durch die Modification-based Invalidierung wird bei Änderung von Werten explizit eine
Invalidierung angestoßen, welche die ungültigen Cacheeinträge entfernt. Die entsprechende
Funktionalität wird über sogenannte Invalidatoren (Interface de.tum.in.dcache.
Invalidator) implementiert.
Der
zu
verwendende Invalidator wird in der globalen Konfigurationsdatei
cacheconfig.xml festgelegt. Für detaillierte Konfigurationsmöglichkeiten wird auf
Anhang 1 (Nutzung des Prototyps) verwiesen.
Beispiel:
...
<entry key="invalidator" value="de.tum.in.dcache.udp.UDPInvalidator" />
...
Bestandteil des Prototyps sind der Local-Invalidator und der UDP-Invalidator. Es ist aber
jederzeit möglich, weitere Implementierungen zu entwickeln. Die Java API Dokumentation
des Interfaces und der Sourcecode der beiden Beispiele können hier helfen.
·
Der Local-Invalidator (de.tum.in.dcache.local.LocalInvalidator)
Diese Implementierung invalidiert Cacheeinträge nur lokal in der gleichen Java
Virtual-Machine. Dies ist die performanteste Methode, da nur interne Java
Methodenaufrufe verwendet werden und keinerlei Netzzugriff benötigt wird. Aus
diesem Grunde können Invalidierungen nicht verloren gehen, eine Fortführung
des Programms erfolgt erst, wenn eine korrekte Rückmeldung über die
Invalidierung erhalten wurde. Allerdings sind keinerlei verteilte Caches möglich.
Durch Verwendung des Local-Invalidators ist man auf eine Cache-Instanz
beschränkt.
·
Der UDP-Invalidator (de.tum.in.dcache.udp.UDPInvalidator)
Der UDP-Invalidator nutzt das UDP-Multicast-Protokoll zum Versenden von
Invalidierungen über das Netzwerk. Hiermit ist möglich, verteilte Caches
aufzubauen.
Weitere Möglichkeiten sind die Implementierung einer Invaldierung unter direkter
Nutzung des TCP-Protokolls oder eines Messaging-Systems mit JMS-Schnittstelle, welches
vor allem in EJB-Umgebungen interessant sein kann. [Bro02].
83
6.1.4 Transaktionssicherung
Zur Realisierung von Transaktionssicherheit in Zusammenspiel mit relationalen Datenbanken wird eine Implementierung des Transaction Object Patterns [KC97] verwendet.
Ziel ist es, die Anforderungen an die Transaktionsverarbeitung von anderen Aufgaben des
Cache-Frameworks zu trennen. Dies geschieht über die Einführung einer TransaktionsKlasse (de.tum.in.dcache.Transaction ), die von Clients genutzt werden kann, um
Transaktionen auszuführen.
Isolation
Es wird der Datenbankisolationslevel READ COMMITTED unterstützt. Hierzu muss sich
das Cache-Framework merken, welche Cacheeinträge innerhalb einer Transaktion
invalidiert wurden (siehe Kapitel 5.4.4). Mit der Annahme, dass innerhalb eines Threads nie
zwei oder mehr Transaktionen parallel ablaufen dürfen, lässt sich diese Aufgabe sehr
9
resourcensparend und performant durch Hilfe einer ThreadLocal -Variablen lösen:
private static final ThreadLocal txCacheKeyList = new ThreadLocal()
{
protected synchronized Object initialValue()
{
return new ArrayList();
}
};
Es werden keine eigenen Objekte für jede begonnene Transaktion instantiiert, sondern in
dieser statischen Liste für jeden Thread (was genau maximal einer laufenden Transaktion
entspricht) die Schlüssel der CacheUnits gespeichert, die invalidiert wurden.
Bei get()-Anfragen wird die Liste des aktuellen Threads nach dem entsprechenden Key
durchsucht, falls dieser gefunden wird, wird kein gecachter Wert zurückgeliefert. Mit
Abschluss einer Transaktion wird die Liste geleert, um zu erlauben, dass ab diesem
Zeitpunkt die Einträge wieder gecacht zurückgeliefert werden dürfen.
JDBCWrapper
Um den Lightweight-Distributed-Cache auch in Enterprise JavaBeans-Systemen einsetzen
zu können, ist es nötig, das Framework über die Zeitpunkte der folgenden Aktionen zu
informieren:
1. Erfolgreiches Abschliessen einer Transaktion (JDBC-Kommando: COMMIT)
2. Zurückrollen einer Transaktion (JDBC-Kommando: ROLLBACK)
9
Eine ThreadLocal-Variable ist eine Variable, die pro Thread einen eigenen Wert speichert. Es kann somit
also nur aus dem gleichen Thread auf einen gesetzten Wert zugegriffen werden.
84
An diesen definierten Punkten müssen vorbereitende oder abschließende Aktionen vom
Cache-Framework ausgeführt werden, wie das Versenden von Invalidierungsrequests oder
der Aufbau eines lokalen Caches für die Dauer der Transaktion.
Da diese Kommandos entweder direkt vom Container (im Falle von Container Managed
Transactions) oder über Methoden, die der Container bereit stellt, wie z.B.
UserTransaction.commit() (im Falle von Bean Managed Transactions) ausgeführt werden,
ist es für EJB-Container-Hersteller ohne Probleme möglich, an diesen zentralen
Einsprungpunkten das Cache-Framework zu informieren.
Falls diese Möglichkeit nicht in Frage kommt, da ein bestehender EJB-Container benutzt
werden muss und dieser nicht verändert werden kann oder darf, bietet sich eine weitere
Möglichkeit, welche auch die im DCache-Framework verwendete ist:
Der Container verwendet laut Spezifikation die Schnittstelle JDBC zur Kommunikation
mit der darunterliegenden Datenhaltungsschicht. Dies geschieht über einen JDBC-Treiber,
der die Befehle entgegennimmt und in die datenbankspezifischen Kommandos umwandelt.
Da dieser JDBC-Treiber beliebig austauschbar ist, bietet sich die Möglichkeit einen
sogenannten ‚Wrapper-Treiber’ zu schreiben, der alle Kommandos an den echten Treiber
durchleitet und an speziellen Punkten (z.B. beim COMMIT-Kommando) die nötigen
zusätzlichen Aktionen ausführt. (Zur Implementierung eines JDBC-Treibers siehe auch
[Vog97])
Abbildung 20 zeigt den Architekturaufbau des JDBC-Wrappers, der für das DCacheFramework verwendet wird.
Der ConnectionWrapper informiert bei Aufrufen von commit() und rollback() über
die NotificationUtils-Klasse die einzelnen ConnectionListener. Das JDBCWrapperPackage ist mittels Listener-Konzept vom Rest des DCache-Frameworks entkoppelt.
Dieses enthält eine Implementierung der ConnectionListener-Schnittstelle, den
CacheTXListener, welcher dafür zuständig ist, das Transaction-Objekt des CacheFrameworks zu informieren.
class CacheTXListener implements ConnectionListener
{
public void notifyCommit()
{
Transaction.commit();
}
}
public void notifyRollback()
{
Transaction.rollback();
}
85
Connection
ConnectionWrapper
#driver:Driver
#stmtcache:Map
#realConnection:Connection
metaData:DatabaseMetaData
autoCommit:boolean
catalog:String
transactionIsolation:int
typeMap:Map
warnings:SQLWarning
closed:boolean
readOnly:boolean
holdability:int
java.sql.Driver
Driver
-URLPREFIX:String
-driver:java.sql.Driver
+connect:Connection
+acceptsURL:boolean
+getPropertyInfo:DriverPropertyInfo[]
+jdbcCompliant:boolean
-getOriginalURL:String
majorVersion:int
minorVersion:int
<<informs about TX>>
Statement
StatementWrapper
#realStatement:Statement
#connectionParent:ConnectionWrapper
connection:Connection
fetchDirection:int
fetchSize:int
maxFieldSize:int
maxRows:int
moreResults:boolean
queryTimeout:int
resultSet:ResultSet
resultSetConcurrency:int
resultSetType:int
updateCount:int
warnings:SQLWarning
cursorName:String
escapeProcessing:boolean
resultSetHoldability:int
generatedKeys:ResultSet
NotificationUtils
<<notifies>>
interface
ConnectionListener
-connectionListeners:List
+addConnectionListener:void
+fireNotifyCommit:void
+fireNotifyRollback:void
0..*
PreparedStatement
PreparedStatementWrapper
-realPreparedStatement:PreparedStatement
-sql:String
+notifyCommit:void
+notifyRollback:void
CacheTXListener
+notifyCommit:void
+notifyRollback:void
SQL:String
metaData:ResultSetMetaData
parameterMetaData:java.sql.ParameterMetaData
Abbildung 20: Architekturübersicht des JDBCWrappers
6.1.5 Erweiterungs- und Verbesserungsmöglichkeiten
Ein Invalidator pro CacheUnit. Die jetzige Version erlaubt die Verwendung und
Konfiguration eines Invalidators pro Cache-Instanz, dies bedeutet, alle Cacheeinträge
werden mit dem gleichen Protokoll invalidiert. Ein Herunterziehen dieser
Konfigurationsmöglichkeit auf die Ebene eines einzelen Cacheeintrages würde es erlauben,
ein Cache-Framework zu schaffen, in dem sowohl UDP Multicast-Invalidierungen für
transaktional nicht schützenswerte als auch synchrone TCP-Invalidierungen für in dieser
Hinsicht sehr schützenswerte Einträge eingesetzt werden können.
86
Die entsprechende Implementierung ist trivial, es ist beispielsweise möglich, durch eine
Methode getInvalidator() in der Klasse AbstractCacheUnit den systemweiten Invalidator
für diese spezielle CacheUnit zu überschreiben.
Speicher- und Performanceoptimierungen. Es sind zahlreiche Optimierungen möglich.
Hierzu zählen unter anderem:
·
Verwendung der FastHashMap [Apa03b], welche speziell für den Einsatz in MultiThread-Umgebungen optimiert ist.
·
Auslagern der Versendung der Invalidierungen in einen eigenen Thread
(Verwenden einer Queue zum Abarbeiten der Invalidierungen).
·
Vermeidung von unnötigen Objektinstantiierungen, welche durch erhöhten
Speicherverbrauch den Java Garbage-Collector zusätzlich belasten und dadurch ein
Performanceproblem darstellen können.
Automatisches Auffinden von Abhängigkeiten. Diese in Kapitel 5.4.5 vorgestellte
Strategie kann durch die Architektur des DCache-Frameworks mit verhältnismäßig
geringem Aufwand implementiert werden. Jede CacheUnit kann eine Liste von abhängigen
CacheUnits speichern, die automatisch bei get()-Anfragen aktualisiert werden. Bei
gewünschten Invalidierungen werden zusätzlich alle CacheUnits dieser Liste invalidiert.
Konfigurierbarkeit des Isolationslevels. Zur Zeit unterstützt der Cache bei Verwendung
von Transaktionen den Read-Committed-Isolationslevel. Durch die Anwendung des
Strategy-Patterns [GHJ+95] könnte man dies konfigurierbar halten und eigene
IsolationStrategy-Klassen für eine Read-Uncommitted-, RepeatableRead- oder SerializableIsolation schaffen.
6.2 CMP2BMP
6.2.1 Intention
In Kapitel 5.3.4 wurde das CRUD-Pattern vorgestellt. Bei der Analyse des EntityBeanInterfaces stellt man fest, dass sich jede der CRUD-Methoden einer entsprechenden BeanMethode zuordnen lässt (siehe Tabelle 6). Es lässt sich erkennen, dass die EJB-PersistenzSchnittstelle eine typische CRUD-Schnittstelle ist und die Implementierung eines EntityBeans eine einfache Anwendung des CRUD-Patterns darstellt.
Da der Prototyp eine mögliche Implementierung einer Persistenzschicht für CMP EntityBeans demonstrieren will, liegt die Idee nahe, ein Framework zu schaffen, mit dem für
beliebige Entity-Beans automatisiert diese Methoden implementiert werden. Genau dies ist
allerdings Aufgabe eines EJB-Containers.
87
EJB
CRUD-Pattern
ejbCreate() / ejbPostCreate()
Create
ejbFindXY()
Read
ejbStore()
Update
ejbRemove()
Delete
Tabelle 6: Gegenüberstellung EJB und CRUD-Pattern
Die Implementierung eines kompletten eigenen EJB-Containers wurde aus zwei Gründen
für diesen Prototyps nicht als sinnvoll erachtet. Zum einen ist die Schnittstellenspezifikation zwischen Applikationsserver und EJB-Container noch zu vage und unklar,
und zum anderen besteht nicht die Möglichkeit, sich auf die Entwicklung der
Persistenzschnittstellen zu beschränken, sondern wäre gezwungen, auch andere
Schnittstellen und Dienste wie Security Management, Transaktionshandling oder
Objectpooling zu behandeln und zumindest so weit zu implementieren, dass beispielhaft
damit gearbeitet werden kann. Dies würde den Rahmen dieser Diplomarbeit deutlich
sprengen.
Auch die Integration einer Persistenzschicht in bestehende Open-Source Applikationsserver wie beispielsweise JBoss wurde aufgrund fehlender klarer Schnittstellen und
schlechter Modularisierung verworfen.
Die präferierte Lösung ist in Abbildung 21 dargestellt. Statt der Implementierung eines
eigenen Containers (Weg B) ist es möglich, aus bestehenden CMP Beans BMP Beans zu
generieren, in denen die obengenannten Methoden überschrieben werden und somit für
eine eigene Persistenzhaltung gesorgt wird (Weg A). Die generierten BMP-Beans können
nun in einen bestehenden EJB-Container deployed werden, welcher alle nötigen Dienste
wie Sicherheit und Resourcenverwaltung mit Ausnahme des Persistenzmanagements
übernimmt.
Weg A, Schritt 1:
Implementation der Persistenzschicht
durch einen CMP2BMP-Konverter
BMP
Weg A, Schritt 2:
Nutzung eines
existierenden
EJB-Containers
Runtime
classes
CMP
Weg B:
Implementation eines
eigenen EJB-Containers
inkl. Persistenzschicht
Abbildung 21: Zwei Wege zur Implementierung einer eigenen EJB-Persistenzschicht
88
6.2.2 Reflective CRUD oder Wrapper-Klassen
Es stellt sich die Frage, wie ein Konverter von CMP nach BMP aussehen und wie die
Implementierung der CRUD-Methoden automatisiert werden kann.
[PPR02] stellt mit dem Reflective CRUD Pattern ein System vor, welches zur Laufzeit über
Java Reflection die benötigten Meta-Daten aus den Klassenattributen erhält, um die
nötigen SQL-Statements für eine Persistenzhaltung des Objektes zu erzeugen und
abzusetzen.
Dieses System mag für einfache Java Objekte eingesetzt werden können. Um eine
komplette Wandlung von CMP- zu BMPBeans durchzuführen, ist die Auslesung über
Reflection zur Laufzeit allerdings nicht ausreichend, da nicht alle Informationen über die
Persistenz aus der Klassen-Metainformation gewonnen werden kann.
Der Deployment-Deskriptor für Entity-Beans (siehe Kapitel 4.3.1) enthält beispielsweise
Informationen über das Primary-Key-Feld oder Definitionen der Finder-Methoden, die
nicht zur Laufzeit ermittelt werden können.
Es ist nötig, vor dem Deployment-Vorgang alle benötigten Informationen zusammenzutragen und derart abzulegen (beispielsweise in generierten Hilfsklassen), dass während
der Laufzeit darauf zugegriffen werden kann. Durchaus möglich ist auch die komplette
einmalige Generierung des kompletten Persistenzmanagementcodes für eine Entity-Bean
inklusive der nötigen SQL-Statements. Dies würde die Verwendung von Reflection zur
Laufzeit unnötig machen, was einen Performancevorteil darstellen kann.
CCMP verwendet größtenteils statisch generierten Code (sogenannte Wrapper-Klassen).
Nur an wenigen ausgewählten Stellen wird zur Laufzeit Reflection eingesetzt.
6.2.3 Einbettung des verteilten Caches
Ein weiteres Ziel des Prototyps ist es, Entity-Beans und Finderergebnisse
zwischenzuspeichern und in einem verteilten System korrekt zu invalidieren. Es stellt sich
hier wie auch bei der Generierung der Persistenzschicht die Frage, wie man in eine EJBUmgebung eingreifen kann, um die nötige Funktionalität möglichst einfach zu integrieren.
In Abbildung 22 sind zwei der möglichen Einsprungpunkte aufgezeigt.
·
Über das JNDI InitialContext-Objekt geschieht in einer EJB-Umgebung
jegliches Lookup von EJB-Objekten. Es ist nun möglich, Proxy-Objekte
zurückzuliefern, welche für ein Caching von Methoden-Ergebnissen sorgen [PJ03].
·
Jeglicher Datenbankzugriff geschieht über den JDBC-Treiber. Auch hier kann über
die Entwicklung eines JDBC-Proxies oder Wrappers ein Einsprungpunkt gefunden
werden. [Iso03].
89
Applikation
InitialContext
JNDI
EJB-Code
Driver
JDBC
DB
Abbildung 22: Mögliche Einsprungpunkte für Cachesysteme
Ein großer Nachteil dieser beiden Methoden ist die weite Entfernung vom eigentlichen
Geschäftslogikcode.
Eine ‚Blackbox’, welche ausschliesslich die abgesetzten SQL-Anfragen sieht, kennt nicht
die Hintergründe dieser Abfragen und kann nicht auf die Intention des Entwicklers
schliessen, um darauf entsprechend zu reagieren. Es ist äußerst schwer, korrekte
allgemeingültige Invalidierungen vorzunehmen, da die Abhängigkeiten von einzelnen
Aufrufen nicht einfach ermittelt werden können. Bei komplexeren SQL-Aufrufen bedarf es
einer sehr guten Analyse des SQL-Codes, um feststellen zu können, welche Tabellen und
Spalten betroffen sind und welche Cacheeinträge Daten aus diesen Bereichen entnommen
haben und invalidiert werden müssen.
Eine bessere Lösung ist es, das Cache-Framework näher an die Geschäftslogik einer EJBApplikation zu ziehen. Es bietet sich an, die nötige Funktionalität in die generierten
Wrapperklassen zu ziehen. Diese Klassen haben direkten Zugriff auf die
Businessmethoden des Entity-Beans (beispielsweise die abstrakten get/set-Methoden
zum Lesen und Setzen von Attributen). Diese können überschrieben werden und die
Cachefunktionalität enthalten.
90
6.2.4 Architektur
CMP2BMP erzeugt aus einer kompilierten CMP-Klasse und weiteren benötigten MetaInformationen aus dem Deployment-Deskriptor eine neue Bean-Klasse nach BMPSchnittstelle, welche von der ursprünglichen CMP-Klasse erbt und alle nötigen
Persistenzmethoden implementiert. Diese Aufgabe hätte bei reiner Verwendung von CMP
der EJB-Container übernommen.
Abbildung 23 verdeutlicht den genauen Ablauf der Generierung.
1
CMP Jar
CMP Descriptor
2
makebmp-descriptor.xsl
CMP Class files
4
3
makedeployment.xsl
CCMP Generator
3
4
DeploymentInfo
.java
4
DeploymentInfo
.class
3
3
Generated
BMP Java files
4
2
javac
javac
5
4
BMP Jar
inherits
BMP Descriptor
CMP Class files
generated
BMP Class files
Abbildung 23: Ablauf der CMP zu BMP Konvertierung
1. Ein Archiv (JAR) mit CMP-EJBs und dazugehörigem Deployment-Deskriptor wird
in ein temporäres Verzeichnis entpackt.
2. Der CMP-Deployment-Descriptor wird durch ein XSL-Script in einen BMPDeployment-Descriptor gewandelt. Die Transformation beschränkt sich auf wenige
Änderungen bei allen enthaltenen CMP Entity-Beans:
a. Ändern des persistence-type Attributes von Container auf Bean.
Dadurch wird dem Applikationsserver mitgeteilt, dass ein BMP-Bean
deployed werden soll.
91
b. Entfernen der cmp-field, query und primkey-field Attribute. Diese
müssen für BMP-Beans nicht angegeben werden.
c. Ändern des ejb-class Attributes. Dieses Attribut gibt die
implementierende Javaklasse für das Enterprise Bean an. Diese wird auf
einen neuen Namen gesetzt (OldName_CCMPWrapper ). Die entsprechende
Klasse wird in nachfolgenen Schritten erzeugt.
3. Aus dem Deployment-Descriptor wird mittels XSLT-Skript der Sourcecode der
Klasse DeploymentInfo erzeugt. Diese Klasse wird anschliessend kompiliert und
für den weiteren Prozess verwendet. Sinn dieser Wandlung ist es, dass aus JavaSourcecode sehr einfach und performant auf Informationen aus dem DeploymentDeskriptor zugegriffen werden kann.
Beispiel:
Deployment-Descriptor:
...
<ejb-class> de.tum.in.j2eedemo.samples.Product </ejb-class>
...
DeploymentInfo.java:
public static String getEJBClass()
{
return "de.tum.in.j2eedemo.samples.Product";
}
4. Mittels eines Javaprogrammes (CCMPGenerator) wird zu jedem CMP Entity-Bean
eine entsprechende Wrapperklasse in Form von Javasource erzeugt. Die neu
generierte Klasse erbt von der abstrakten CMP-EJB Klasse und implementiert
folgende Funktionalität:
a. alle abstrakten get/set-Methoden
b. die Methoden zum Anlegen, Laden, Speichern und Löschen von EntityBeans (ejbCreate()/ejbLoad()/ejbStore()/ejbRemove())
c. alle Findermethoden, die im Falle einer Nutzung von reinem CMP im
Deployment-Deskriptor mittels EJB QL definiert werden.
Damit wird die komplette Schnittstelle zur Persistenzschicht abgebildet.
5. Die bisherigen CMP-Klassen, die neu erzeugten Wrapperklassen und der neue
Deployment-Deskriptor werden zu einem neuen Archiv zusammengefügt. Dies
enthält die fertigen BMP-Beans. Es bietet die exakt gleiche Remoteschnittstelle wie
die CMP-Beans und kann im Applikationserver deployed werden.
92
Das erzeugte Archiv enthält Entity-Beans nach dem DualPersistentEntityBeanPattern
[Mar02], da sie durch eine einfache Änderung des Deployment-Deskriptors als CMP- wie
auch als BMP-Beans einzusetzen sind.
BMP2CMP - BMP to CMP Converter
Serializable
interface
EntityDeployment
+getAttribute:Attribute
+getFinderMethod:FinderMethod
de.tum.in.ccmp.gen.CCMPEntityBean
de.tum.in.ccmp.gen.CCMPFindMethod
-FIX_VARIABLES:List
-FIX_METHODS:List
-datasource:String
-nonPKAttributes:List
-allAttributes:List
-OBJECT_WRAPPERS:Map
-theContainingBean:CCMPEntityBean
-deploymentMethod:EntityDeployment.FinderMethod
-homeMethod:Method
+CCMPEntityBean
+writeToFile:void
+Attribute
+FinderMethod
EJBName:String
EJBClass:Class
remoteClass:Class
homeClass:Class
databaseTableName:String
attributes:Collection
primaryKeyAttribute:Attribute
finderMethods:Collection
cacheAllFinders:boolean
CCMPCreateMethod
0..*
+CCMPFindMethod
-getParameterType:Class
-getParameterName:String
-shouldBeCached:boolean
-buildSQL:String
+writeToFileContent:void
entityDeployment:EntityDeployment
cacheAllFinders:boolean
fullyQualifiedClassName:String
simpleClassName:String
packageName:String
createMethods:List
fileName:String
datasourceString:String
fileContent:List
<<use>>
containingBean:CCMPEntityBean
returnType:Class
EJBFindName:String
finderClassName:String
homeFindName:String
parameterCount:int
parameterDeclaration:String
parametersForCall:String
wrappedParametersForCall:String
exceptionDeclaration:String
returningCollection:boolean
<<use>>
0..*
de.tum.in.ccmp.gen.CCMPGenerator
de.tum.in.ccmp.gen.JavaFile
-deploymentinfoClass:Class
-targetDirectory:File
-datasource:String
-INDENT_STEP:String
indent:String
-INITIAL_INDENT:String
+main:void
+CCMPGenerator
+generate:void
#generate:void
-getBeanIDs:Set
-getEntityDeployment:EntityDeployment
+JavaFile
+add:void
+addAll:void
+startBlock:void
+endBlock:void
+assertBlocksClosed:void
lines:List
Utility-Class:
CCMPUtils
-CCMPUtils
+tryToClose:void
+tryToClose:void
+tryToClose:void
+tryToClose:void
Exceptions:
EJBException
CCMPException
FinderException
CCMPFinderException
RuntimeException
de.tum.in.ccmp.gen.CCMPGeneratorException
-theMessage:String
-rootCause:Exception
rootCause:Exception
+CCMPException
+CCMPException
+CCMPException
+printStackTrace:void
+printStackTrace:void
+printStackTrace:void
+CCMPFinderException
+CCMPFinderException
+printStackTrace:void
+printStackTrace:void
+CCMPGeneratorException
+CCMPGeneratorException
+printStackTrace:void
+printStackTrace:void
causedByException:Exception
causedByException:Exception
message:String
Abbildung 24: Architekturübersicht BMP2CMP-Framework
93
6.2.5 Erweiterungen und Verbesserungsmöglichkeiten
·
CMR (Container Managed Relations) werden nicht unterstützt. CMR bilden
eine Umsetzung des Smart Pointer Patterns [Mey96] in die EJB-Architektur, es ist
eine einfach Navigation zwischen durch Aggregationen verknüpften Entity-Beans
möglich. Die lästige explizite Modellierung durch das Ablegen von PrimaryKeys in
Attributen entfällt. Eine entsprechende vollständige Einbettung in CMP2BMP wäre
aufwendig und würde den Rahmen dieses Prototyps sprengen.
·
Local Interfaces werden nicht unterstützt. Durch die Verwendung von Local
Interfaces in Entity-Beans lässt sich der Overhead einer Nutzung des RMI(-IIOP)Prototolls verhindern. Dies führt zu Performancevorteilen, erlaubt aber keinen
entfernten Zugriff auf das Bean. Da sich keine Änderungen in der
Persistenzhaltungsschicht ergeben, wäre eine Unterstützung durch das CMP2BMPFramework nicht schwierig.
·
Zusammengesetzte PrimaryKeys. Zur Zeit existiert keine Unterstützung für
zusammengesetzte Primary Keys. Es kommen nur einfache Klassen des java.langPackage wie java.lang.String oder java.lang.Integer in Frage.
·
Native Typen als Parameter. In der jetzigen Version können keine Entity-Beans
benutzt werden, die in Finder-Methoden native Typen (int, short, float) oder
Arrays aus diesen Typen (int[], short[], float[]) als Parameter besitzen. Der
Konvertierungsvorgang bricht mit einer Fehlermeldung ab. Auf eine entsprechende
Erweiterung wurde bewusst verzichtet, da sie die Lesbarkeit des Sourcecodes
beeinträchtigen würde.
·
EJB QL Parser. Zur Zeit existiert nur ein sehr rudimentärer EJB QL Parser.
(CCMPFindMethod.buildSQL() ). Es bietet sich an, den EJB QL Parser von Sun
Microsystems zu verwenden, welcher mit dem Sourcecode des J2EE mitgeliefert
wird (com.sun.ejb.sqlgen.SQLGenerator ) [Sun01a].
6.3 J2EEDemo
Die J2EE-Beispielapplikation ist ein Entwicklungsframework mit Beispielsourcecode. Es
erhebt nicht den Anspruch, alle Bereiche von J2EE zu demonstrieren, vielmehr ist es
abgestimmt auf genau das, was mit diesem Prototyp gezeigt werden soll. Sie enthält
1. eine EJB-Applikation mit
o einem Stateless Sessionbean ( Access )
o zwei CMP Entity-Beans ( Product und Category )
94
2. eine Webapplikation mit Beispiel-JSP-Seiten, die den Zugriff auf EJBs
demonstrieren und
3. Apache Ant Buildfiles, die zeigen, wie man EJB-jar’s, WARs (Web Archives) und
EARs (Enterprise Archive) bauen kann und die Applikation in Applikationsservern deployed.
Desweiteren wurde der CMP2BMP-Konverter so integriert, dass es möglich ist, die
EJB’s als reine CMP-Beans oder als CCMP-generierte BMP-Beans zu nutzen.
Abbildung 25 zeigt die Klassenstruktur der J2EEDemo-Applikation.
j2eedemo - Enterprise Java Beans Demonstration Application
SessionBean
AccessEJB
de.tum.in.j2eedemo.AccessHome
EntityBean
ProductEJB
de.tum.in.j2eedemo.ProductHome
java.lang.String
EntityBean
CategoryEJB
de.tum.in.j2eedemo.CategoryHome
java.lang.String
-sessionCtx:SessionContext
+ejbActivate:void
+ejbPassivate:void
+ejbRemove:void
+setSessionContext:void
+unsetSessionContext:void
-entityCtx:EntityContext
code:String
name:String
price:Double
categoryPK:String
<<access>>
-entityCtx:EntityContext
pk:String
name:String
+ejbCreate:void
+ejbPostCreate:void
<<access>>
+createProduct:ProductRemote
+createCategory:CategoryRemote
+getProducts:List
+getProductByCode:ProductRemote
+getAllCategories:Collection
+testMethod:void
+ejbRemove:void
+setEntityContext:void
+unsetEntityContext:void
+ejbStore:void
+ejbLoad:void
+ejbActivate:void
+ejbPassivate:void
+setEntityContext:void
+unsetEntityContext:void
+ejbStore:void
+ejbLoad:void
+ejbActivate:void
+ejbPassivate:void
+ejbRemove:void
0..*
+ejbCreate:String
+ejbPostCreate:void
+ejbCreate:String
+ejbPostCreate:void
+findByPrimaryKey:CategoryRemote
+findAll:Collection
+findByName:Collection
+getPk:String
+setPk:void
+getName:String
+setName:void
+findByPrimaryKey:ProductRemote
+findAll:Collection
+findByCategoryPK:Collection
+findByName:Collection
+getCode:String
+setCode:void
+getName:String
+setName:void
+getPrice:Double
+setPrice:void
+getCategoryPK:String
+setCategoryPK:void
+setCategory:void
Abbildung 25: Architekturübersicht J2EEDemo
95
6.4 Performance
Motivation
Durch Lasttestmessungen soll gezeigt werden, wie durch den Einsatz von CCMP die
Performance einer EJB-Applikation gesteigert werden kann. Durch die erhaltenen Werte
wird demonstriert, welche Schwächen aktuelle Applikationsserver bei der Umsetzung der
Entity-Bean-Persistenzschicht teilwesie aufweisen.
Testumgebung
Als Server diente ein Pentium 4 mit 1.8 GHz und 1 GB Hauptspeicher mit dem
Betriebssystem Microsoft Windows 2000 Server.
Als Datenbank wurde Oracle 9i Release 2 Enterprise (9.0.2) verwendet. Die Installation
wurde mit den Standardeinstellungen vorgenommen.
Für alle Applikationsserver wurde Java 2 Standard Edition in der Version 1.4.1_02-b06
verwendet.
Der Client, welcher die Lasttests über das lokale 100MBit-Netz ausgeführt hat, war ein Intel
Mobil Pentium 3 mit 750 MHz und 256 MB Hauptspeicher, als Betriebssystem kam
Microsoft Windows XP Professional zum Einsatz.
Für die Tests wurden folgende Applikationsserver verwendet:
1. Ironflare Orion 2.0.1
2. Oracle 9iAS J2EE 9.0.3.0.0 (OC4J)
3. JBoss 3.2.0 mit Tomcat 4.1.24
Aufgrund von Lizenzbestimmungen war es nicht möglich, die verwendete Version des
OC4J-Servers mit dem Prototyp dieser Arbeit auszuliefern.
Auf den Applikationsservern wurde die im Prototyp enthaltene J2EE-Beispielapplikation
deployed. Als Datenbasis wurde das Beispielsystem, welches über den entsprechenden Link
in der Webapplikation anzulegen ist, verwendet.
96
Messung
Die Messreihen wurden mit dem Microsoft Web Applikation Stress Tool [Mic00] in der
Version 1.1.293.1 durchgefuehrt. Tabelle 7 zeigt das für die Tests verwendete Skript.
Verb
Path
Group
Delay
GET
/j2eedemo/productlist.jsp
default
0
GET
/j2eedemo/productdetail.jsp?code=FIESTA
default
0
GET
/j2eedemo/productdetail.jsp?code=GOLF
default
0
GET
/j2eedemo/productdetail.jsp?code=SL45
default
0
Tabelle 7: Konfiguration des Microsoft Web Application Stress Tool
Die Testlaufzeit (Test Run Time) betrug für jede Testreihe 15 Minuten mit einer
Einwärmphase (Suspend Warmup) von einer Minute. Die verschiedenen gleichzeitigen
Benutzer sind durch die Option Concurrent Connections simuliert worden. Hiermit wurde
die Anzahl der Threads (Stress level) auf 1, 10, 100 bzw. 500 eingestellt. Ein simulierter
Benutzer wartet zwischen einzelnen Seitenaufrufen 0 Millisekunden, setzt also direkt nach
erhaltener Antwort eine neue Anfrage ab.
Das Ergebnis einer Testreihe wurde jeweils drei Mal wiederholt. Es wurden die
Durchschnittswerte aus diesen Wiederholungen, ganzzahlig gerundet, verwendet.
Ergebnis und Bewertung
Abbildung 26 zeigt die während der Tests gewonnenen Ergebnisse. Auf der Y-Achse sind
die erreichten Anfragen pro Sekunde (Page Requests per second, PReq/s) aufgetragen.
Es wird deutlich, dass auf den einzelnen Applikationsservern durch die Verwendung von
CCMP in allen Fällen eine Performancesteigerung im Vergleich zu CMP erreicht werden
kann. Diese fällt teils signifikant aus und erreicht in einigen Fällen den Faktor 10.
Desweiteren ist zu beobachten, dass die Geschwindigkeit bei Erhöhung der Anzahl der
gleichzeitigen Benutzer unter Verwendung von CCMP nicht so drastisch einbricht wie mit
CMP. Dies zeigt die sehr hohe Skalierbarkeit des CCMP-Frameworks.
Der Ironflare Orion Applikationsserver erzielt bei diesem Test mit Abstand die besten
Ergebnisse. Auf Grund des hier vorhandenen sehr hohen Niveaus fällt der durch Einsatz
von CCMP auf Orion erreichte Vorteil deshalb nicht so groß aus wie bei Verwendung des
OC4J oder JBoss Applikationsservers.
97
451
359
409
399
215
223
219
199
CMP
26
26
20
9
41
90
89
75
160
185
200
173
150
270
317
321
PReq/s
CMP
CMP
CCMP
CCMP
Orion
OC4J
Jboss
Orion
OC4J
Jboss
1 user
317
90
26
399
215
185
10 user
321
89
26
451
223
200
100 user
270
75
20
409
219
173
500 user
160
41
9
359
199
150
Abbildung 26: Performancevergleich CMP / CCMP
98
CCMP
7 BEWERTUNG UND AUSBLICK
“Der Markt verhindert keine Fehler, er bestraft sie.” (Fredmund Malik)
Komplexe Client/Server-Architekturen werden heutzutage immer häufiger mit einer
Mehrschichtenarchitektur realisiert. Durch die dadurch entstehende Kapselung können
einzelne Teilbereiche losgelöst voneinander betrachtet und implementiert werden.
Es zeigt sich in der Praxis, dass die Lastverteilung eine der Aufgaben ist, die mit den heute
zur Verfügung stehenden Mitteln und Produkten sehr gut und transparent lösbar ist. Die
technischen Herausforderungen sind hier vergleichsweise gering. Die durch Lastverteilung
erreichbare Skalierbarkeit bietet gute Voraussetzungen, um ausfallsichere Systeme zu
entwerfen.
Auf Datenbankebene sind relationale Systeme (RDMBS) durch ihre Skalierbarkeit und
Robustheit die erste Wahl für den Einsatz in großen Architekturen. Die große KnowhowVerbreitung und die geringen Wartungskosten sparen hohe Investitionen und sorgen für
bestmögliche Datensicherheit. Im Gegensatz zu den objektorientierten Systemen (ODBMS)
bieten sie zwar keine objektorientierte Zugriffsschnittstelle, doch ist durch den sehr weit
verbreiteten Standard SQL und die Javaschnittstelle JDBC hier ein recht einheitlicher Weg
zu erkennen, auf dem aufbauend Frameworks zum Zugriff aus objektorientierten Systemen
geschaffen werden können. Es zeigt sich durch Produkte wie z.B. Oracle Real Application
Cluster, dass hochskalierbare und ausfallsichere Datenbanksysteme einsetzbar sind.
Durch die J2EE-Architektur und der enthaltenen Komponententechnologie EJB bietet
Sun Microsystems eine Middle-Tier Plattform für das Entwickeln der Geschäftslogik.
Wichtige Aufgaben wie Security- oder Resourcenmanagement werden transparent durch
die Applikationsserver, welche die J2EE-Architektur unterstützen, übernommen.
Nach Einschätzung des Autors wird sich CMP (Container Managed Persistence) als
Programmierschnittstelle in J2EE-basierten Anwendungen zum akzeptierten Standard
entwickeln. Durch die Neuerungen in der EJB Spezifikation Version 2.1 gerade im Bereich
der wichtigen Datenbankabfragesprache EJB QL (EJB Query Language) wird es immer
besser möglich, auch komplexe und große Projekte mittels EJB zu modellieren und
umzusetzen.
BMP wird seine Anwendungsgebiete verteidigen, diese sind allerdings eher in der
Anbindung bestehender relationaler Datenbankstrukturen zu suchen. Beim Design von
neuen Anwendungen oder einer sich bietenden Möglichkeit einer Datenmigration wird
BMP als Programmierschnittstelle durch die schlechte Trennung von Persistenz- und
Geschäftslogikschicht in der Bedeutung weiter sinken.
99
Durch die Nutzung von CMT (Container Managed Transactions) wird der Applikationsentwickler weitgehend von der Transaktionsbehandlung befreit, bei Nutzung von BMT
(Bean Managed Transactions) behält er aber weiterhin die Möglichkeit einer feingranularen
Steuerung von Transaktionen.
Der Zeitaufwand für die Implementierung von Remote-, Home- und Implementationsklasse inklusive zusätzlicher Beschreibung in Standard-Deployment-Deskriptoren bei
Nutzung der EJB-Architektur stellt einen nicht zu unterschätzenden Kostenfaktor dar und
ist für eine Vielzahl von Anwendungen unakzeptabel. Durch die Entwicklung und Nutzung
von Code- und Deploymentgeneratoren [Beu03] [XDo03] lässt sich der
Programmieraufwand für die Erstellung von EJBs verringern. Die Entwicklung dieser
Hilfsprogramme wird mit fortschreitender Verbreitung von EJB immer wichtiger werden.
Eine der größten Herausforderung für die Hersteller von Applikationsservern ist die
performante Implementierung der CMP-Schnittstelle. Hier offenbaren sich bei aktuellen
Produkten teilweise noch erhebliche Mängel. Es wird immer deutlicher, dass es zur
Erreichung der geforderten Geschwindigkeit und Ausfallsicherheit nötig ist, performante
Mechanismen zu entwickeln, die das Verteilen (Skalieren) auf mehrere physikalische Server
erlauben. Dies ist mit geeigneten intelligenten Zwischenspeichern (Caches) möglich. Die im
Rahmen dieser Diplomarbeit vorgestellten Mechanismen zeigen, wie diese Aufgabe nahezu
transparent für den Anwender zu lösen ist.
Um eine bessere Aufgabentrennung zu ermöglichen, wäre es sehr hilfreich, wenn sich eine
einheitliche Schnittstelle für die Benutzung verteilter Java-Caches durchsetzen würde
(beispielsweise [JCa03]). Dies erhöht die Modularisierung und ermöglicht es spezialisierten
Herstellern, ihre Caches in J2EE-Applikationsserver zu integrieren.
Auch sollten klarere Schnittstellen geschaffen werden, um es O/R-Mapping Anbietern zu
ermöglichen, ihre Produkte komfortabel in beliebige EJB-Container zu integrieren. Viele
der im Rahmen dieser Diplomarbeit vorgestellten Muster für die Implementierung einer
O/R-Mapping-Schicht wie beispielsweise konfigurierbare Denormalisierung sind für große
Projekte wichtig, allerdings in heutigen Produkten noch zu selten umgesetzt.
J2EE wird sich durch den Einsatz der weit verbreiteten objektorientieren Programmiersprache Java und einer guten, durchdachten und modernen Spezifikation immer mehr zu
einem Standard für den Aufbau großer verteilter Architekturen entwickeln.
Der auf der vierten Seite dieser Diplomarbeit erwähnte Aphorismus „All efforts to replace N
technologies by 1 new technology usually end up with N plus 1 technologies. ” (OMG
EAI/Workshop 2000) darf aber nicht als Aufforderung geshen werden, die Augen vor
neuen Technologien zu verschliessen. Es muss klar sein, dass auch die J2EE-Plattform kein
Allheil- oder Wundermittel zur Implementierung eines performanten und transparenten
Persistenzmanagments in großen, skalierbaren und ausfallsicheren Systemen darstellt. Für
das Design und die Entwicklung eines Projektes sollte man sich nicht auf den Einsatz einer
bestimmten Techologie versteifen.
100
There's no panacea. Whether its EJB, Servlets, CORBA, JMS, or the OO paradigm--no
solution is a panacea for solving every aspect of a business problem. Study the business
problem, understand the technology, hire an experienced architect, and choose to build
systems based on appropriate technologies. [Mon00]
101
102
ANHANG 1: NUTZUNG DES PROTOTYPS
Allgemein
Die Beschreibungen in diesem Anhang sollen bei der Installation und der Konfiguration
des zu dieser Arbeit geschriebenen Prototyps helfen. Es wird ein schneller Einstieg in die
Bedienung und Benutzung gegeben. Für detaillierte Architekturbeschreibungen und
Hintergründe sei auf Kapitel 6 verwiesen.
Die nachfolgenden Beschreibungen gehen von der Benutyung einer Microsoft WindowsPlattform aus. Für andere Betriebssysteme (z.B. Linux) müssen teilweise kleine
Änderungen vorgenommen werden. Dies betrifft vor allem den Aufruf von Skriptdateien,
die teilweise nur als Windows-Batchfile (*.bat) vorliegen.
Systemvorrausetzungen
Der Prototyp ist komplett in der Programmiersprache Java geschrieben, es gibt deswegen
keine grundsätzliche Einschränkung auf bestimmte Plattformen oder Betriebssysteme. Die
Vorraussetzungen sind:
·
Java 2 Standard Edition 1.4.1 (J2SE 1.4.1) oder höher.
·
Apache Ant Build Tool 1.5.2 oder höher [Apa03a]. Ant in der Version 1.5.3 ist
mitgeliefert.
·
JBoss 3.2.0 mit Tomcat 4.1.24, Ironflare Orion 2.0.1, Oracle 9iAS J2EE 9.0.3.0.0
oder andere EJB2.0 kompatible Applikationserver. JBoss 3.2.0 mit Tomcat 4.1.24
ist mitgeliefert.
·
Eine relationale Datenbank (Oracle 9i, MySQL). Empfohlen werden kann auch
HSQLDB [HSQ03]. Es ist ein relationales Datenbanksystem, geschrieben in
reinem Java und sehr gut geeignet für die Entwicklung von J2EE-Applikationen. Es
ist klein (ca. 200kb), sehr einfach zu konfigurieren und unterstützt einen Auszug
von ANSI-92 SQL und beinhaltet einen JDBC-Treiber. HSQLDB ist mitgeliefert
(Bestandteil von JBoss).
Mit geringen Anpassungskosten sollte der Prototyp auch auf anderen J2EE 1.3
unterstützenden Applikationsservern als den oben erwähnten JBoss und Oracle 9iAS
lauffähig sein. Aus der Neuerungen der EJB 2.0 Spezifikation wird einzig die EJB QL (EJB
Query Language) benutzt. Es wurde auf Features wie Local-Interfaces oder Container
Managed Relations (CMR) verzichtet.
103
Es wird mindestens Ant Version 1.5.2 benötigt, da das <xmlcatalog>-Tag in VorgängerVersionen nicht verfügbar war. Da dieses Tag nur zur Validierung der DeploymentDeskriptoren verwendet wird, sollte mit wenig Aufwand eine Lauffähigkeit auf der Version
1.5(.1) zu erreichen sein. Die zur Zeit aktuelle Version 1.5.3 würde ebenfalls erfolgreich
getestet.
Von den Neuigkeiten des J2SE 1.4.1 wird vor allem das Preferences-Framework
(java.util.prefs) benutzt, und es wird ein neuer Konstruktor in der Klasse
java.lang.RuntimeException verwendet. Auch hier kann mit wenigen Umstellungen
die Lauffähigkeit auf J2SE 1.3.1 erreicht werden. Die Version J2SE 1.4.2 beta wurde
ebenfalls erfolgreich getestet.
Für die Design-Phase wurde die UML Software Together J 6.01 von Borland/TogetherSoft
zum Modellieren der Klassenstrukturen benutzt. Die in der Dokumentation enthaltenen
UML-Diagramme sind mit diesem Softwarepaket erstellt. Die Projektdateien sind
Bestandteil des Prototyps und liegen in den entsprechenden Verzeichnissen. Together J ist
mit einer Universitätslizenz der Technischen Universität München (TUM) registriert.
104
Verzeichnisstruktur
Tabelle 7 beschreibt die Struktur des Hauptverzeichnisses. Es beinhaltet die DrittHersteller-Tools Apache Ant, JUnit und JBoss, die drei Teile des Prototyps DCache,
CMP2BMP und J2EEDemo, die API-Dokumentation und Hilfs-Shell-Skripte.
Verzeichnis
Inhalt
ant/
jboss/
Apache Ant 1.5.3 Distribution ohne
Dokumentation
JBoss 3.2.0 mit Tomcat
(entpackte Standardinstallation)
4.1.24
junit3.8.1/
JUnit-Testframework für automatische
Tests (junit.org)
dcache/
Das Lightweight Distributed Cache Framework
cmp2bmp/
j2eedemo/
Das CMP2BMP - Framework
Die J2EE Demonstrations-Applikation
resources/
Benötigte Dateien für die Generierung
der
Dokumentation
(z.B.
eine
Übersichtsseite im HTML-Format)
doc/
Dokumentation. Die API-Dokumentation wird erst nach erfolgreichem
Build-Vorgang erzeugt
Jboss.bat
Skript zum Starten von Jboss unter
Windows-Plattformen
jdbcclient.bat
Skript zum Starten des HSQLDB
Database-Managers unter WindowsPlattformen
build.xml
Readme.txt
Ant Build Datei
Textdatei mit wichtige Informationen
zum Prototyp
Tabelle 8: Verzeichnisstruktur des Prototyps
105
Schnellstart
Das folgende Vorgehen dient einem schnellen Einstieg. Es wird beschrieben, wie Sie den
Prototyp bauen, JBoss starten, die Beispielapplikation deployen und sich die zugehörige
Web-Applikation ansehen können.
1. Öffnen sie eine Shell (z.B. DOS-Eingabeaufforderung) und wechseln sie in das
prototyp-Verzeichnis.
2. Testen Sie, ob sie die korrekte Java- bzw Ant-Version installiert haben:
C:\da\prototyp> java –version
java version "1.4.1_02"
Java(TM) 2 Environment, Standard Edition (build 1.4.1_02-b06)
Java HotSpot(TM) Client VM (build 1.4.1_02-b06, mixed mode)
C:\da\prototyp> ant –version
Apache Ant version 1.5.3 compiled on April 16 2003
Sie müssen mindestens die in den Systemanforderungen beschriebenen Versionen
konfiguriert haben.
3. Bauen die den kompletten Prototyp durch Eingabe von
C:\da\prototyp> ant
[...]
BUILD SUCCESSFUL
Total time: 25 seconds
Nach erfolgreichem Durchlauf wurde ein J2EE Enterprise Archive (. ear) erzeugt
und im autodeploy-Verzeichnis des JBoss Applicationservers abgelegt.
4. Starten Sie den JBoss Applikationserver durch Eingabe von
C:\da\prototyp> jboss.bat
[...]
19:21:26,120 INFO [Server] JBoss (MX MicroKernel)
[3.2.0 (build: CVSTag=JBoss_3_2_0 date=200304110033)]
Started in 23s:814ms
Nun ist der Applikationserver gestartet und hat die J2EE Beispielapplikation
deployed. JBoss hat nun auch die benötigten Tabellen in der mitgelieferten
HSQLDB-Datenbank angelegt. (Achtung: Dies geschieht nur, falls die CMPApplikation benutzt wird, bei späterer Umstellung auf das CCMP kann die
automatische Generierung nicht erfolgen)
106
5. Öffnen Sie einen Webbrowser und rufen Sie die folgende Seite auf:
http://localhost:8080/j2eedemo/
Füllen sie die Beispieldatenbank (durch Auswählen des entsprechenden Links).
Hierdurch werden eine kleine Anzahl von Produkten und Kategorien angelegt.
6. Nun können sie die Beispielwebapplikation betreten. Dazu folgen Sie dem
entsprechenden Link.
Anwendung
Aus dem Hauptverzeichnis des Prototypens können sie ant-Targets aufrufen, welche sich
mit den folgenden zentralen Aufgaben beschäftigen
·
Bauen des Prototyps und der API-Dokumentation
·
Deployen der J2EE-Applikation in einen Applikationsserver
·
Löschen aller temporären Dateien (Aufräumen der Applikation)
Größtenteils enthält die Build-Datei build.xml keine eigene Logik, sondern deligiert die
Aufrufe an die Teilbereiche dcache, cmp2bmp und J2EEDemo.
1. Bauen der Applikation und Dokumentation
Durch die Eingabe von
c:\diplom\prototyp> ant all
in einer Kommandozeile oder Shell werden alle Teile des Prototyps inklusive der
Java API Dokumentation gebaut. Nach erfolgreichem Durchlauf des BuildProzesses finden Sie unter doc/index.html eine Startseite mit Links zu allen
Teilen der Dokumentation.
2. Deployen der Applikation in einen Applikationsserver
Durch
c:\diplom\prototyp> ant deploy
wird die J2EE-Applikation in den eingestellten Applikationsserver deployed. Dies
geschieht entweder durch Kopieren des Enterprise Archive (Ear) in ein
sogenanntes autodeploy-Verzeichnis (JBoss) oder durch Benutzung von
proprierären Herstellertools (Oracle 9iAS, Orion).
107
3. Löschen aller temporären Dateien
Der Befehl
c:\diplom\prototyp> ant clean
löscht alle nicht benötigten Dateien. Dazu gehören alle Klassen in temporär
angelegten Unterverzeichnissen tmp/ sowie alle Klassendateien und erzeugte
Archive. Diese können aus dem Sourcecode durch erneutes Bauen der Applikation
wiederhergestellt werden.
Für genauere Informationen über die Bedienung der Teilbereiche wird auf die ausführliche
Dokumentation in den nachfolgenden Unterkapiteln verwiesen.
DCache
Das DCache-Framework enthält eine komplette Implementierung des in dieser Arbeit
vorgestellten Lightweight Distributed Cache Patterns. Eine detaillierte Beschreibung dieses
Patterns und genauere Informationen über die Funktionsweise finden sie in Kapitel 6.
Die gezeigte Nutzung des DCache-Frameworks für das Cachen von Entity-Beans und
ihren Findermethoden ist nur eines der vielen möglichen Anwendungsgebiete. Es ist auch
für andere Aufgabengebiete und Implementierungen einsetzbar.
Verzeichnisstruktur
Tabelle 9 zeigt den Verzeichnisaufbau des DCache-Unterverzeichnisses. Hier befindet sich
vor allem der Sourcecode, Klassendiagramme, Together J-Projektdateien und für das
DCache-Framework benötigte Hilfsbibliotheken.
Konfiguration
Alle Konfigurationseinstellungen zum DCache-Framework werden in der zentralen
Konfigurationsdatei cache-config.xml getätigt. Diese Datei kann nicht zur Laufzeit
geändert werden. Damit getätigte Änderungen übernommen werden, muss die Java-Virtual
-Machine, in der das DCache-Framework läuft, neu gestartet werden.
Die Datei liegt im XML-Format vor und wird über das J2SE Preferences-Framework
eingelesen. Eine Übersicht aller Einstellungsmöglichkeiten finden Sie in Tabelle 10.
108
Verzeichnis
Inhalt
src/
Der Quellcode des DCache-Framework
resources/
Benötigte Dateien zum Bauen der API
Dokumentation, zusätzlich Diagramme
10
im GIF- und WMF -Format
lib/
Benötigte Hilfsbibliotheken.
build.xml
Ant Build File
cacheconfig.xml
Zentrale Konfigurationsdatei des CacheFrameworks
dcache.tpr|.tws
Together J Projektdateien.
Tabelle 9: Verzeichnisstruktur DCache
Konfigurationsknoten
Erklärung
de.tum.in.dcache.invalidator
Konfiguriert die Klasse des zu
benutzenden Invalidators.
de.tum.in.dcache.maxentries
de.tum.in.dcache.udp.
UDPInvalidator.udpport
de.tum.in.dcache.udp.
UDPInvalidator.udpmulticastaddress
Definiert die maximal Anzahl von
Cacheeinträgen. Falls diese Anzahl
erreicht ist, greift die Verdrängungsstrategie und entfernt nach dem LRUAlgorithmus lang nicht zugegriffene
Cacheeinträge.
Definiert den Port, auf dem der
UDPInvalidator auf Pakate hört.
Definiert die Adresse, auf dem der
UDPInvalidator auf Multicast-Pakete
hört.
Tabelle 10: Konfigurationsparameter des DCache-Frameworks
10
WMF (Windows Meta File) ist ein Vektor-Grafikformat der Microsoft Corporation
109
Dies ist ein Auszug aus der cacheconfig.xml Konfigurationsdatei:
<node name="dcache">
<map>
<entry key="invalidator"
value="de.tum.in.dcache.udp.UDPInvalidator" />
<entry key="maxentries" value="10000" />
</map>
<node name="udp">
<map />
<node name="UDPInvalidator">
<map>
<entry key="udpmulticastaddress" value="224.0.0.1" />
<entry key="udpport" value="7001" />
</map>
</node>
</node>
</node>
Anwendung
Verteilung des Caches
Das DCache-Framework ist keine eigenständige Applikation, sondern wird in solche
integriert. Zur Demonstration der Funktionfähigkeit an einem einfachen Beispiel dient die
de.tum.in.dcache.samples.CacheTest Klasse. Durch sie wird gezeigt, wie die
Verwendung für das Cachen von Produkten in einem typischen E-Commerce-System
aussehen könnte. Als Beispiel wird hier der Name von eines Produktes
zwischengespeichert. Um die Klasse übersichtlich zu halten, werden die Daten nicht in
einer Datenbank, sondern mittels der Config.put()-Methode in der Konfigurationsdatei
cache-config.xml persistent gemacht.
Starten sie im DCache-Vezeichnis mittels
c:\j2eeproto\dcache> ant goserver
den Beispiel-Server. Dieser gibt einen Produktnamen aus, der einmalig aus der
Konfigurationsdatei mittels Config.get() gelesen wurde und danach immer aus dem
Zwischenspeicher geladen wird.
Nach erfolgreichem Start erhalten sie folgende Ausgaben:
[java]
[java]
[java]
[java]
[java]
[java]
[java]
[java]
[java]
[java]
110
CacheTest Server is running. Printing productnames:
init Invalidator de.tum.in.dcache.udp.UDPInvalidator...
port = 7001
multicastaddress = 224.0.0.1
cache configuration: maxentries = 10000
reading new...
null
null
null
...
Dies bedeutet, dass der Produktname zur Zeit nicht gesetzt (null) ist. Dieser wird im
Sekundentakt ausgegeben. Das einmalige ‚reading new...’ bedeutet, dass nur einmal auf
die persistenten Daten zugegriffen wurde, um den Wert zu lesen, alle folgenden Anfragen
wurden aus dem Cache beantwortet.
Nun können sie den Demonstrationsclient verwenden, um den Namen zu ändern und über
UDP-Invalidierung den Server aufzufordern, den Cacheeintrag zu invalideren und die
Daten neu vom physikalischen Speicher zu lesen.
Hierzu starten sie, während der Server läuft, in einer zweiten Shell folgenden Befehl:
c:\j2eeproto\dcache> ant -Darg=newname go
Auf dem Server erscheinen folgende Ausgaben:
...
[java]
[java]
[java]
[java]
[java]
[java]
[java]
[java]
...
null
null
null
null
reading new...
name
name
name
Dies zeigt, dass der Request angekommen, der Cacheeintrag invalidiert wurde und die
Daten neu von Festplatte gelesen wurden.
Weitere detaillierte Informationen finden sie in den Sourcecode-Kommentaren der Klasse
de.tum.in.dcache.CacheTest.samples.CacheTest.
Transaktionssicherheit
Die soeben beschriebene Demonstration zeigt die Fähigkeit, einen über mehrere Server
verteiten Cache zu bilden.
Die zweite wichtige Aufgabe des DCache-Frameworks ist die Wahrung von Transaktionssicherheit beim Cachen von Datenbankanfragen, auf die über eine JDBC-Schnittstelle
zugegriffen wurde. Um diese Fähigkeit zu demonstrieren wird das JUnit-Framework
verwendet.
Gehen sie wie folgt vor:
1. Stellen Sie sicher, dass sich das junit.jar im lib-Verzeichnis ihrer benutzten AntDistribution befindet. Sie finden diese Bibliothek im junit3.8.1-Verzeichnis des
Prototyps. Falls sie das mitgelieferte Ant verwenden, ist diese Datei bereits an der
korrekten Stelle.
111
2. Öffnen sie den Sourcecode der Klasse de.tum.in.junit.TransactionTest
und stellen sie an der markierten Stelle die nötigen Verbindungsdaten zum Zugriff
auf ihre gewünschte Datenbank ein
3. Führen die den JUnit-Test durch Eingabe von
c:\j2eeproto\dcache> ant unittest
im dcache-Verzeichnis aus.
Bei erfolgreichem Test sehen sie Ausgaben wie
[junit]
[junit]
[junit]
[junit]
[junit]
[junit]
[junit]
dbreads = 39
Testsuite: de.tum.in.junit.TransactionTest
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 2.183 sec
------------- Standard Output --------------dbreads = 39
------------- ---------------- -------------Testcase: testDBTX took 2.183 sec
BUILD SUCCESSFUL
Zur Provokation eines misslungenen Tests können sie den JDBCWrapper ausschalten, in
dem sie die entsprechend kommentieren Datenbankeinstellungen in der Klasse de.tum.
in.junit.TransactionTest verwenden. Dadurch können die Transaktionskommandos
wie commit() nicht vom DCache-Framework abgefangen werden und die Tests werden
nicht erfolgreich abschließen.
Die entsprechenden Ausgaben deuten auf den Fehler hin:
[junit]
[junit]
[junit]
[junit]
[junit]
Testcase: testDBTX FAILED
it was not consistent
junit.framework.AssertionFailedError: it was not consistent
at [...]
Testcase: testDBTX
BUILD FAILED
Das genaue Vorgehen und die Funktionsweise dieses Testes entnehmen Sie bitte den
Anmerkungen im Sourcecode der Klasse de.tum.in.junit.TransactionTest.
CMP2BMP
Das CMP2BMP-Framework wandelt alle CMP Entity-Beans eines EJB-Moduls in BMPBeans und erzeugt eine eigene Persistenzschicht, die das DCache-Framework nutzt und
damit fuer eine gesteigerte Anwendungsperformance sorgt.
112
Verzeichnisstrukur
Tabelle 11 zeigt die Verzeichnisstruktur des BMP2CMP-Frameworks.
Verzeichnis
lib/
resources/
src/
build.xml
Inhalt
Benötigte Libraries (hier nur das ejb.jar)
verwendete XSL-Skripte und die EJB2.0
DTD zum lokalen Validieren der
Deploymen-Deskriptoren
Der cmp2bmp-Sourcecode
Das Ant buildfile
Tabelle 11: Verzeichnisstruktur des CMP2BMP-Frameworks
Konfiguration
Tabelle 12 beschreibt die Konfigurationseinstellugen des CMP2BMP-Frameworks.
Parametername
source-ejb-module
Beschreibung
Dateiname des EJB-Archives
(Bsp.: c:\ejbtest\bin\myejbs.jar )
dest-ejb-module
Dateiname des zu erzeugenen EJBArchives. Dieser Name darf nicht auf die
gleiche Datei wie source-ejb-module
zeigen.
(Bsp.: c:\ejbtest\bin\newejbs.jar )
ccmp.datasource
JNDI-Name, über den die Datenbank
angesprochen werden soll. Die erzeugten
BMP-Beans machen einen Lookup auf
diesen JNDI-String und verwenden dies
als Datasource. (Bsp: java:DefaultDS)
Tabelle 12: Konfigurationsparameter des CMP2BMP-Frameworks
113
Anwendung
Der Aufruf erfolgt über das Ant-Buildfile. Es gibt drei Startparameter, die beim Aufruf des
Ant-Files angegeben werden müssen. Diese können entweder per Kommandozeile oder
aus einem eigenen Ant-Skript erfolgen.
Falls der Aufruf per Kommandozeile erfolgt müssen die einzelnen Startparameter
angegeben werden:
ant –Dsource-ejb-module=../myejb.jar –Ddest-ejb-module=../newejbs.jar
-Dccmp.datasource=jdbc:/DefaultDS all
Aus einem eigenen Ant Skript erfolgt der Aufruf folgendermassen:
<ant dir="cmp2bmp/">
<property name="source-ejb-module" location="bin/j2eedemo_pre.jar"/>
<property name="dest-ejb-module" location="bin/j2eedemo.jar"/>
<property name="datasource" value="jdbc:/DefaultDS"/>
</ant>
J2EEDemo
Die J2EEDemo-Applikation bietet eine Entwicklungsgrundlage für eigene J2EEAnwendungen. Es wurde eine Umgebung geschaffen, die es sehr einfach macht, EJB- und
Webmodule zu entwicklen und nach J2EE Standard zu einem Enterprise Archive
zusammenzufügen. Zur Demonstration enthält sie zusätzlich
·
Ein Session-Bean (Access)
·
Zwei CMP Entity-Beans (Product und Category)
·
Eine Webapplikation, die auf die EJBs zugreift.
114
Verzeichnisstruktur
Das J2EE-Demonstrationsframework besitzt die in Tabelle 12 dargestellte Verzeichnisstruktur.
Verzeichnis
Inhalt
web/
Die Beispiel Webapplikation
resources/
J2EE-Deskriptoren für das Bauen des
Enterprise Ear Files (EAR)
ejb/
Die Beispiel EJB Applikation
build.xml
Das Ant buildfile
j2eedemo.properties
Konfigurationsdatei
Tabelle 13: Verzeichnisstruktur der J2EE-Demoapplikation
Einrichten der Datenbank
Das J2EE-Framework benötigt eine konfigurierte Datenbank inkl. einer leeren
Tabellenstruktur. Bei Benutzung der JBoss und für Entwicklungszwecke bietet sich an, die
mitgelieferte und vorkonfigurierte HSQLDB-Datenbank zu verwenden.
Die Tabellenstruktur kann entwerder manuell oder automatisch angelegt werden. Sowohl
JBoss als auch der 9iAS bieten die Möglichkeit, beim Deployen von CMP-Beans die
nötigen CREATE TABLE-Statements auf der konfigurierten Datenquelle auszuführen.
Falls das manuelle Anlegen erwünscht ist, hier eine mögliche Vorgehensweise für die
mitgelieferte HSQLDB-Datenbank:
1. Starten die mittels Aufruf der jboss.bat Batch-Datei den JBossApplikationsserver. Nach erfolgreicher Initialisierung erscheint als letze Meldung
auf der Konsole die Meldung:
14:18:39,251 INFO [Server] JBoss (MX MicroKernel) [3.2.0
(CVSTag=JBoss_3_0_6 Date=200301260037)] Started in 0m:8s:234ms
115
2. Starten des Datenbank-Clients mittens der mitgelieferten jdbclient.bat.
Anmerkung: Sie können den HSQLDB-Datenbankclient auch starten, in dem sie
folgenden Link in einem Browserfenster aufrufen:
http://localhost:8080/jmx-console/HtmlAdaptor?action=invokeOp&
name=jboss%3Aservice%3DHypersonic&methodIndex=3
3. Auswahl ‚HSQL Database Engine Server’ im Connection-Fenster und Hinzufügen
des Ports 1476 in der URL-Spalte. (jdbc:hsqldb:hsql://localhost:1476 )
4. Ausführen der folgenden Statements:
CREATE TABLE Product (
code VARCHAR NULL PRIMARY KEY,
name VARCHAR NULL,
price DOUBLE NULL,
categoryPK BIGINT NULL )
CREATE TABLE Category (
pk BIGINT PRIMARY KEY,
name VARCHAR NULL )
Für andere Datenbanken sind eventuell kleine Änderungen nötig. Die entsprechenden
Statements für die Benutzung einer Oracle-Datenbank sehen folgendermassen aus:
CREATE TABLE Product (
code VARCHAR NULL PRIMARY KEY,
name VARCHAR NULL,
price DOUBLE NULL,
categoryPK BIGINT NULL )
CREATE TABLE Category (
pk BIGINT PRIMARY KEY,
name VARCHAR NULL )
Anwendung
Die J2EEDemo-Applikation muss im jeweiligen EJB-Container deployed werden.
Detaillierte Informationen finden Sie in der Dokumentation des verwendeten
Applikationsservers.
Hier folgt die Beschreibung des Build- und Deployvorgangs unter Verwendung des
mitgelieferten JBoss-Applikationsservers:
1. Passen sie die Konfigurationsdatei j2eedemo/j2eedemo.properties an und
aktivieren oder deaktivieren sie je nach Wunsch die die Benutzung des CCMPFrameworks.
116
2. Bauen sie das Enterprise Archive File durch Eingabe folgender Befehle in einer
Shell:
c:\prototyp\j2eedemo> ant clean all
3. Starten die mittels Aufruf der jboss.bat Batch-Datei den JBoss-Applicationserver.
Nach erfolgreicher Initialisierung erscheint als letze Meldung auf der Konsole die
Meldung:
14:18:39,251 INFO [Server] JBoss (MX MicroKernel) [3.0.6
(CVSTag=JBoss_3_0_6 Date=200301260037)] Started in 0m:8s:234ms
4. Deployen sie die Applikation mittels:
c:\prototyp\j2eedemo> ant deploy
Nach kurzer Zeit erscheint im JBoss-Konsolenfenster folgende Ausgaben:
...
14:29:04,918
14:29:04,918
14:29:04,980
...
14:29:05,136
INFO
INFO
INFO
[EjbModule] Deploying Product
[EjbModule] Deploying Category
[EjbModule] Deploying Access
INFO
[MainDeployer] Deployed package:
file:/D:/diplom/prototyp/jboss/server
/default/deploy/j2eedemo.ear
5. Rufen sie mit einem Browser
Ihrer Wahl die Webapplikation unter
auf.
http://localhost:8080/j2eedemo/
117
118
LITERATURVERZEICHNIS
[Apa03a]
The Apache Software Foundation, The Apache Ant Project, URL,
http://ant.apache.org, (11. 4. 2003).
[Apa03b] The Apache Software Foundation, The Jakarta Commons, URL,
http://jakarta.apache.org/commons, (1. 4. 2003).
[Amb00]
Scott W. Ambler, The Design of a Robust Persistence Layer For Relational
Databases, (2000), URL, http://www.ambysoft.com/persistenceLayer.pdf,
(20. 4. 2003).
[Amb03]
Scott W. Ambler, Encapsulating Data Access, (2003), URL,
http://www.agiledata.org/essays/implementationStrategies.html, (18. 4. 2003).
[Beu03]
Cedric Beust, EJBGen, An EJB2.0 code generator, URL,
http://www.beust.com/cedric/ejbgen, (9. 4. 2003).
[Bir95]
Kenneth P. Birman, Building Secure and Reliable Network Applications,
Department of Computer Science, Cornell University, Ithaca, New York,
(1995).
[BMR+96] Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, Michael
Stal, Pattern Oriented Software Architecture – A System of Patterns, Wiley, (1996).
[BP96]
Ganesha Beedubail, Udo Pooch, An Architecture for Object Replication in
Distributed Systems, Technical Report, (1996).
[Bro02]
Kyle Brown, The Propagate Cache Updates Pattern, PLoP 2002 Conference,
(2002).
[CDK01] G. Coulouris, J. Dollimore and T. Kindberg, Distributed Systems – Concepts and
Design, third edition, Addison-Wesley, (2001).
[CK96]
Jens Coldewey, Wolfgang Keller, Objektorientierte Datenintegration – ein
Migrationsweg zur Objekttechnologie, Objektspektrum Magazin Juli/August
1996: S. 20-28, (1996).
[Com93]
D.Comer, Internetworking with TCP/IP, Volume III, Client-Server Programming,
Prentice-Hall, (1993).
[CMS03]
K. Chow, R. Morin, K.Shiv, Enterprise Java Performance: Best Practices, Intel
Technology Journal Volume 7, Issue 1, 2003, (2003).
[Col96]
Jens Coldewey, Decoupling of Object-Oriented Systems – A Collection of patterns,
sd&m GmbH & Co.KG, München, (1996).
119
[DZ83]
J.D. Day, H. Zimmermann, The OSI Reference Model, In: Proceedings of the
IEEE, Band 71, S. 1334-1340, Dezember 1983, (1983).
[FCL97]
M. J. Franklin, M. J. Carey, M. Livny, Transactional client-server cache
consistency: alternatives and performance, (1997).
[Fow02]
Martin Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley,
(2002).
[Fus97]
Mark L. Fussell, Foundations of Object Relational Mapping, (1997).
[FY98]
Brian Foote, Joseph Yoder, MetaData and Active Object-Models, (1998).
[Gei95]
Kurt Geihs, Client/Server-Systeme, Grundlagen und Architekturen, Internat.
Thomson Publishing, (1995).
[GHJ+95] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns,
Elements of Reusable Object-orioented Software, Addison Wesley, (1995).
[GR93]
J. Gray, A. Reuter, Transaction Processing, Morgan Kaufmann, (1993).
[GS91]
Jim Gray, Daniel P. Siewiorek, High Availability Computer Systems, IEEE
Computer, 24(9), S. 39-48, (1991).
[GS96]
Rachid Guerraoui, Andre Schiper, Fault Tolerance by Replication in Distributed
Systems, (1996).
[GT02]
David Gourley, Brian Totty, HTTP: The Definitive Guide, O’Reilly (2002).
[Hew03a] Hewlett Packard, Resource for hp e-commerce traffic director sa8220 , URL,
http://h20000.www2.hp.com/bizsupport/TechSupport/Resource.jsp?locale=
en_US& taskId=115&prodSeriesId=62914&prodTypeId=15351, (14. 4. 2003).
[Hew03b] Hewlett Packard, SDK and RTE 1.4 for hp-ux, URL,
http://www.hp.com/products1/unix/ java/java2/sdkrte14/index.html,
(9. 5. 2003).
[HR01]
Theo Härder, Erhard Rahm, Datenbanksysteme – Konzepte und Techniken der
Implementierung, 2.Auflage, Springer Verlag, (2001).
[HSQ03]
HSQLDB, Hypersonic SQL Database, URL, http://www.hsqldb.org, (3. 3.
2003).
[Int98]
Internet Engineering Task Force, Internet Draft UUIDs and GUIDs. URL,
http://www1.ics.uci.edu/~ejw/authoring/uuid-guid/draft-leach-uuids-guids01.txt, (8. 4. 2003).
120
[Iso03]
Isocra Ltd., Isocra Livestore Transparent Data Cache, URL,
http://www.isocra.com/livestore, (12. 5. 2003).
[Jav03]
Java Skyline, Java Skyline, Magazine for Java Server Developers, URL,
http://www.javaskyline.com, (9. 5. 2003).
[JCa03]
JCache, JCache – Java Temporary Caching API, URL,
http://jcp.org/en/jsr/detail?id=107, (5. 5. 2003).
[JDO03a] Java Data Objects, Java Specification Requests, Java Data Objects (JDO)
Specification, URL, http://jcp.org/en/jsr/detail?id=012, (2. 4. 2003).
[JDO03b] JDOCentral.com, Developers Community For Java Data Objects, URL,
http://www.jdocentral.com, (5. 4. 2003).
[JUG03]
Java Uuid Generator, Java Uuid Generator (JUG) home page, URL,
http://www.doomdark.org/doomdark/proj/jug/index.html, (4. 4. 2003).
[KC97]
Wolfgang Keller, Jens Coldewey, Relational Database Access Layers: A Pattern
Language, Collected Papers from the PLoP’96 and EuroPLoP’96 Conferences,
Technical Report, (1997).
[Kel97]
Wolfgang Keller, Mapping Objects to Tables, A Pattern Language, (1997).
[Kel98]
Andreas Kelz, Relationale Datenbanken, Eine Einführung, URL,
http://v.hdm-stuttgart.de/~riekert/lehre/db-kelz, (1998).
[Kel03]
Wolfgang Keller, Patterns for Object/Relational Database Mapping , URL,
http://www.objectarchitects.de, (9. 5. 2003).
[Mar02]
Floyd Marinescu, EJB Design Patterns, Advanced Patterns, Processes and Idioms,
Wiley, (2002).
[Mey96]
Scott Meyers, More Effective C++, Addison Wesley, (1996).
[Mic00]
Microsoft TechNet, Performance Testing with the Web Application Stress Tool,
URL, http://www.microsoft.com/technet/itsolutions/ecommerce/
maintain/optimize/d5wast_2.asp, (3. 5. 2003).
[Mon00]
Richard Monson-Haefel, Top 8 Architecture Tips for Distributed Computing,
URL, http://java.oreilly.com/news/ejbtips_0500.html, (8. 4. 2003).
[Mon01]
Richard Monson-Haefel, Enterprise JavaBeans, third edition, O’Reilly, (2001).
[NBW+01] Ramesh Nagappan, Cedric Beust, Marc Wilcox et al., Professional Java Server
Programming – J2EE 1.3 Edition, Wrox Press, (2001).
121
[NNW93] Elizabeth J. O’Neil, Patrick E. O’Neil, Gerhard Weikum, The LRU-K Page
Replacement Algorithm For Database Disk Buffering, (1993).
[OAA+00] L. Opyrchal, M. Astley, J. Auerbach, G. Banavar, R. Strom, D. Sturman,
Exploiting IP Multicast in content-based publish-subscribe systems, (2000).
[OMG03] Object Management Group (OMG), Unified Modelling Language – The UML
Resource Page. URL, http://www.omg.org/uml, (2. 4. 2003).
[Ora01]
Oracle Corporation, Oracle Real Application Clusters – Cache Fusion Delivers
Scalability, An Oracle Whitepaper, URL: http://otn.oracle.com/products/
oracle9i/pdf/appclusters_cache.pdf, (3. 4. 2003).
[Ora02a]
Oracle Corporation, Oracle 9i Real Application Clusters – Concepts, Release 2
(9.2) URL: http://otn.oracle.com/docs/products/oracle9i/doc_library/
release2/rac.920/a96597.pdf, (5. 4. 2003).
[Ora02b] Oracle Corporation, Oracle 9i Application Server: mod_oc4j Technical Overview,
An Oracle Whitepaper, May 2002, URL: otn.oracle.com/products/ias/ohs/
collateral/r2/mod_oc4j_wp.pdf, (7. 5. 2003).
[Rad01]
Radware Ltd., Windows Terminal Services Load Balancing with the Web Server
Director, Techincal Report, URL,
http://www.allasso.es/base/docs/11005149796.pdf, (11. 4. 2003).
[RAJ02]
Ed Roman, Scott Ambler, Tyler Jewell, Mastering Enterprise JavaBeans, Second
Edition, Wiley (2002).
[PJ03]
Daniel Pfeifer, Hannes Jakschitsch, Method-Based Caching in Multi-Tiered
Server Applications,Technical Report, OOPSLA 2003, (2003).
[PPR01]
Macario Polo, Mario Piattini, Francisco Ruiz, Reflective CRUD (RCRUD:
Reflective Create, Read, Update & Delete), (2001).
[Sey94]
John E. Seytabla, Why we need SQL3, URL:
http://www2-iiuf.unifr.ch/ds/courses/db/pdf/whysql3.htm (1994).
[Sim03]
SimpleORM.org, SimpleORM Database Notes, URL:
http://www.uq.net.au/~zzabergl/simpleorm/DBNotes.html, (13. 4. 2003).
[SM90]
J.W. Schmitt, Florian Matthes, Language Technology for Post-Relational Data
Systems, (1990).
[Smi02]
122
Wayne D. Smith, TPC-W: Benchmarking An Ecommerce Solution, Revision 1.2,
Intel Corporation, URL: http://www.tpc.org/tpcw/TPC-W_Wh.pdf, (2002).
[SQL03]
SQLj.org, Online Resource Devoted To The Development And Dissemination Of
Information On The SQLj Set Of Standards, URL, http://www.sqlj.org,
(17. 4. 2003).
[Sun01a]
Sun Microsystems Inc., Java 2 Platform, Enterprise Edition Overview, URL,
http://java.sun.com/j2ee, (5. 4. 2003).
[Sun01b]
Sun Microsystems Inc., Java 2 Platform, Enterprise Edition v1.3 Specification.
URL, http://java.sun.com/j2ee/j2ee-1_3-fr-spec.pdf, (5. 4. 2003).
[Sun01c]
Sun Microsystems Inc, Enterprise Java Beans Technology Overview. URL,
http://java.sun.com/products/ejb, (5. 4. 2003).
[Sun01d]
Sun Microsystems Inc, Enterprise Java Beans Specification v2.0. URL,
http://java.sun.com/products/ejb/docs.html#specs, (5. 4. 2003).
[TS01]
A. Tanenbaum, M. van Steen, Distributed Systems: Principles and Paradigm,
Prentice-Hall, (2001).
[VCK96] John M. Vlissides, James O. Coplien, Norman L. Kerth, Pattern Languages of
Program Design 2, Addison-Wesley, (1996).
[Vog97]
Oliver Markus Vogel, Entwurf und Implementierung eines JDBC-Treibers,
Studienarbeit, Universität Stuttgart, Fakultät für Informatik, (1997).
[W3C03]
World Wide Web Consortium, Extensible Stylesheet Language Transformations,
Version 1.0, W3C Recommendation, URL, http://www.w3.org/TR/xslt,
(5. 4. 2003).
[Web98]
Michael Weber, Verteilte Systeme, Spektrum Akademischer Verlag, (1998).
[Wuy98]
Roel Wuyts, Declarative reasoning about the structure of object-oriented systems,
Proceedings TOOLS USA’98 IEEE Computer Society Press, S. 112-124
(1998).
[WV02]
G. Weikum und G. Vossen, Transactional Information Systems, Morgan
Kaufmann, (2002).
[XDo03]
XDoclet, XDoclet Attribute Oriented Programming for Java, URL,
http://xdoclet.dourceforge.net, (5. 5. 2003).
[YJW98]
Joseph W. Yoder, Ralph E. Johnson, Quince D. Wilson, Connecting Business
Objects to Relational Databases, (1998).
It’s the end of the world as we know it. – R.E.M.
123
Herunterladen