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