Autor: Iqbal Khan
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.
Domänenobjekte, Zwischenspeicherung von Domänenobjekten.
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.
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.
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 (); }
}
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.
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.
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:
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.
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.
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.
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.
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.