Wie verwaltet man Web-Sitzungsdaten

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 19. Jun 2004

Von Ted Pattison

Die Ablage und gemeinsame Nutzung von Daten in Webanwendungen kann auf verschiedene Weisen realisiert werden, die alle ihre Vor- und Nachteile mit sich bringen. Dieser Beitrag zeigt am Beispiel von Visual Basic-Lösungen die Möglichkeiten und ihre speziellen Eigenarten.

* * *

Auf dieser Seite

Strategien für ASP-Entwickler Strategien für ASP-Entwickler
Der Verwalter der gemeinsamen Properties Der Verwalter der gemeinsamen Properties
Das ASP-Application-Objekt Das ASP-Application-Objekt
Die beste Lösung Die beste Lösung
Grenzen der Pufferungsmethoden Grenzen der Pufferungsmethoden
Zugriffssperren und Gleichzeitigkeit Zugriffssperren und Gleichzeitigkeit
Durchsetzung der ACID-Regeln Durchsetzung der ACID-Regeln
Gemeinsame Nutzung von Daten über Prozess- und Maschinengrenzen hinweg Gemeinsame Nutzung von Daten über Prozess- und Maschinengrenzen hinweg

Strategien für ASP-Entwickler

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild04
Beim Entwurf einer Webanwendung erfordert gerade die Zustandsverwaltung häufig recht schwierige Entscheidungen. So müssen Sie zum Beispiel entscheiden, ob es wirklich erforderlich ist, für den Zugriff auf bestimmte Daten einen Rundgang zur Datenbank zu machen. Gelegentlich ist der Ablauf wesentlich schneller, wenn sich solche Rundgänge vermeiden lassen. Manchmal ist es aber die einzig sinnvolle Lösung, zur Bearbeitung des Auftrags vom Client auch Lese- und Schreibzugriffe auf das Datenbank-Verwaltungssystem (DBMS) durchzuführen. Diesmal geht es um die Frage, wann der Zugriff auf ein DBMS sinnvoll ist und wann nicht.

Datenpufferung auf dem Webserver
Viele Entwickler haben relativ schnell entdeckt, dass sie die Leistung einer Anwendung verbessern können, indem sie wichtige Daten, die auf mehreren ASP-Seiten oder von mehreren COM-Objekten gebraucht werden, im Arbeitsspeicher puffern. Meistens ergibt sich die Geschwindigkeitssteigerung durch die Verringerung der Anzahl der Datenbankzugriffe. Eine Webanwendung kann eben lokale Daten wesentlich schneller bearbeiten als Daten, die sie erst mühsam über das Netz hinweg aus irgendeiner Datenbank beschaffen muss.

In Visual Basic stehen Ihnen zur Pufferung von Daten auf dem Webserver drei gängige Verfahren zur Verfügung, die ich Ihnen im Folgenden vorstellen möchte. Das erste beschränkt sich auf das .BAS-Modul, das übrigens auch als Standardmodul bekannt ist. Beim zweiten wird der SPM eingesetzt, der Manager für gemeinsame Properties (shared property manager). Und schließlich eigenen sich auch noch ASP-Application-Variablen zur Pufferung der Daten. Jede dieser Methoden hat Vor- und Nachteile, die sich auf die Leistung, Synchronisation und gleichzeitige Zugriffe auswirken. Wie Sie noch feststellen werden, hat jede Methode auch ihre spezifischen Stärken. Wenn Sie also Webanwendungen mit Visual Basic entwickeln, ist es für die Leistung der fertigen Anwendung wichtig, beim Entwurf der Pufferungsstrategie einige bestimmte Faktoren zu berücksichtigen und richtig einzuschätzen.

Die erste Methode wird vollständig in Visual Basic implementiert. Sie können Daten von verschiedenen Visual Basic-Objekten aus gemeinsam benutzen, indem Sie diese Daten in einem .BAS-Modul als Konstanten und Variablen definieren. In diesem Fall kann nämlich jede Komponente im selben ActiveX-DLL-Projekt diese Konstanten und Variablen sehen. Allerdings darf man nicht vergessen, dass Konstanten und Variablen, die im .BAS-Modul definiert werden, sozusagen Privateigentum eines bestimmten Threads sind. Visual Basic speichert alle Daten aus dem .BAS-Modul im threadspezifischen TLS-Speicher ab (thread-local storage, Bild B1). Das bedeutet, dass es in einem einzigen Prozess von ein und derselben Konstanten oder Variablen mehrere Instanzen geben kann.

Bild01
B1 .BAS-Moduldaten

Wenn viele Anwender gleichzeitig Ihre Webanwendung benutzen, bearbeitet die ASP-Laufzeitschicht die Aufträge von den Clients mit Hilfe eines Vorrats an STA-Arbeitsthreads (single-threaded apartment). Sobald die ASP-Seiten Objekte von einer Ihrer Komponenten anlegen, werden diese Objekte über die verschiedenen Threads aus dem Vorrat verteilt. Wenn nun ein bestimmter Thread zum ersten Mal ein Objekt von Ihrer ActiveX-DLL anlegt, initialisiert die DLL alle ihre .BAS-Moduldaten im TLS.

Warum sollte man die Daten im .BAS-Modul halten, statt eine der anderen beiden Pufferungsmethoden anzuwenden? Der Hauptvorteil ist die wesentlich höhere Geschwindigkeit. Sie kann bis zu zehnmal höher als bei den beiden anderen Verfahren sein. Ein Objekt kann schneller auf TLS-Daten zugreifen als auf Daten, die irgendwo auf der Speicherhalde liegen (sowohl der SPM als auch das ASP-Application-Objekt sind auf Daten angewiesen, die auf der Speicherhalde, dem "Heap" liegen). Die Leistung des Verwalters der gemeinsamen Properties und des ASP-Application-Objekts werden auch durch den Umstand beeinflusst, dass zur Laufzeit ein Zugriff auf ein Wörterbuch (Dictionary) erfolgt, in dem ein bestimmter Stringwert gesucht wird.

Ein weiterer wichtiger Grund für die höhere Leistung beim Zugriff auf die Daten im .BAS-Modul ist, dass keine Sperrung von Mehrfachzugriffen erforderlich wird. Da sämtliche .BAS-Moduldaten sowieso threadspezifisch sind, gibt es keine gleichzeitigen Zugriffe durch mehrere Threads, die irgendwie synchronisiert werden müssten. Es gibt keinen Bedarf, irgendwelche Zugriffe zu sperren, um die logische Folgerichtigkeit (Konsistenz) der Daten zu gewährleisten. SPM und ASP-Application-Objekt müssen sich dagegen beide mit Synchronisationsproblemen herumschlagen. Daher setzen beide ihre eigenen Verriegelungsstrategien ein, um in Gegenwart von mehreren Threads gleichzeitige Zugriffe auf dieselben Daten zu verhindern. Während die entsprechende Sperrung und Synchronisation meistens zwingend erforderlich ist, hat die Anforderung und Rückgabe der entsprechenden Riegel einen negativen Einfluss auf die Geschwindigkeit.

Allerdings hat die Zwischenspeicherung der Daten in den .BAS-Moduldaten auch zwei gewichtige Nachteile. Beide hängen mit dem Unstand zusammen, dass von den .BAS-Moduldaten jeweils eine Instanz pro Thread angelegt wird. Erstens ist es ganz schlicht und ergreifend Speicherverschwendung, wenn im selben Prozess redundante Kopien derselben Daten gehalten werden. Zweitens bleibt man aus rein praktischen Erwägungen mehr oder weniger auf Lesezugriffe beschränkt, wenn es mehrere Kopien derselben Daten im Prozess gibt. Betrachten wir zuerst die Speicherausnutzung etwas genauer und wenden uns dann den Problemen zu, die durch die mehrfachen Kopien für den Zustand der Daten entstehen.

Wenn Sie eine Anwendung für die mittlere Schicht entwickeln, die auf den IIS, den Microsoft Transaction Services (MTS) oder auf COM+ beruht, dürfen Sie nicht vergessen, dass Sie sich auf einen Hostprozess verlassen, in dem vermutlich eine ganze Menge Threads laufen. Der Prozess für ein MTS-Serverpaket kann zum Beispiel bis zu 100 Threads enthalten. Der Prozess für eine COM+-Anwendung enthält meistens um die 8 bis 40 Threads, was unter anderem auch von der Zahl der Prozessoren abhängig ist. In den IIS 4.0 hält sich die ASP-Laufzeitschicht normalerweise einen Vorrat von 10 Threads pro Prozess und pro Prozessor, während die ASP-Laufzeitschicht im IIS 5.0 deren 25 betreibt. Wie Sie sehen, gibt es in der mittleren Schicht eine ganze Reihe von Threads in den Prozessen. Und jeder Thread erhält seine eigene private Instanz von jeder Konstanten und jeder Variablen, die im .BAS-Modul definiert wird.

Wenn Sie mit Konstanten arbeiten, die im .BAS-Modul definiert werden, tauschen Sie gewissermaßen Speicherplatz gegen Geschwindigkeit. Wieviel Speicherplatz Sie dafür brauchen, lässt sich leicht abschätzen. Multiplizieren Sie einfach die Größe Ihres .BAS-Moduls mit der Zahl der Threads im Hostprozess. Bei den heutigen Speichergiganten tauschen viele Entwickler gerne den Speicherplatz ein, um ein wenig mehr Leistung aus der Anwendung herauszukitzeln.

Es wichtig, die ActiveX-DLLs mit der Option Retained In Memory (In Speicher erhalten) zu erstellen (Bild B2). Mit dieser Option erhalten Sie die gewünschte höhere Laufleistung, weil sie die DLL während der gesamten Lebensdauer der Anwendung daran hindert, die .BAS-Moduldaten von irgendeinem Thread aus dem Speicher zu werfen. Ohne diese Option wird die DLL immer wieder einmal die .BAS-Moduldaten der einzelnen Threads aus dem Speicher werfen und bei Bedarf neu laden. Die Option Retained in Memory ist deswegen wichtig, weil sie dafür sorgt, dass die .BAS-Moduldaten nur einmal pro Thread initialisiert werden. Hierzu sollten Sie das neuste Service Pack vom Visual Studio 6.0 einspielen. Service Pack 3 behebt einige Bugs, die mit der Option Retained in Memory zusammenhängen.

Bild02
B2 Die Option "Retained In Memory" ist wichtig.

Wenden wir uns nun dem zweiten Problem mit den .BAS-Moduldaten zu. Im selben Prozess mehrere Instanzen von denselben Daten zu halten, führt leicht zu Problemen mit dem aktuellen Zustand der Daten. Verschiedene Objekte aus demselben Prozess sehen nämlich verschiedene Instanzen derselben Variablen. Sie können dadurch nicht garantieren, welche Objekte sich dieselbe Instanz teilen. Das bedeutet, dass ein Objekt, das in einer Multithreading-Umgebung läuft, nicht zuverlässig die Änderungen sehen kann, die ein anderes Objekt an der Variablen vornimmt. Und das macht es praktisch unmöglich (oder zumindest sehr schwierig), mit .BAS-Modulvariablen zu arbeiten, die nicht nur ausgelesen (read-only), sondern auch geändert werden (read/write).

Aus den gerade beschriebenen Gründen vermeiden viele Programmierer in den ActiveX-DLLs für die mittlere Schicht .BAS-Modulvariablen, soweit es geht. Andere Programmierer sind zu dem Schluss gekommen, dass sie die .BAS-Modulvariablen initialisieren können, wenn die DLL von einem Thread geladen wird. Die Dinge können sich als recht zuverlässig erweisen, wenn Sie die Disziplin aufbringen, .BAS-Modulvariablen nach ihrer Initialisierung nur noch auszulesen, aber nicht mehr zu ändern. Auf diese Weise können Sie dynamisch Werte in den improvisierten Puffer übernehmen, die nicht schon beim Kompilieren der DLL vorliegen. Durch die dynamische Übernahme der Daten ist die Lösung flexibler als eine Lösung, die sich auf Konstanten beschränkt. Sie könnten auf diese Weise zum Beispiel Daten puffern, die Sie von einem DBMS erhalten haben.

Am einfachsten lassen sich die .BAS-Modulvariablen initialisieren, indem Sie dem ActiveX DLL-Projekt eine Sub Main-Prozedur geben. Auch Sub Main muss in einem .BAS-Modul definiert werden. Wenn Sie das Startobjekt im Dialog Project/Properties auf Sub Main setzen, wird diese Prozedur aufgerufen, sobald ein Thread Ihre DLL initialisiert. Sie können also die .BAS-Modulvariablen Thread für Thread initialisieren. Vergessen Sie aber nicht, die Option "Retained In Memory" zu benutzen. Es wäre keine gute Idee, die Sub Main-Prozedur mehrfach für denselben Thread aufzurufen.

Es gibt noch einen letzten Punkt, der so wichtig ist, dass ich ihn an dieser Stelle erwähnen möchte. Wenn Sie das Threadmodell Ihres DLL-Projekts auf "Single Threaded" ("einfädig") ändern, zwingen Sie alle Objekte aus der DLL dazu, auf einem einzigen Thread zu laufen, der als Haupt-STA-Thread bekannt ist. Da sich alle Objekte, die innerhalb desselben Prozesses von einer singlethreaded DLL angelegt werden, immer denselben Thread teilen, sehen sie auch alle dieselbe Instanz einer .BAS-Modulvariablen. Das ermöglicht es Ihnen, von jedem Objekt aus, das von dieser DLL angelegt wird, eine .BAS-Modulvariable zuverlässig auszulesen und zu ändern.

Obwohl eine singlethreaded DLL bestimmte Probleme mit dem Datenzustand zu lösen scheint, schadet sie umso stärker in Situationen, in denen eigentlich gleichzeitige Zugriffe erfolgen müssten. Alle Aufträge von den Clients müssen sich durch dasselbe Nadelöhr zwängen und über denselben STA-Thread ausgeführt werden. Aus diesem Grund sollte man singlethreaded DLLs in Visual Basic-Komponenten für IIS, MTS und COM+ möglichst vermeiden.

 

Der Verwalter der gemeinsamen Properties

Die zweite wichtige Puffermethode betrifft den SPM. Damit ist eine kleine Komponentenbibliothek gemeint, die in den Laufzeitschichten von MTS und COM+ zu finden ist. Der SPM ist ein Wörterbuch mit Namen/Wertepaaren, mit denen man benannte Propertywerte lesen und schreiben kann. Der SPM hat den großen Vorteil, dass er es den Objekten, die im selben Prozess auf verschiedenen Threads laufen, ermöglicht, von einem benannten Propertywert nur eine einzige Instanz zu sehen (Bild B3). Das bedeutet, dass der SPM effizienter als .BAS-Moduldaten mit dem Speicher umgeht. Außerdem sind auch die Aktualisierungen, die ein einzelnes Objekt im SPM durchführt, zuverlässig von allen anderen Objekten im selben Prozess zu sehen, und zwar unabhängig davon, welches Objekt auf welchem Thread läuft.

Bild03
B3 Datenpufferung mit dem Shared Property Manager

Da die gemeinsamen Properties aus einer Propertygruppe von jedem Thread aus zugänglich sind, ist es wichtig, sich um die relevanten Aspekte der Synchronisation und der gleichzeitigen Zugriffe zu kümmern. Zum Glück bietet der SPM einen sehr einfachen Mechanismus an, über den ein Objekt die Properties einer Gruppe exklusiv sperren kann, um selbst eine Reihe von Lese- und Schreibzugriffen auf diese Properties durchzuführen. Anders gesagt, der SPM ermöglicht es einem Objekt, eine Sequenz von isolierten Lese- und/oder Schreibzugriffen durchzuführen. Dadurch lassen sich Szenarien verhindern, in denen ein Objekt die partielle, unvollständige Arbeit von anderen zu Gesicht bekommt.

L1 Aktualisierung von gemeinsamen Properties

Dim spm As SharedPropertyGroupManager 
Dim pg As SharedPropertyGroup 
Dim p1 As SharedProperty, p2 As SharedProperty 
Dim AlreadyExists As Boolean 
' lege zwei gemeinsame Properties an 
Set spm = New SharedPropertyGroupManager 
Set pg = spm.CreatePropertyGroup("MyGroup", _ 
                                 LockMethod, _ 
                                 Process, _ 
                                 AlreadyExists) 
Set p1 = pg.CreateProperty("MyProp1", AlreadyExists) 
Set p2 = pg.CreateProperty("MyProp2", AlreadyExists) 
' lies einen Wert aus 
Dim x As Long 
x = p.Value 
' weise einen Wert zu 
p1.Value = x + 10 
p2.Value = p1.Value + 20 
' aufräumen 
Set p1 = Nothing 
Set p2 = Nothing 
Set pg = Nothing 
Set spm = Nothing

Listing 1 zeigt ein Beispiel für eine Komponente, die zwei gemeinsame Properties isoliert aktualisiert. Der erste Punkt, auf den ich in diesem Beispiel hinweisen möchte, ist das zweite Argument, das an CreatePropertyGroup übergeben wird. Mit LockMethod wird die Isolationsstufe der Propertygruppe festgelegt. Die Isolationsstufe LockMethod weist den SPM an, die Sperre für den gesamten Methodenaufruf aufrechtzuerhalten. Sobald also ein Objekt ein Property aus der Propertygruppe anfasst, legt der SPM eine exklusive Sperre vor, die solange gilt, bis die aktuelle Methode zum Aufrufer zurückkehrt. Die Logik für die Rückgabe der Sperre ist schon im MTS und COM+ enthalten.

Eine korrekte Zugriffssperre ist für den einwandfreien Zustand der Daten unverzichtbar. Allerdings behindert sie auch den gleichzeitigen Zugriff durch mehrere Threads. Sie sollten jede Art von Sperre vermeiden, wenn die Sperre nicht unbedingt erforderlich ist. Normalerweise ist es nicht nötig, alle Daten zu einer einzigen Propertygruppe zusammenzufassen. Tatsächlich ist es meistens sogar besser, die Daten in mehrere Gruppen aufzuteilen, weil jede dieser Gruppen unabhängig von den anderen mit den Synchronisationsmechanismen hantieren kann. Obwohl das ASP-Application-Objekt für eine entsprechende Sperrung ausgelegt wurde, bietet es nur eine anwendungsweite Sperre an. Die Synchronisationsmechanismen vom SPM haben eine feinere Auflösung, die eine Optimierung von gleichzeitigen Zugriffen ermöglicht, ohne den logischen Zustand der Daten zu beeinträchtigen.

Manche gemeinsamen Propertygruppen brauchen gar keine drastischen Zugriffssperren. Wenn Sie eine Propertygruppe zusammenstellen können, die mit der Isolationsstufe LockSetGet statt LockMethod auskommt, werden die Sperren nicht für die gesamte Dauer des Methodenaufrufs aufrechterhalten. Statt dessen werden die Sperren für jeden einzelnen Propertyzugriff angefordert und wieder aufgehoben. Das bedeutet, dass LockSetGet geringere Auswirkungen auf die gleichzeitigen Zugriffe durch mehrere Threads hat. Allerdings bedeutet dies auch, dass Sie häufiger Sperren errichten und wieder aufheben. Mit der Isolationsstufe LockSetGet kann es durchaus geschehen, dass die Sperre für eine bestimmte Propertygruppe im selben Methodenaufruf mehrfach errichtet und wieder aufgehoben wird. Das ist bei der Isolationsstufe LockMethod nie der Fall.

Wichtig ist, dass sich die Sperrung mit dem SPM nicht völlig außer Kraft setzen lässt. Sie müssen die Propertygruppe entweder mit LockMethod oder mit LockSetGet anlegen. Dieser Aufwand ist unter bestimmten Umständen einfach zu hoch, besonders dann, wenn die Properties nach ihrer Initialisierung nur noch ausgelesen werden. Der Zugriff erfolgte spürbar schneller, würde der SPM das Auslesen eines Properties ermöglichen, ohne eine Sperre zu errichten und wieder aufzuheben.

Ein weiterer Nachteil des SPMs ist zudem, dass er immer exklusive Locks einrichtet. Der SPM ist einfach nicht so schlau wie ein DBMS. Ein ordentliches DBMS kennt sich mit den Riegeln aus und legt für Leseoperationen einen gemeinsamen Riegel vor, statt zum exklusiven Riegel zu greifen. Solche gemeinsamen Riegel verhindern, dass sich die Leser gegenseitig behindern. Bei einem exklusiven Riegel sperrt ein Leser alle anderen Leser aus. Daher haben die gemeinsamen Riegel (shared locks) einen geringeren Einfluss auf gleichzeitige Zugriffe und führen zu einem reibungsärmeren Betrieb.

Es gibt noch einen weiteren Grund, aus dem der SPM spürbar langsamer als .BAS-Modulvariablen ist. Wenn Sie den SPM benutzen, müssen Sie im Normalfall drei verschiedene COM-Objekte erzeugen und freigeben, wie in Listing L1 ersichtlich. Der Bau und der anschließende Abriss von Objekten erfordert natürlich Taktzyklen. Das ist ein weiterer Punkt, der den Leistungsabstand zwischen der Arbeit mit .BAS-Moduldaten und dem SPM vergrößert.

Als letzter Vorteil des SPMs sei noch seine Fähigkeit angeführt, für Lösungen in der gemeinsamen Nutzung der Daten zu sorgen, die mit .BAS-Moduldaten nicht möglich sind. So können mehrere ActiveX-DLL-Projekte Daten gemeinsam nutzen, solange sie im selben Prozess laufen. In Visual Basic geschriebene Komponenten können sich Daten mit anderen Komponenten teilen, die zum Beispiel in C++ entwickelt wurden. Allerdings darf man nicht vergessen, dass der direkte Einsatz des SPMs in einer ASP-Seite kaum praktikabel ist. Die SPM-Schnittstellen sind zwar dual, aber die SPM-Komponenten bieten Methoden an, die einige Probleme bereiten. Es ist daher nicht ratsam, den Zugriff auf diese Methoden von einer ASP-Seite aus zu versuchen, in der Sie mit VBScript oder JavaScript arbeiten.

 

Das ASP-Application-Objekt

Die letzte Pufferungsmethode, auf die ich hier näher eingehen möchte, ist der Einsatz des ASP-Application-Objekts. Ein Visual Basic-Objekt, das auf einer ASP-Seite angelegt wird, kann folgendermaßen eine ASP-Application-Variable anlegen und benutzen:

Dim appl As ASPTypeLibrary.Application 
Set appl = GetObjectContext("Application") 
appl.Value("MyProp") = "My quintessential value"

Natürlich müssen Sie die ASP-Typbibliothek sowie die MTS und COM+-Typbibliothek referenzieren, um in dieser Weise auf das ASP-Application-Objekt zugreifen zu können. Außerdem muss das IISIntrinsics-Attribut Ihrer konfigurierten Komponente für COM+ auf True gesetzt werden. Allerdings ist das der vorgegebene Standardwert und Sie brauchen im Normalfall nichts zu ändern. Dieses Attribut ist nicht über das Verwaltungsprogramm für die COM+-Dienste zugänglich, sondern nur über COM+-Admin-Objekte.

Das ASP-Application-Objekt unterscheidet sich insofern vom SPM, als dass die Namen seiner Variablen nicht prozessweit gültig sind. Statt dessen beschränken sich die ASP-Application-Variablen auf eine IIS-Anwendung (also auf ein virtuelles Verzeichnis). Es ist durchaus möglich, dass zwei verschiedene IIS-Anwendungen im selben Prozess laufen. Die ASP-Application-Variablen der einen IIS-Anwendung sind dann in der anderen weder sichtbar noch zugänglich. Und das ist gut so, weil sich die IIS-Anwendungen nicht gegenseitig stören können, nur weil es vielleicht Namenskonflikte in den ASP-Application-Variablen gibt. Andererseits stellt dies natürlich auch eine Beschränkung dar, weil sich mehrere IIS-Anwendungen keine ASP-Application-Variablen teilen können (Ich vermute, das dürfte wohl auch der Grund dafür sein, warum sie überhaupt Application-Variablen genannt werden.).

Was die Synchronisation und Zugriffssperrung anbetrifft, bietet das ASP-Application-Objekt keinerlei Kontrolle über deren Granularität an. Es gibt nur einen relativ groben Riegel für jede Anwendung. Diese Lösung ist nicht annähernd so flexibel wie der SPM, weil die Sperrung von ASP-Application-Variablen dadurch zu einer Alles-oder-Nichts-Entscheidung wird. Beim SPM können Sie eine bestimmte gemeinsame Propertygruppe sperren, ohne die gemeinsamen Properties aus den anderen Gruppen ebenfalls zu blockieren. Im Gegensatz dazu werden alle Zugriffe auf ASP-Application-Variablen blockiert, wenn ein Visual Basic-Objekt oder eine ASP-Seite eine Sperre für das ASP-Application-Objekt einrichtet, bis die Sperre aufgehoben wird. Wie der SPM benutzt auch das ASP-Application-Objekt keine gemeinsamen Riegel, sondern baut immer eine exklusive Sperre auf. Wie man sich leicht vorstellen kann, entsteht durch Konzepte, bei denen das ASP-Application-Objekt häufig gesperrt wird, in stark ausgelasteten Anwendungen ein deutlicher Flaschenhals.

Anders als der SPM zwingt das ASP-Application-Objekt Ihnen keine Sperren auf, wenn Sie keine brauchen. Schauen wir uns ein Beispiel an, bei dem das sinnvoll ist. Nehmen wir an, Sie müssten beim Start der Anwendung eine ganze Reihe von Umgebungsvariablen im ASP-Application-Objekt initialisieren. Indem Sie das ASP-Application-Objekt sperren, können Sie sicherstellen, dass kein Client die Daten sieht, bis sich Alles in einem stabilen Zustand befindet. Nach dem Abschluss der Initialisierung können Sie die Sperre aufheben und die Variablen bis zum Lebensende der Anwendung in Lesezugriffen benutzen. Solange die Daten nur ausgelesen werden, gibt es auch keinen Grund, irgendwelche Locks einzurichten und wieder aufzuheben. Der Umstand, dass die ASP-Application-Variablen ohne expliziten Synchronisationsmechanismus zugänglich sind, macht sie etwas schneller als den SPM.

Es gibt noch einen weiteren Vorteil, den die ASP-Application-Variablen gegenüber den beiden anderen Methoden haben. Sie sind direkt von ASP-Seiten und von Visual Basic-Komponenten aus zugänglich. Das kann sich in einem Projekt als sehr nützlich erweisen, an dem ASP-Anwender und Visual Basic-Programmierer gemeinsam arbeiten.

 

Die beste Lösung

Ich habe drei der gängigeren Methoden zur Pufferung von Daten auf der Serverseite beschrieben, die bei Visual Basic-Programmierern üblich sind. Inzwischen dürften Sie genug über diese Methoden wissen, um die richtigen Fragen zu stellen. Ist die Arbeitsgeschwindigkeit wichtiger oder der Speicherbedarf? Reicht der Lesezugriff auf die gepufferten Daten aus oder müssen die Variablen während der Lebensdauer der Anwendung gelegentlich aktualisiert werden? Sind irgendwelche Synchronisationsmechanismen erforderlich, um den korrekten Zustand der Daten zu gewährleisten? Lassen sich die Sperren auch tatsächlich vermeiden, wenn man sie gar nicht braucht? Wie kann man die Zahl der Variablen minimieren, die zeitweilig für andere gesperrt werden? Solche und ähnliche Fragen werden Ihnen helfen, wenn Sie für eine Webanwendung die richtige Pufferungsstrategie suchen.

Es gibt aber noch eine vierte Option. Sie könnten nämlich zu den Schluss kommen, dass keine der verfügbaren Pufferungsmethoden für Ihre Ansprüche ausreicht. So bietet zum Beispiel keine der drei gerade besprochenen Methoden irgendeine Form von Dauerhaftigkeit oder Persistenz an. Der Zugriff auf Werte im Arbeitsspeicher ist zwar sehr schnell, aber solche Werte sind in kritischen Situationen auch schnell verloren, zum Beispiel bei einem Systemversagen oder beim simplen Absturz der Anwendung. Keine der drei Methoden lässt sich so in Transaktionen einbinden, dass sich die Änderungen automatisch zurücknehmen ließen (Rollback). Und keine eignet sich für die gemeinsame Nutzung von Daten über Prozess- oder Maschinengrenzen hinweg. Schließlich können auch weder SPM noch ASP-Application-Objekt mit gemeinsamen Riegeln (shared locks) umgehen. Sie kennen nur exklusive Riegel, die eine wesentlich stärkere Auswirkung auf gleichzeitige Zugriffe haben.

Nun mag es zwar gelegentlich sinnvoll sein, DBMS-Zugriffe möglichst zu vermeiden, aber es gibt auch Zeiten, in denen gerade DBMS-Zugriffe einfach die beste (und vielleicht sogar einzig mögliche) Lösung darstellen. Bei den folgenden Anforderungen sollte man durchaus das DBMS in den Kreis der Kandidaten aufnehmen:

  1. Die Daten müssen dauerhaft gespeichert werden und sich bei Bedarf auch rekonstruieren lassen

  2. Zur Verbesserung von gleichzeitigen Zugriffen sind Leseriegel (read locks) erforderlich

  3. Im Falle eines Fehlers müssen Aktualisierungen zurückgenommen werden

  4. Die Locks müssen mit den Locks von anderen Datenquellen synchronisiert werden

  5. Die Daten sollen über Prozess- und Maschinengrenzen hinweg gemeinsam benutzt werden

 

Grenzen der Pufferungsmethoden

Wenn Sie sich eine Pufferungsstrategie ausdenken, fertigen Sie im Normalfall von den häufig benutzten Daten eine Kopie an und bringen Sie an einem Ort unter, der einen schnellen Zugriff gewährleistet. Solange die Originale (sie liegen im DBMS) und die Kopien (sie liegen nun auf dem Webserver) synchron bleiben, ist alles in Ordnung. Sobald die Daten aber aktualisiert werden müssen, drängen sich automatisch Probleme mit dem logischen Zustand der Daten auf. Wenn zum Beispiel eine andere Anwendung die Originale im DBMS ändert, sind die Kopien im Webserver einfach nicht mehr auf dem aktuellen Stand der Dinge. Die Pufferungsstrategien für Daten, die nur ausgelesen werden sollen, sind wesentlich einfacher sind als für Daten, die während der Lebensdauer der Anwendung auch geändert werden dürfen.

Schauen wir uns als Beispiel die Pufferungsstrategie für eine Webanwendung an. Stellen Sie sich vor, Sie wollten eine Webseite entwickeln, auf welcher der Anwender den aktuellen Warenbestand ablesen kann. Sie entscheiden sich dafür, die Bestandszahlen in ASP-Application-Variablen zu puffern, um sich einige Rundgänge zum DBMS zu ersparen. Nehmen wir weiterhin an, dass eine andere Anwendung die Bestandszahlen in bestimmten Abständen direkt im DBMS aktualisiert. Die Bestandszahlen, die Sie im Webserver puffern, stimmen dann schnell nicht mehr mit den aktuellen Zahlen aus dem DBMS überein. Also brauchen Sie eine geeignete Logik zur Aktualisierung der Kopien von den Bestandszahlen (Inventory Level), die Sie in Ihrer Anwendung puffern.

Eine Lösung wäre, den Bestandszahlen eine Zeitmarke zu geben. Sobald ein Client feststellt, dass die gepufferten Daten älter als irgendein konfigurierbares Aktualisierungsintervall sind (zum Beispiel fünf Minuten), machen Sie wieder einen Rundgang zum DBMS und besorgen die neusten Bestandszahlen, die Sie natürlich wieder puffern. Für die nächsten fünf Minuten löst dann keine Anfrage vom Client einen neuen Rundgang zum DBMS aus.

Eine Auffrischungsmethode wie diese bringt einen spürbaren Leistungsgewinn mit sich, sofern die Anwendung kleinere Abweichungen zwischen angezeigtem und tatsächlichen Warenbestand tolerieren kann. Es gibt nämlich immer das Risiko, dass der Anwender veraltete Werte sieht. Wenn Sie garantieren müssen, dass die Anwendung auf jeden Fall immer gültige Werte anzeigt, müssen Sie für jede Client-Anfrage einen Rundgang zum DBMS machen. Wie Sie sehen, geht es hier um den Tausch von Leistung gegen Genauigkeit. Was sich für eine Anwendung als optimale Lösung erweist, kann für die andere völlig ungeeignet sein.

Betrachten wir nun eine schwierigere Situation. Stellen Sie sich vor, es ginge um die Implementierung einer Methode, die bei jeder Client-Anfrage die gepufferten Daten aktualisieren soll. Es wird ja nichts auf die Festplatte geschrieben, wenn Sie ein Datum im SPM oder im ASP-Application-Objekt ändern. Das bedeutet, dass sich Aktualisierungen im Falle eines Absturzes der Anwendung nicht rekonstruieren lassen. In den meisten Anwendungen ist der Verlust von aktualisierten Daten völlig inakzeptabel. Wenn Ihre Methode zurückkehrt, ohne die Meldung eines Fehlers auszulösen, soll der Client die Garantie dafür haben, dass seine Änderungen in einer dauerhaften Form abgespeichert wurden und sich im Falle eines Programmabsturzes auch rekonstruieren lassen.

Nun wird die Implementierung einer einfachen Methode zur Abspeicherung der Aktualisierungen auf dem Laufwerk zwar nicht allzu schwierig sein, aber der Code zur Rekonstruktion der Werte nach einem Absturz oder die Optimierung der Antwortzeiten bei umfangreicheren Aktualisierungen dürften schon um einiges aufwendiger werden. Versuchen Sie doch als Beispiel einmal die folgenden Fragen zu beantworten. Woher weiß Ihre Anwendung beim Start, dass es einen Absturz gegeben hat und einige Daten vom Laufwerk rekonstruiert werden müssen? Wie sichern Sie die persistenten Daten im Zuge eines Backups, der die Daten im Falle eines Festplattenversagens schützen soll, und wie spielen Sie diese gesicherten Daten wieder ein? Wie viele Daten müssen Sie im Falle einer Aktualisierung auf die Festplatte hinausschreiben, bevor Sie die Kontrolle wieder an den Client übergeben können? Ist eine Optimierung in der Form möglich, dass Sie nur eine minimale Datenmenge hinausschreiben, die den Unterschied zwischen den alten und den neuen Daten repräsentiert? (Diese Art von Optimierung findet sich zum Beispiel im Transaktionslog vom SQL Server.)

Die gerade angesprochenen Fragen sind alle wichtig und müssen bei der Entwicklung einer Pufferungsstrategie für eine mehrschichtige Anwendung bedacht werden. Denken Sie einmal ganz vorsichtig daran, wieviel Zeit es wohl beanspruchen wird, den Code zu entwerfen, zu implementieren, zu testen und zu debuggen, der die Aktualisierungen dauerhaft speichert und rekonstruierbar macht. Und nun vergleichen Sie diese Investition einfach mal mit den Lizenzkosten für die Installation eines SQL Servers. Wenn der SQL Server gegenüber der selbstentwickelten Pufferung eine vergleichbare Leistung bietet, dürfte er vermutlich eine Menge Zeit und Geld einsparen. Bevor Sie aber entscheiden, ob Sie ein DBMS brauchen, gibt es noch einige Dinge, über die Sie Bescheid wissen sollten.

 

Zugriffssperren und Gleichzeitigkeit

Zu den erwähnenswerten Beschränkungen von SPM und ASP-Application-Objekt gehört deren Unfähigkeit, gemeinsame Locks einzusetzen. Beide bieten nur exklusive Locks an. Sobald sich aber ein Client solch einen exklusiven Riegel für ein bestimmtes Datum beschafft, blockiert er damit jeden anderen Client, bis er die Sperre wieder aufhebt. In vielen Situationen sind exklusive Sperren einfach übertrieben. Gemeinsame Sperren reichen meistens völlig aus, um den logischen Zustand der Daten zu schützen, und wirken sich zudem nicht so stark auf gleichzeitige Zugriffe aus.

Ein DBMS bietet eine optimierte Form der gleichzeitigen Zugriffe an, indem es exklusive Sperren durch gemeinsame Sperren ergänzt. Ein Client, der eine gemeinsame Sperre vorlegt, blockiert nur solche Clients, die sich an Schreibzugriffen versuchen. Eine gemeinsame Sperre (shared lock) hält aber keine anderen Clients ab, die nur Lesezugriffe auf dieselben Daten durchführen. Durch diese höhere Granularität im Zugriffsschutz erhalten Sie schnellere Antwortzeiten und einen höheren Durchsatz, weil sich die reinen Leser nicht mehr gegenseitig behindern.

Wegen ihrer einfachen Synchronisationsmethoden werden SPM und das ASP-Application-Objekt am besten für Daten benutzt, die beim Start der Anwendung geladen und später nur ausgelesen werden. Unter Vorbehalten lassen sie sich auch noch in Szenarien einsetzen, in denen die Änderungen an den gepufferten Daten nur vergleichsweise selten eintreten. Wie schon erwähnt, lässt sich die Programmleistung durch eine regelmäßige Aktualisierung des Datenbestands erhöhen, wenn dadurch die Zahl der Rundgänge zum DBMS kleiner wird. In großen Anwendungen mit häufigen Aktualisierungen braucht man allerdings eine bessere Synchronisation. Weder der SPM noch das ASP-Application-Objekt können dann mithalten.

Welche Optionen haben Sie? Sie könnten eine C++-Implementierung vornehmen, die dem SPM ähnelt und neben der exklusiven Sperre auch die Logik für gemeinsame Locks enthält. Allerdings dürfte der Aufwand beträchtlich sein und zudem Erfahrungen erfordern, über welche Ihre Firma nicht unbedingt verfügt. Als Alternative könnten Sie sich viel Zeit und Geld ersparen, indem Sie einfach auf die Sperrmechanismen zurückgreifen, die von einem ausgereiften DBMS angeboten werden.

Sie werden überrascht sein, wie gut der SQL Server läuft, wenn er auf derselben Maschine installiert wurde, auf der auch die fragliche Webanwendung läuft. Zwar erfordert auch die Bearbeitung von Daten aus der lokalen SQL Server-Datenbank prozessüberschreitende Aufrufe, aber letztlich ist der Vorgang auch nicht wesentlich langsamer als der Zugriff via SPM oder ASP-Application-Objekt auf Daten, die im Arbeitsspeicher liegen. Überzeugen Sie sich durch entsprechende Messungen selbst. Die Leistungsunterschiede sind viel kleiner, als manche Entwickler befürchten. Die interne Datenpufferung im SQL Server ist recht flott und durch die gemeinsamen Locks (shared locks) wird die Leistung Ihrer Anwendung profitieren.

 

Durchsetzung der ACID-Regeln

Obwohl der SPM und das ASP-Application-Objekt durch die exklusiven Locks eine gewisse Isolierung bieten, lassen sie sich nicht mit einem DBMS vergleichen, weil sie die ACID-Regeln nicht durchsetzen können (atomicity, consistency, isolation, durability - also atomare Operationen, logische Folgerichtigkeit, Isolierung und Dauerhaftigkeit). Wenn Sie mehr über diese Regeln erfahren möchten, finden sie in zahlreichen Artikeln im System Journal, unter anderem in meinem Artikel "So unterstützt der MSMQ verteilte Anwendungen" im System Journal 06/1999, S. 64.

Wir haben bereits festgestellt, dass weder der SPM noch das ASP-Application-Objekt eine dauerhafte Speicherung der Daten anbieten. Überlegen wir nun kurz, wie "atomar", also unteilbar die Vorgänge sind. Weder der SPM noch das ASP-Application-Object bieten irgendeine Art von Rücknahme der laufenden Vorgänge an (Rollback). Was geschieht zum Beispiel, wenn Sie aus einer laufenden COM+-Transaktion heraus die Daten im SPM ändern und später die Transaktion zurücknehmen? Die Änderungen, die Sie im SPM vorgenommen haben, werden nicht automatisch rückgängig gemacht. Das bedeutet, dass Sie noch den Code entwickeln müssen, der Fehlerbedingungen erkennt und die Änderungen "von Hand" rückgängig macht. Wenn Sie die Änderungen dagegen in ein DBMS schreiben, lassen sich die Änderungen durch den simplen Abbruch der laufenden Transaktion automatisch rückgängig machen. Das ist ein großer Vorteil, denn Sie ersparen es sich, den betreffenden Code selbst entwickeln zu müssen.

Außerdem sollte man sich darüber im Klaren sein, dass die Locks, die vom SPM oder ASP-Application-Objekt eingerichtet werden, nicht mit anderen Datenquellen koordiniert werden. Wenn Sie zum Beispiel aus einer laufenden COM+-Transaktion heraus einen Wert im SPM und einen Wert in einer Oracle-Datenbank ändern, werden die Sperren auf diesen Datenelementen nicht synchronisiert. Der SPM hebt die Sperre normalerweise auf, sobald die aktuelle aufgerufene Methode zum Aufrufer zurückkehrt, also noch bevor der DTC (der Koordinator der verteilten Transaktionen) sein zweiphasiges Abschlussprotokoll einleitet. Diese mangelnde Synchronisation kann zu Verletzungen der ACID-Regeln führen.

Was würde sich in diesem Szenario ändern, wenn man die Datenelemente vom SPM in eine lokale SQL Server-Datenbank verlegt? Da der SQL Server zugleich ein Ressourcenmanager ist, können Sie die Sperren, die von mehreren Ressourcenmanagern im Rahmen einer COM+-Transaktion eingerichtet werden, leicht synchronisieren. So könnten Sie zum Beispiel aus der laufenden COM+-Transaktion eine Verbindung zur lokalen SQL Server-Datenbank herstellen und eine zweite Verbindung zur fernen Oracle-Datenbank. Da beide Verbindungen automatisch in die verteilte Transaktion eingetragen werden, synchronisiert der DTC die Locks, die von beiden Datenbanksystemen eingerichtet werden, und die ACID-Regeln bleiben gewahrt.

 

Gemeinsame Nutzung von Daten über Prozess- und Maschinengrenzen hinweg

Weder der SPM noch das ASP-Application-Objekt ermöglichen die gemeinsame Nutzung von Daten über Prozessgrenzen hinweg. Zwei Visual Basic-Objekte können nur dann dieselben SPM-Daten sehen, wenn sie auch im selben Prozess laufen. Dasselbe ASP-Application-Objekt ist für zwei Visual Basic-Objekte oder ASP-Seiten nur dann sichtbar, wenn sie im selben Prozess und in derselben ASP-Anwendung laufen.

Wie teilen sich nun zwei Visual Basic-Objekte Daten, die zwar auf demselben Computer, aber in verschiedenen Prozessen liegen? Eine denkbare Lösung wäre zum Beispiel der Rückgriff auf Win32-Programmiermethoden, zum Beispiel auf Dateien, die auf den Speicher abgebildet werden (memory mapped files). Leider ist diese Lösung in Visual Basic nur relativ umständlich zu implementieren. In solchen Fällen wechselt man am besten auf Programmiersprachen, die sich von Haus aus besser dafür eignen, zum Beispiel auf C++.

Als Alternative könnte man die Daten in eine Datei hinausschreiben, damit sie von mehreren Prozessen gemeinsam benutzt werden können. So können sich zum Beispiel Visual Basic-Objekte, die in verschiedenen Prozessen laufen, die Daten teilen, indem sie die Werte in eine gemeinsame Datei schreiben und von dort auslesen. Allerdings haben Sie bei dieser Lösung immer noch die Probleme mit der dauerhaften Speicherung zu lösen, von denen bereits die Rede war. Außerdem ist es mehr als wahrscheinlich, dass Sie zur Vermeidung von gleichzeitigen Zugriffen auch einen entsprechenden Synchronisationsmechanismus entwickeln müssen. Und wie Sie sich sicher lebhaft vorstellen können, ist es nicht ganz einfach, einen Verriegelungsmechanismus zu entwickeln, der mehreren Prozessen den gleichzeitigen Zugriff auf dieselbe Datei gestattet, ohne den logischen Zustand der Daten zu gefährden.

Wie Sie sehen, ist die gemeinsame Nutzung der Daten von verschiedenen Prozessen aus keine leichte Aufgabe. Über Computergrenzen hinweg wird das Problem naturgemäß noch größer. Allerdings ist die gemeinsame Nutzung von Daten über die Maschinengrenzen hinweg eine übliche Forderung an die Anwendungen. Das gilt besonders dann, wenn der Code in einer Webfarm laufen soll, wo die Aufträge von den Clients an verschiedene Webserver weitergeleitet werden. Welcher Auftrag dabei an welchen Server gelangt, ist meistens kaum vorhersagbar. Ihr Code muss in der Lage sein, von jedem der Webserver in der Farm aus auf die Anwendungs- und Sitzungsdaten zuzugreifen.

Während die gemeinsame Nutzung der Daten von verschiedenen Prozessen aus auf einer lokalen Maschine mit einfachen Dateien zu bewerkstelligen sein mag, ist diese Lösung für ein größeres Netz kaum zu gebrauchen. In einer Anwendung, die für eine mittlere bis hohe Nutzerzahl ausgelegt wird, ist es nicht sehr sinnvoll, mit gemeinsamen Dateien zu arbeiten, die irgendwo in einem freigegebenen Verzeichnis liegen. Wer schon einmal versucht hat, eine Anwendung auf eine größere Nutzerzahl umzustellen, die mit Access-Dateien arbeitet (.MDB), wird das bestätigen können. Es muss auf der Maschine, auf der die gemeinsamen Daten liegen, einen speziellen Prozess geben, dessen einzige Aufgabe es ist, die I/O-Zugriffe auf das lokale Dateisystem durchzuführen.

Schauen wir uns einmal genauer an, wie man die Sitzungsdaten in einer Webfarm ohne DBMS verwalten könnte. Sie könnten zum Beispiel einen Computer (einen COM+-Anwendungsserver) dafür abstellen, die Sitzungsdaten für alle Kunden aufzunehmen. Zur Bearbeitung eines Kundenauftrags auf einem der Webserver aus der Farm können Sie auf dem COM+-Anwendungsserver ein Objekt aktivieren. Objekte, die auf dem COM+-Anwendungsserver laufen, können alle im selben Prozess laufen und daher mit dem SPM auf die gemeinsamen Daten zugreifen. Sobald ein zweiter Auftrag vom selben Client eintrifft und an einen anderen Webserver aus der Farm weitergeleitet wird, können Sie wiederum im COM+-Anwendungsserver ein Objekt aktivieren, das auf die Daten zugreifen kann, die beim ersten Auftrag geschrieben wurden. Dieser Lösungsansatz stellt gewissermaßen die Grundlage für den Lese- und Schreibzugriff auf gepufferte Daten in einer Webfarm dar.

Wenn Sie es erst einmal soweit getrieben haben, möchten Sie sicherlich auch noch die Logik für die dauerhafte Speicherung der Daten einbauen und die Daten zudem gegen Systemfehler schützen. Sofern gleichzeitige Zugriffe auftreten können, die zu logischen Fehlern in den Daten führen, brauchen Sie auch noch einen geeigneten Synchronisationsmechanismus. Sollen die Änderungen bei Bedarf rückgängig gemacht werden, brauchen Sie wohl einen Aktualisierungspuffer, mit dessen Hilfe Sie die automatische Rücknahme von Änderungen implementieren. Sollen die Sperren und Riegel zudem mit anderen Datenquellen synchronisiert werden, müssen Sie auch noch den Code einbauen, der Ihre Anwendung zum Ressourcenmanager macht, der mit dem DTC zusammenarbeiten kann.

Was will ich mit diesem Beispiel sagen? Nun, es zeigt, dass die meisten der heutigen mehrschichtigen Anwendungen komplexe Probleme mit sich bringen, die bereits vom DBMS gelöst wurden. Es gibt keinen Grund, das Rad neu zu erfinden (und wer hat schon die Zeit dazu). Bis zu einem gewissen Grad spielt es dabei eigentlich keine Rolle, ob Sie nun zum SQL Server greifen, zu Oracle, zu DB2 oder einem anderen ausgereiften Produkt. Die DBMS-Entwickler haben sich bereits die Mühe gemacht, dieselben Probleme vernünftig zu lösen, auf die Sie als Anwendungsentwickler immer wieder stoßen. Deren Code hat schon manche Generation durchlaufen, mit Re-Design, Implementierung, Test, Fehlersuche und dem Einsatz in der Praxis. Wenn Sie Geld für eine DBMS-Lizenz ausgeben, erhalten Sie normalerweise auch einen ansehnlichen Gegenwert.

Vielen Entwicklern schwebt der Gedanke vor, sie könnten die Leistung ihrer Anwendung steigern, indem sie bestimmte Sitzungs- und Anwendungsdaten in der Webfarm puffern. Allerdings sind schon viele Firmen, die sich an solchen Lösungen versucht haben, über kurz oder lang zu dem Schluss gekommen, dass sie auch mit viel Aufwand oft nicht wesentlich schneller werden als eine viel einfachere Lösung, die mit einem handelsüblichen DBMS arbeitet.