Domänenobjekt-Caching-Muster für .NET

Autor: Iqbal Khan

Abstrakt

Durch Caching wird die Anwendungsleistung erheblich verbessert, da dadurch kostspielige Zugriffe auf die Datenbank reduziert werden. Wenn Sie jedoch Caching in Ihrer Anwendung verwenden möchten, müssen Sie entscheiden, was zwischengespeichert werden soll und wo Ihr Caching-Code abgelegt werden soll. Die Antwort ist einfach. Cachen Sie Ihre Domänenobjekte und fügen Sie Caching-Code in Ihre Persistenzklassen ein. Domänenobjekte sind für jede Anwendung von zentraler Bedeutung und stellen deren Kerndaten und Geschäftsvalidierungsregeln dar. Und obwohl Domänenobjekte möglicherweise einige schreibgeschützte Daten enthalten, handelt es sich bei den meisten Daten um Transaktionsdaten, die sich häufig ändern. Daher können Sie Domänenobjekte nicht einfach als „globale Variablen“ für die gesamte Anwendung beibehalten, da sich die Daten in der Datenbank ändern und Ihre Domänenobjekte veraltet sind, was zu Problemen mit der Datenintegrität führt. Hierfür müssen Sie eine geeignete Caching-Lösung verwenden. Und Ihre Optionen sind ASP.NET Cache, Caching Application Block in der Microsoft Enterprise Library oder eine kommerzielle Lösung wie NCache für Alachisoft . Persönlich würde ich von der Verwendung von ASP.NET Cache abraten, da Sie dadurch gezwungen werden, von der Präsentationsebene (ASP.NET-Seiten) zwischenzuspeichern, was schlecht ist. Der beste Ort zum Einbetten von Caching in Ihre Anwendung sind die Persistenzklassen Ihrer Domänenobjekte. In diesem Artikel erweitere ich ein früheres Entwurfsmuster namens Domain Objects Persistence Pattern für .NET, das ich geschrieben habe. Ich zeige Ihnen, wie Sie intelligentes Caching in Ihre Anwendung integrieren können, um deren Leistung zu steigern, und welche Überlegungen Sie dabei beachten sollten.

Das Domain Objects Caching Pattern versucht, eine Lösung für das Caching von Domänenobjekten bereitzustellen. Die Domänenobjekte in diesem Muster wissen nicht, welche Klassen sie beibehalten und ob sie zwischengespeichert werden oder nicht, da die Abhängigkeit nur in eine Richtung besteht. Dadurch wird der Entwurf von Domänenobjekten wesentlich einfacher und verständlicher. Es verbirgt außerdem den Caching- und Persistenzcode vor anderen Subsystemen, die die Domänenobjekte verwenden. Dies funktioniert auch in verteilten Systemen, in denen nur die Domänenobjekte herumgereicht werden.

Geltungsbereich

Domänenobjekte, Zwischenspeicherung von Domänenobjekten.

Problem Definition

Domänenobjekte bilden das Rückgrat jeder Anwendung. Sie erfassen das Datenmodell aus der Datenbank und auch die Geschäftsregeln, die für diese Daten gelten. Es ist sehr typisch, dass die meisten Subsysteme einer Anwendung auf diese gemeinsamen Domänenobjekte angewiesen sind. Und normalerweise verbringen Anwendungen die meiste Zeit damit, diese Domänenobjekte entweder zu laden oder in der Datenbank zu speichern. Die tatsächliche „Verarbeitungszeit“ dieser Objekte ist sehr gering, insbesondere für N-Tier-Anwendungen, bei denen jede „Benutzeranfrage“ sehr kurz ist.

Das bedeutet, dass die Leistung der Anwendung stark davon abhängt, wie schnell diese Domänenobjekte der Anwendung zur Verfügung gestellt werden können. Wenn die Anwendung zahlreiche Datenbankfahrten durchführen muss, ist die Leistung normalerweise schlecht. Wenn die Anwendung diese Objekte jedoch in der Nähe zwischenspeichert, verbessert sich die Leistung erheblich.

Gleichzeitig ist es sehr wichtig, dass der Code für das Caching von Domänenobjekten an einem so zentralen Ort gespeichert wird, dass die Anwendung automatisch mit dem Cache interagiert, unabhängig davon, wer die Domänenobjekte lädt oder speichert. Darüber hinaus müssen wir den Caching-Code vor dem Rest der Anwendung verbergen, damit wir ihn bei Bedarf problemlos entfernen können.

Lösung

Wie oben beschrieben ist die Lösung eine Erweiterung eines vorhandenen Entwurfsmusters namens Domain Objects Persistence Pattern für .NET. Dieses Muster erreicht bereits das Ziel, Domänenobjekte vom Persistenzcode und auch vom Rest der Anwendung zu trennen. Diese doppelte Entkopplung bietet große Flexibilität bei der Gestaltung. Die Domänenobjekte und der Rest der Anwendung bleiben völlig unabhängig davon, ob die Daten aus einer relationalen Datenbank oder einer anderen Quelle (z. B. XML, Flatfiles oder Active Directory/LDAP) stammen.

Daher ist der beste Ort zum Einbetten von Caching-Code die Persistenzklassen. Dadurch wird sichergestellt, dass unabhängig davon, welcher Teil der Anwendung den Lade- oder Speicheraufruf für Domänenobjekte ausgibt, zuerst ordnungsgemäß auf das Caching verwiesen wird. Dadurch wird auch der gesamte Caching-Code vor dem Rest der Anwendung ausgeblendet und Sie können ihn bei Bedarf durch etwas anderes ersetzen.

Domänen- und Persistenzklassen

In diesem Beispiel betrachten wir eine Employee-Klasse aus der Northwind-Datenbank, die der Tabelle „Employees“ in der Datenbank zugeordnet ist.

// Domain object "Employee" that holds your data 
public class Employee {
    // Some of the private data members
    // ... 
    public Employee () { }

    // Properties for Employee object 
    public String EmployeeId {
        get { return _employeeId; }
        set { _employeeId = value; }
    }

    public String Title {
        get { return _title; }
        set { _title = value; }
    }

    public ArrayList Subordinates {
        get { return _subordinates; }
        set { _subordinates = value; }
    }
}
// Interface for the Employee persistence
public interface IEmployeeFactory {
    // Standard transactional methods for single-row operations 
    void Load (Employee emp);
    void Insert (Employee emp);
    void Update (Employee emp);
    void Delete (Employee emp);

    // Load the related Employees (Subordinates) for this Employee 
    void LoadSubordinates (Employee emp);

    // Query method to return a collection of Employee objects 
    ArrayList FindByTitle (String title);
}

// Implementation of Employee persistence 
public class EmployeeFactory : IEmployeeFactory {
    // all methods described in interface above are implemented here 
}

// A FactoryProvider to hide persistence implementation 
public class FactoryProvider {
    // To abstract away the actual factory implementation 
    public static IEmployeeFactory GetEmployeeFactory () { return new EmployeeFactory (); }
}

Beispielanwendung

Unten finden Sie ein Beispiel dafür, wie eine Clientanwendung diesen Code verwenden wird.

public class NorthwindApp
    {
        static void Main (string[] args) {
        Employee emp = new Employee();
        IEmployeeFactory iEmpFactory = FactoryProvider.GetEmployeeFactory();

        // Let's load an employee from Northwind database. 
        emp.EmployeeId = 2;
        iEmpFactory.load(emp);

        // Pass on the Employee object 
        HandleEmployee(emp);
        HandleSubordinates(emp.Subordinates);

        // empList is a collection of Employee objects  
        ArrayList empList = iEmpFactory.FindByTitle("Manager");
        }
    }

Der obige Code zeigt Ihnen die Gesamtstruktur Ihrer Klassen für den Umgang mit der Persistenz und dem Caching von Domänenobjekten. Wie Sie sehen, gibt es eine klare Trennung zwischen der Domänen- und der Persistenzklasse. Und es gibt eine zusätzliche FactoryProvider-Klasse, mit der Sie die Persistenzimplementierung vor dem Rest der Anwendung verbergen können. Die Domänenobjekte (in diesem Fall „Employee“) bewegen sich jedoch in der gesamten Anwendung.

Cache-Schlüssel erstellen

Die meisten Cache-Systeme stellen Ihnen einen stringbasierten Schlüssel zur Verfügung. Dabei bestehen die Daten, die Sie zwischenspeichern, aus verschiedenen Klassen („Kunden“, „Mitarbeiter“, „Bestellungen“ etc.). In dieser Situation kann es zu einem Konflikt zwischen einer EmployeeId von 1000 und einer OrderId von 1000 kommen, wenn Ihre Schlüssel keine Typinformationen enthalten. Daher müssen Sie auch einige Typinformationen als Teil des Schlüssels speichern. Nachfolgend finden Sie einige empfohlene Schlüsselstrukturen. Sie können Ihre eigenen Rezepte nach den gleichen Grundsätzen zusammenstellen.

  1. Schlüssel für einzelne Objekte: Wenn Sie nur einzelne Objekte aufbewahren, können Sie Ihre Schlüssel wie folgt zusammenstellen:
    1. „Kunden:PK:1000“. Das bedeutet, dass das Objekt des Kunden einen Primärschlüssel von 1000 hat.
  2. Schlüssel für verwandte Objekte: Für jedes einzelne Objekt möchten Sie möglicherweise auch verwandte Objekte behalten, damit Sie sie leicht finden können. Hier sind Schlüssel dafür:
    1. „Kunden:PK:1000:REL:Bestellungen“. Dies bedeutet eine Auftragssammlung für den Kunden mit dem Primärschlüssel 1000
  3. Schlüssel für Abfrageergebnisse: Manchmal führen Sie Abfragen aus, die eine Sammlung von Objekten zurückgeben. Und diese Abfragen können auch jedes Mal unterschiedliche Laufzeitparameter erfordern. Sie möchten diese Abfrageergebnisse speichern, damit Sie die Abfrage beim nächsten Mal nicht erneut ausführen müssen. Hier sind die Schlüssel dafür. Bitte beachten Sie, dass diese Schlüssel auch Laufzeitparameterwerte umfassen:
    1. „Mitarbeiter:QRY:FindByTitleAndAge:Manager:40“. Dies stellt eine Abfrage in der Klasse „Employees“ mit dem Namen „FindByTitleAndAge“ dar, die zwei Laufzeitparameter benötigt. Der erste Parameter ist „Titel“ und der zweite ist „Alter“. Und ihre Laufzeitparameterwerte werden angegeben.

Caching bei Transaktionsoperationen

Die meisten Transaktionsdaten enthalten einzeilige Vorgänge (Laden, Einfügen, Aktualisieren und Löschen). Diese Methoden basieren alle auf Primärschlüsselwerten des Objekts und sind der ideale Ausgangspunkt für die Bereitstellung von Caching-Code. So gehen Sie mit den einzelnen Methoden um:

  1. Lademethode: Überprüfen Sie zunächst den Cache. Wenn Daten gefunden werden, holen Sie sie von dort ab. Andernfalls laden Sie es aus der Datenbank und legen Sie es dann im Cache ab.
  2. Einfügemethode: Nachdem Sie eine Zeile erfolgreich zur Datenbank hinzugefügt haben, fügen Sie ihr Objekt ebenfalls zum Cache hinzu.
  3. Aktualisierungsmethode: Nachdem Sie eine Zeile in der Datenbank erfolgreich aktualisiert haben, aktualisieren Sie auch deren Objekt im Cache.
  4. Löschmethode: Nachdem Sie eine Zeile erfolgreich aus der Datenbank entfernt haben, entfernen Sie auch deren Objekt aus dem Cache.

Nachfolgend finden Sie ein Beispiel für eine Load-Methode mit integrierter Caching-Logik. Denken Sie daran, dass Sie nur ein einzelnes Objekt (einzelne Zeile) aus der Datenbank laden.

// Check the cache before going to the database 
void Load(Employee emp)
    { 
        try{
            // Construct a cache-key to lookup in the cache first
            // The cache-key for the object will be like this: Employees:PK:1000stringobjKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());

            objectobj = CacheUtil.Load(objKey);
            if(obj == null)
            {
                // item not found in the cache. Load from database and then store in the cache_LoadFromDb(emp);

                // For simplicity, let's assume this object does not depend on anything elseArrayListdependencyKeys = null;
                CacheItemRemovedCallbackonRemoved = null;

                CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );

                // Now, load all its related subordinatesLoadSubordinates(emp);
            }
            else {
                    emp.Copy((Employee)obj);
                }
        }
        catch(Exception e)
        {
            // Handle exceptions here
        }
    }

Bitte beachten Sie hier einige Dinge.

  1. RemovedItemCallback: Dies ist ein Delegat, der es Ihrer Anwendung ermöglicht, asynchron benachrichtigt zu werden, wenn das angegebene Element aus dem Cache entfernt wird.
  2. Ablauf: Sie können eine absolute Zeit oder einen Leerlaufzeitablauf angeben. Obwohl wir oben kein Ablaufdatum angegeben haben, hätten Sie zwei Arten von Ablaufdatum angeben können. Das eine ist ein Ablauf zu einer festen Zeit (z. B. in 10 Minuten) und das zweite ist ein Ablauf in Leerlaufzeit (z. B. wenn das Element 2 Minuten lang inaktiv ist).

Caching-Beziehungen

Domänenobjekte stellen normalerweise relationale Daten dar, die aus einer relationalen Datenbank stammen. Wenn Sie sie zwischenspeichern, müssen Sie daher ihre Beziehungen berücksichtigen und auch die zugehörigen Objekte zwischenspeichern. Außerdem müssen Sie eine „Abhängigkeit“ zwischen dem Objekt und allen zugehörigen Objekten herstellen. Der Grund dafür ist, dass Sie beim Entfernen des Objekts aus dem Cache auch alle zugehörigen Objekte entfernen sollten, damit keine Probleme mit der Datenintegrität auftreten. Nachfolgend finden Sie ein Codebeispiel für die Angabe von Beziehungen im Cache.

// LoadSubordinates method 
void LoadSubordinates (Employee emp) {
    try {
        // Construct a cache-key to lookup related items in the cache first
        // The cache-key for related collection will be like
        this : Employees : PK : 1000 : REL : Subordinates
        string relKey = CacheUtil.GetRelationKey ("Employees",
            "Subordinates", emp.EmployeeId.ToString ());
        string employeeKey = CacheUtil.GetObjectKey ("Employee",
            emp.EmployeeId.ToString ());

        object obj = CacheUtil.Load (relKey);
        if (obj == null) {
            // Subordinates not found in the cache. Load from
            database and then store in the cache
            _LoadSubordinatesFromDb (emp);

            ArrayList subordList = emp.Subordinates;

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also but
                with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            // Count + 1 is so we can also put a dependency on
            the Supervisor
            ArrayList dependencyKeys = new ArrayList (subordList.Count + 1);
            for (int index = 0; index, subordList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    subordList[index].EmployeeId.ToString ());
                CacheUtil.Store (objKey, subordList[index], null,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }
            dependencyKeys[subordList.Count] = employeeKey;
            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (relKey, subordinateList,
                dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default,
                onRemoved);
        } else {
            // Subordinates already in the cache. Let's get them
            emp.Subordinates = (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

Im obigen Beispiel werden Sie feststellen, dass eine Sammlung von der Datenbank zurückgegeben wird und jedes Objekt innerhalb der Sammlung einzeln im Cache gespeichert wird. Anschließend wird die Sammlung als einzelnes Element zwischengespeichert, jedoch mit einer Cache-Abhängigkeit von allen einzelnen Objekten in der Sammlung. Das heißt, wenn einzelne Objekte im Cache aktualisiert oder entfernt werden, wird die Sammlung automatisch vom Cache entfernt. Dadurch können Sie die Datenintegrität beim Caching von Sammlungen aufrechterhalten.

Sie werden im obigen Beispiel auch feststellen, dass die Sammlung eine Cache-Abhängigkeit vom „Primärobjekt“ aufweist, dessen zugehörige Objekte die Sammlung enthält. Diese Abhängigkeit bedeutet auch, dass die Sammlung entfernt wird, wenn das primäre Objekt entfernt oder im Cache aktualisiert wird, um die Datenintegrität aufrechtzuerhalten.

Caching in Abfragemethoden

Eine Abfragemethode gibt eine Sammlung von Objekten basierend auf den darin angegebenen Suchkriterien zurück. Es kann Laufzeitparameter annehmen oder auch nicht. In unserem Beispiel haben wir einen FindByTitle, der „title“ als Parameter akzeptiert. Unten finden Sie ein Beispiel dafür, wie Caching in eine Abfragemethode eingebettet ist.

// Query method to return a collection 
ArrayList FindByTitle (String title) {
    try {
        // Construct a cache-key to lookup items in the cache first
        // The cache-key for the query will be like this:
        Employees : PK : 1000 : QRY : FindByTitle : Manager
        string queryKey = CacheUtil.GetQueryKey ("Employees",
            "Query", title);

        object obj = CacheUtil.Load (queryKey);
        if (obj == null) {
            // No items found in the cache. Load from database
            and then store in the cache
            ArrayList empList = _FindByTitleFromDb (title);

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also
                but with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            ArrayList dependencyKeys = new ArrayList (empList.Count);
            for (int index = 0; index, empList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    empList[index].EmployeeId.ToString ());

                CacheUtil.Store (objKey, empList[index], null, Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }

            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (queryKey, empList, dependencyKeys,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved);
        } else {
            // Query results already in the cache. Let's get them 
            return (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

Im obigen Beispiel werden Sie genau wie bei der Beziehungsmethode feststellen, dass eine Sammlung aus der Datenbank zurückgegeben wird und jedes Objekt innerhalb der Sammlung einzeln im Cache gespeichert wird. Anschließend wird die Sammlung als einzelnes Element zwischengespeichert, jedoch mit einer Cache-Abhängigkeit von allen einzelnen Objekten in der Sammlung. Das heißt, wenn einzelne Objekte im Cache aktualisiert oder entfernt werden, wird die Sammlung automatisch vom Cache entfernt. Dadurch können Sie die Datenintegrität beim Caching von Sammlungen aufrechterhalten.

Anwendungen in Serverfarmen

Das obige Muster funktioniert sowohl für Einzelserver- als auch für Serverfarm-Bereitstellungsumgebungen. Das Einzige, was sich ändern muss, ist die zugrunde liegende Caching-Lösung. Die meisten Caching-Lösungen sind für Einzelserverumgebungen gedacht (z. B. ASP.NET Cache und Caching Application Block). Es gibt jedoch einige kommerzielle Lösungen wie Alachisoft NCache (http: // www.alachisoft.com), die Ihnen einen verteilten Cache bereitstellen, der in einer Serverfarmkonfiguration funktioniert. Auf diese Weise kann Ihre Anwendung einen Cache von jedem Server in der Farm verwenden und alle Cache-Aktualisierungen werden sofort an die gesamte Serverfarm weitergegeben.

Zusammenfassung

Mithilfe des Domain Objects Caching Pattern haben wir gezeigt, wie Sie Caching-Code in Ihre Persistenzklassen einbetten sollten. Und wir haben die am häufigsten verwendeten Situationen von Last, Abfragen und Beziehungen im Hinblick auf das Caching behandelt. Dies sollte Ihnen einen guten Ausgangspunkt geben, um zu bestimmen, wie Sie Caching in Ihrer Anwendung verwenden sollten.


Autor: Iqbal Khan arbeitet für Alachisoft, ein führendes Softwareunternehmen, das Lösungen für verteiltes .NET- und Java-Caching, O/R-Mapping und SharePoint-Speicheroptimierung anbietet. Sie erreichen ihn unter iqbal@alachisoft.com €XNUMX.

© Copyright Alachisoft 2002 - Alle Rechte vorbehalten NCache ist eine eingetragene Marke der Diyatech Corp.