Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Lokales Caching

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

Der Hauptanwendungsfall ist das Cachen von Daten von Fremdapplikationen, daher werden insbesondere die Daten, die über ein Gateway zu entfernten Objekten geliefert werden, gecachet.

Dieses Muster soll aufzeigen, wie ein ein Cache implementiert wird, der lokal Daten zwischenspeichert. Zudem wird hiermit gelöst, wie Daten im Cache ungültig werden. Hier erfolgt über Framework-Klassen im Standard eine Anbindung an den Microsoft Servicebus oder RabbitMQ.

Design

Das folgende Diagramm gibt einen Überblick über die Komponenten des Cachings.

Implementierung

Es ist vorgesehen, Caching entweder für Daten von Serviceaufrufen (also Daten aus einen Gateway) oder für Daten von Datenbankenabfragen (also Daten aus einen Repository) zu verwenden.
Dieses Dokument erklärt, wie man Daten von Service- und Repositoryaufrufen ab Platform 3.11 cachen kann.

Um Caching in einer Fachanwendung zu verwenden, ist folgendes zu implementieren. Die einzelne Teile werden weiter unten in diesem Dokument erklärt.

  • Implementierung und Interface von CacheInvalidator. Hier reicht es von der Basisklasse CacheInvalidator/Interface ICacheInvalidator abzuleiten und im Konstruktor die Events zu abonnieren
  • Implementierung und Interface von Cache. Hier reicht es, von der Basisklasse Cache/Interface ICache abzuleiten
  • [Optional] Implementierung eines CacheItemKeyResolver, falls beim Auslösen eines BusinessEvents nur bestimmte Einträge eines CacheScopes ungültig werden sollen
  • Implementierung und Interface Caching-Gateway bzw. Caching-Repository zum Anschluss des Caches
  • Auch Daten eines Repositorys - also Daten des eigenen Landes - werden über die Anbindung von transienten Subscriptions aktualisiert. Dies ist so notwendig da alle Prozesse diese gecacheten Daten lokal in ihrem Prozess halten. Da aber alle Caches aktualisiert werden müssen - also auch über Prozessgrenzen hinweg - müssen alle Caches in allen Prozessen informiert werden.
  • Die Objekthierarchien müssen vollständig geladen werden.
Caching von Elementen im Gateway

In diesem Beispiel werden Buchtitel, die mit einen bestimmten String beginnen, über einen Query-Service abgefragt und gecachet. Hierbei soll der ganze Cache ungültig markiert werden, wenn ein Buch hinzugefügt, entfernt oder aktualisiert wird.

Das folgende Beispiel beinhaltet eine Besonderheit: pro gecachtem Schlüssel werden mehrere BuchTitel (also BuchTitel[]) gecachet. Für zu cachende Einzelinstanzen wie eine Person ist dann Person anstelle von BuchTitel[] zu verwenden.

internal class BuchTitel
{
    public Guid Id { get; set; }

    public string Titel { get; set; }
}
CacheInvalidator

Erst muss eine konkrete Implementierung von CacheInvalidator und dessen Interface erstellt werden. Die Schnittstelle wird dem BusinessCases-Projekt und die Implementierung zum Gateways-Projekt hinzugefügt.

Die konkrete Implementierung von CacheInvalidator wird automatisch durch das Framework als Singleton registriert. Eine manuelle Registrierung ist nicht nötig.

Die konkrete Implementierung soll von der Basisklasse CacheInvalidator ableiten und dessen Interface von ICacheInvalidator ableiten.

Die konkrete Implementierung bekommt per Konstruktor-Injektion einen ICacheScopeBuilder. Es werden zwei Implementierungen von ICacheScopeBuilder zu Verfügung gestellt:

  • Fachdaten, die pro Systemstrukturelement / Verwendungszweck getrennt werden sollen, sollen ISystemStructureCacheScopeBuilder injekten.
  • Infrastrukturdaten, die Systemstruktur unabhängig sind, sollen IGlobalCacheScopeBuilder injekten.

In dem Konstruktor der konkreten Implementierung sollen die Events abonniert werden. In diesem Beispiel, soll der ganzen Cache ungültig werden, wenn einen Buch hinzugefügt, entfernt oder aktualisiert wird. In dem Konstruktor, werden die relevanten Created-, Deleted- und UpdatedEvents abonniert.

internal interface IBuchTitelCacheInvalidator : ICacheInvalidator
{
}
internal class BuchTitelCacheInvalidator : CacheInvalidator, IBuchTitelCacheInvalidator
{
    public BuchTitelCacheInvalidator(IBusinessEventForwarder businessEventForwarder)
        : base(businessEventForwarder)
    {
        Subscribe<CreatedEventService.IBuchCreatedEventService, CreatedEventService.RaisedNotification>();
        Subscribe<DeletedEventService.IBuchDeletedEventService, DeletedEventService.RaisedNotification>();
        Subscribe<UpdatedEventService.IBuchUpdatedEventService, UpdatedEventService.RaisedNotification>();
    }
}
[Optional] CacheItemKeyResolver<TEventRequest>

Der CacheItemKeyResolver ist optional, wird aber in diesem Beispiel benötigt, da nur einzelne Bücher ungültig werden.

Cache<T>

Als Nächstes muss eine konkrete Implementierung von Cache<TItem> und dessen Interface erstellt werden. Die Implementierung und der Interface werden in BusinessCases hinzugefügt. Die Implementierung wird automatsich mit Default Lifestyle in Castle registriert.

Die konkrete Implementierung erbt von der Basisklasse Cache<TItem> und dessen Interface von ICache<TItem>.

Die Cache-Implementierung ermöglicht den typisierten Zugriff auf den .NET Memory Cache. Hier wird per Konstruktur-Injektion die konkrete Implementierung des CacheInvalidators von der vorherigen Schritt injiziiert.

internal interface IBuchTitelCache : ICache<BuchTitel[]>
{
}

internal class BuchTitelCache : Cache<BuchTitel[]>, IBuchTitelCache
{
    public BuchTitelCache(
        IBuchTitelCacheInvalidator buchTitelCacheInvalidator,
        ISystemStructureCacheScopeBuilder cacheScopeBuilder)
        : base(buchCacheInvalidator, cacheScopeBuilder)
    {
    }
}

Die Eigenschaften AbsoluteExpirationOffset oder SlidingExpiration auf der Basisklasse Cache<TItem> können überschrieben werden.

  • AbsoluteExpirationOffset
    Gibt eine fixten Zeitraum an, nachdem der Cache automatisch ungültig wird.
  • SlidingExpiration
    Falls der Cache inaktiv ist für das angegebene Zeitraum, wird der Cache automatisch ungültig.
Caching-Gateway

Der Cache wird durch Implementierung eines Caching-Gateway vor der Gateway angeschlossen. Die Implementierung des Caching-Gateways wird automatsich mit Default Lifestyle registriert. Sowie beim normalen Gateway, soll das Interface zum BusinessCases-Projekt und die Implementierung dem Gateways-Projekt hinzugefügt werden.

Die Implementierung des Caching-Gateways ist ähnlich zum Decorator-Pattern vom urspruglichen Gateway, aber es sollten nicht notwendigerweise alle Methoden des urspruglichen Gateways implementiert werden - nur die für das Caching relevanten.

Das Caching-Gateway bekommt die Cache-Klasse und das normale Gateway per Konstruktor-Injektion. Zuerst wird im Cache gesucht; falls die Daten nicht im Cache vorhanden sind, wird das normale Gateway aufgerufen, die Ergebnis gecachet und dann anschließend zurückgegeben.

internal interface IBuchServiceGateway
{
    BuchTitel[] QueryByTitel(string titelBegintMit);
    void UpdateBuch(Buch buch);
}
internal interface ICachingBuchServiceGateway
{    
    BuchTitel[] QueryByTitel(string titelBegintMit);
}
internal class BuchServiceGateway : IBuchServiceGateway
{ 
    public BuchTitel[] QueryByTitel(string titelBegintMit)
    {
         // ...
    }
}

internal class CachingBuchServiceGateway : ICachingBuchServiceGateway
{
    private readonly IBuchServiceGateway buchServiceGateway;
    private readonly IBuchTitelCache buchTitelCache;

    public CachingBuchServiceGateway(IBuchServiceGateway buchServiceGateway, IBuchTitelCache buchTitelCache)
    {
        // ...
        this.buchServiceGateway = buchServiceGateway;
        this.buchTitelCache = buchTitelCache;
    }

    public BuchTitel[] QueryByTitel(string titelBegintMit)
    {
        return buchTitelCache.GetOrSet(
            titelBegintMit,
            () =>
                {
                    return buchServiceGateway.QueryByTitel(titelBegintMit);
                });
    }
}
Anschluss
public sealed class BuchQueryController : IBuchQueryController
{
    private readonly ICachingBuchServiceGateway cachingBuchServiceGateway;

    public BuchVerwaltenController(ICachingBuchServiceGateway cachingBuchServiceGateway)
    {
        // ...
        this.cachingBuchServiceGateway = cachingBuchServiceGateway;
    }

    public BuchTitel[] QueryByTitel(string titelBegintMit)
    {
        // ...
        return cachingBuchServiceGateway.QueryByTitel(titelBegintMit);
    }
}

Der Windsor Container für den ServiceBusClient ist zu konfigurieren. Hierbei werden die benötigen Klassen von Framework und SB registriert.

public class ServiceHostFactoryConfigurator : WcfContainerConfigurator
{
    // ...

    protected override void OnContainerConfigured(IWindsorContainer windsorContainer)
    {
        if (windsorContainer == null) { throw new ArgumentNullException(nameof(windsorContainer)); }

        windsorContainer.Install(new ServiceBusClientInstaller(IsInsideWcfOperation));
        // ...

        base.OnContainerConfigured(windsorContainer);
    }
}
Caching von Elementen im Repository

In diesem Beispiel werden Bücher über die Methode QueryById() von einem Repository abgefragt und gecachet. Ein Buch, das aktualisiert wird, soll hierbei in dem Cache als ungültig deklariert werden (also nicht alle Bücher).

CacheInvalidator

Analog zu oben, wird der CacheInvalidator implementiert.

internal interface IBuchCacheInvalidator : ICacheInvalidator
{
}
internal class BuchCacheInvalidator : CacheInvalidator, IBuchCacheInvalidator
{
    public BuchCacheInvalidator(IBusinessEventForwarder businessEventForwarder)
        : base(businessEventForwarder)
    {
        Subscribe<UpdatedEventService.IBuchUpdatedEventService, UpdatedEventService.RaisedNotification, BuchUpdatedCacheItemKeyResolver>();
    }
}

Die Subscription hier ist notwendig, um Instanz-übergreifend mithilfe des ServiceBus / RabbitMQ den Cache zu aktualisieren.

Cache<T>

Die Implementierung des Caches erfolgt so:

internal interface IBuchCache : ICache<Buch>
{
}
internal class BuchCache : Cache<Buch>, IBuchCache
{
    public BuchCache(IBuchCacheInvalidator buchCacheInvalidator, ISystemStructureCacheScopeBuilder cacheScopeBuilder)
        : base(buchCacheInvalidator, cacheScopeBuilder)
    {
    }
 
    protected override TimeSpan? SlidingExpiration { get { return TimeSpan.FromMinutes(15); } }
} 
Caching-Repository

Caching für Repositoryabfragen wird ähnlich zur Gatewayabfragen umgesetzt (vgl. unten). Eine Caching-Repository wird implementiert. Die Implementierung von der Caching-Repository wird automatsich mit Default Lifestyle registriert. Sowie beim normalen Repository, soll der Interface zur BusinessCases-Projekt und die Implementierung zur Repositories-Projekt hinzugefügt werden.

Die Implementierung der Caching-Repository implementiert die cache-relevanten Methoden der Repository.

Der Caching-Repository bekommt der Cache-Klasse und der normalen Repository per Konstruktor-Injektion. Zu erst wird in Cache gesucht; falls die Daten nicht ins Cache vorhanden sind, wird der normalen Repository aufgerufen, die Ergebnis gecached und dann anschließend zurückgegeben.

internal interface IBuchRepository
{
    Buch QueryById(Guid buchId);
    IEnumerable<Buch> Add(Buch[] buecher);
}
internal interface ICachingBuchRepository
{    
    Buch QueryById(Guid buchId);
}
internal class BuchRepository : IBuchRepository
{
    public BuchTitel[] QueryByTitel(string sessionToken, string titelBegintMit)
    {
         ....
    }
 
    public IEnumerable<Buch> Add(Buch[] buecher)
    {
         ....
    }
}
internal class CachingBuchRepository : ICachingBuchRepository
{
    private readonly IBuchRepository buchRepository;
    private readonly IBuchCache buchCache;
 
    public CachingBuchRepository(IBuchServiceGateway buchServiceGateway, IBuchCache buchCache)
    {
        ...
        this.buchServiceGateway = buchServiceGateway;
        this.buchCache = buchCache;
    }
 
    public Buch QueryById(Guid buchId)
    {
        Buch result = buchCache.GetOrSet(
            buchId.ToString(),
            () =>
            {
                Buch nonCachedBuch = buchRepository.QueryById(buchId);
                Detach(nonCachedBuch); // Objekte, die mit NHibernate verbunden sind, sollten nicht im Cache hinterlegt werden
                return nonCachedBuch;
            });
        return result;
    }
}
Anschluss
public sealed class BuchVerwaltenController : IBuchVerwaltenController
{
    private readonly ICachingBuchRepository cachingBuchRepository;

    public BuchVerwaltenController(ICachingBuchRepository cachingBuchRepository)
    {
        // ...
        this.cachingBuchRepository = cachingBuchRepository;
    }

    public IEnumerable<Buch> QueryById(IEnumerable<Guid> buchIds)
    {
        // ...
        IEnumerable<Buch> queriedBuecher = cachingBuchRepository.QueryById(buchIds);
        return queriedBuecher;
    }
}

Der Windsor Container für den ServiceBusClient ist zu konfigurieren. Hierbei werden die benötigen Klassen von Framework und SB registriert.

public class ServiceHostFactoryConfigurator : WcfContainerConfigurator
{
    // ...

    protected override void OnContainerConfigured(IWindsorContainer windsorContainer)
    {
        if (windsorContainer == null) { throw new ArgumentNullException(nameof(windsorContainer)); }

        windsorContainer.Install(new ServiceBusClientInstaller(IsInsideWcfOperation) { DatabaseSchema = "BIB" });
        // ...

        base.OnContainerConfigured(windsorContainer);
    }
}

Abgrenzung

Es soll kein vollständiger verteilter Cache implementiert werden.

Stärken und Schwächen

Stärkenz
  • Flexibilität
  • Performance
Schwächen
  • Daten werden redundant gehalten
  • Subscription an den ServiceBus/RabbitMQ im Konstruktor
Cookie Consent mit Real Cookie Banner