Caching

Caching wird verwendet, um einen performanten Zugriff auf Daten zu ermöglichen und der Zugriff auf die Originalquelle für den gegebenen Kontext zu langsam ist. Typische Beispiele hierbei sind Zugriffe auf eine Datenbank oder Daten, die per Serviceaufruf geladen werden. Dabei sind typische Engpässe Netzwerk- und Festplattenzugriffe. Caching ist nur relevant für das Lesen von Daten.

Wo ist cachen sinnvoll?

Immer, wenn auf eine andere Schicht mit Prozesswechsel lesend zugegriffen wird, kann Caching relevant werden. Dabei sollte das Caching vor dem Schichtenwechsel erfolgen, wobei das an verschiedenen Stellen innerhalb einer Schicht und in unterschiedlichen Schichten angewendet werden kann. Wo das sinnvoll ist, hängt sehr von der Häufigkeit von Datenänderungen, technischer Unterstützung und weiteren Faktoren ab. Das folgende Bild zeigt exemplarisch relevante Stellen, bei denen Caching in Frage kommt:

Die Stellen (a) bis (e) sind für das Caching relevant, haben allerdings unterschiedliche Vor- und Nachteile, die wir ein wenig beleuchten.

  • (a) - Im Workflow kann das Resultat von Serviceaufrufen gecacht werden.
    Anmerkungen: In Workflows ist das Cachen normalerweise nicht sinnvoll, da durch die asynchrone Verarbeitung selten Aufrufe Performance-kritisch sind bzw. dies signifikant ist. Die Serviceaufrufe erfolgen ohnehin per Request-/Reply und somit nicht synchron. Siehe Messaging.
  • (b) - Im UI bietet sich das Caching insbesondere für Daten an, dies sich sehr selten ändern, um das Laden von Seiten verbessern, aber auch um das Laden z.B. von Tabellen zu beschleunigen.
    Anmerkungen: Im UI gibt es noch keine Unterstützung für das Caching seitens des Frameworks. Insbesondere ist dabei zu berücksichtigen, dass Daten veralten können! Bei dem Caching im Service - also eine Schicht tiefer - bietet unser Framework eine komfortable Unterstützung an, Service-übergreifend Änderungen zu propagieren und damit die Caches zu invalidieren (d.h. veraltete Daten aus dem Cache zu entfernen)! Das wird weiter unten beschrieben.
  • (c) - In der Service-Fassade bietet es sich an, Responses zu Requests zu cachen, die eine entsprechende Id haben.
    Anmerkungen: Implementiert man auch für lesende Zugriffe Idempotenz mit unserer Idempotency-API, so werden durch den Client ausgeführte Retries (siehe Resilienz) "gecacht" (dies ist genau genommen nicht dasselbe, hat aber für Retries denselben Effekt). Idempotenz müssen für lesende Operation eigentlich nicht implementiert werden, da diese inhärent idempotent sind. Sie mache aber wie gerade beschrieben das System performanter und resilienter - auch im lesenden Fall.
    Möchte man im Fall (c) auch Caching für andere Fälle nutzen, so muss ein eindeutiger Schlüssel als Identifikator für einen passenden Response vorliegen. Kritisch kann aber hier das Invalidieren sein. Für Services, die nur Daten des eigenen Bausteins liefern, hat man über Business Events die ungültige Daten signalisieren, noch relativ unkritisch und beherrschbar. Sind die Daten externer Bausteine in den Responses enthalten, so ist dies wirklich kritisch, da man ggf. nicht die Business Events kennt. Hier sollte man darauf achten, dass es sich bei externen Events zur Invalidierung um Events von EntityServices handelt. Ggf. reicht aber auch, dass die Daten nach einer gewissen Zeit invalidiert werden.
  • (d) - Hier nutzt der Service lesend einen anderen, aus Sicht des Bausteins externen Service und cacht die erhaltenen Daten.
    Anmerkungen: Hier bietet das Framework eine geeignete Lösung, die weiter unten beschrieben ist. Auch diesem Fall ist das Invalidieren wie unter (c) beschrieben, genau zu bewerten!
    Dieser Fall ist eigentlich nur wirklich relevant, wenn die eigentliche Business-Logik externen Daten benötigt und hier eine entsprechende Performance notwendig ist. Orchestrierende Query-Operationen sollten mit äußerster Vorsicht implementiert werden, da diese das Timeout-Risiko deutlich erhöhen (vgl. Resilienz). Hier kann die Caching-Implementierung unseres Frameworks aber helfen. Am besten ist es aber die Daten der externen Quelle in der eigenen Datenbank zu "cachen", da dies auch bei Applikationsstart wirkt. Siehe hierzu Muster: Daten (fremder Länder) für eigene Zwecke vorhalten.
  • (e) - In diesem Fall werden Daten der Datenbank - gekapselt über ein Repository - gecacht.
    Anmerkungen: Der Einsatz von Caches vor einem Repository ist bei QueryServices (dort ist der Begriff Repository nicht ganz korrekt, vgl. Muster: CQRS Light in Kombination mit Muster: Lokales Caching für DB-Zugriff ) und für die Nutzung von Caching in eigenen Services relevant: An dieser Stelle werden die Daten eines Aggregates gecacht. Als Caching-Schlüssel fungiert hier beispielsweise eine EntityId.
  • (f) - Dieser Fall ist identisch zu Fall (c).

Die Hauptanwendungsfälle sind also das Cachen von Daten von Fremdanwendungen - insbesondere von Daten, die per Gateway zu externen Services gelesen werden sowie das Caching von Aggregate von einem Repository. Insbesondere hierzu bieten wir eine geeignete Unterstützung durch das Schleupen.CS Framework an.

Vorsicht! Wie bereits angedeutet, ist es sehr wichtig im Blick zu haben, dass Daten veralten können. Hierbei ist insbesondere zu beachten, dass Caches bei der Implementierung in Schleupen.CS auf jedem Server existieren. Letzten Endes müssen hier die Anforderungen geklärt sein.

Unterstützung lokaler Caches mit Host-übergreifender Validierung per Schleupen.CS

Schleupen.CS bietet mithilfe von Bibliotheken eine Lösung für das Cachen an. Diese cacht Daten lokal, d.h. pro w3wp-Prozess, also pro Host. Das Invalidieren kann Server-übergreifend durch das Abonnieren von Business Events erfolgen.

Die zwei Kernbestandteile dieser Lösung sind der Cache selber und ein sogenannter Invalidator. Der Cache selber wird genauso wie erwartet verwendet. Dies zeigen die nächsten beiden Abbildungen, bei denen eine Abfrage ausgeführt wird. Liegen die Daten nicht vor - erster Aufruf - so werden diese aus der Datenbank per Repository gelesen:

Beim zweiten Aufruf befinden sich die Daten im Cache, sodass diese aus diesem zurückgegeben werden:

Das Caching für dem Repository ist für externe Daten sehr ähnlich.

Hierbei sind der intern benutzte System.Runtime.Caching.MemoryCache eine Singleton und die zu nutzende Cache-Klasse transient im DI-Container registriert.

Werden nun in diesem Beispiel Aggregate geändert (Schritt (1), (2)) und persistiert (Schritte (3)), so müssen alle Server hiervon Kenntnis erhalten. Dies erfolgt durch Auslösen von Business Events (Schritt (4), (6)). Dabei abonniert die eigene Anwendung die eigenen Business Events, um auf demselben, aber auch auf anderen Servern im Cluster zu invalidieren ((7)).

Für den Server, auf dem das Aggregate geändert worden ist lässt sich dies optimieren!

Das Invalidieren wiederum erfolgt durch den Invalidator. Dieser ist als Singleton im DI-Container registriert, damit er dauerhaft (also unabhängig von Service-Calls) auf Events des Message Brokers reagieren kann. Der Invalidator hat eine transiente Subskription, hängt also direkt am Exchange im RabbitMQ und benötigt somit keine Queue. Dadurch ist die Benachrichtigung sehr zeitnah.

Cookie Consent mit Real Cookie Banner