Entwickeln Sie einen generischen Caching-Service, um die Leistung zu verbessern

Angenommen, ein Mitarbeiter bittet Sie um eine Liste aller Länder der Welt. Da Sie kein Geografieexperte sind, surfen Sie zur Website der Vereinten Nationen, laden die Liste herunter und drucken sie für sie aus. Sie möchte jedoch nur die Liste prüfen; sie nimmt es eigentlich nicht mit. Da Sie als letztes ein weiteres Stück Papier auf Ihrem Schreibtisch benötigen, geben Sie die Liste an den Aktenvernichter weiter.

Einen Tag später fordert ein anderer Mitarbeiter dasselbe an: eine Liste aller Länder der Welt. Sie verfluchen sich, weil Sie die Liste nicht geführt haben, und surfen erneut zur Website der Vereinten Nationen. Bei diesem Besuch der Website stellen Sie fest, dass die UN ihre Länderliste alle sechs Monate aktualisiert. Sie laden die Liste für Ihren Mitarbeiter herunter und drucken sie aus. Er schaut es sich an, danke und verlässt die Liste wieder bei Ihnen. Dieses Mal legen Sie die Liste mit einer Nachricht auf einer angehängten Haftnotiz ab, die Sie daran erinnert, sie nach sechs Monaten zu verwerfen.

Sicher genug, in den nächsten Wochen fordern Ihre Mitarbeiter die Liste immer wieder an. Sie gratulieren sich zur Einreichung des Dokuments, da Sie das Dokument schneller aus dem Aktenschrank extrahieren können als von der Website. Ihr Aktenschrankkonzept setzt sich durch. Bald fängt jeder an, Gegenstände in Ihren Schrank zu legen. Um zu verhindern, dass der Schrank unorganisiert wird, legen Sie Richtlinien für die Verwendung fest. In Ihrer offiziellen Eigenschaft als Aktenschrankmanager weisen Sie Ihre Mitarbeiter an, Etiketten und Haftnotizen auf allen Dokumenten anzubringen, in denen die Dokumente und ihr Entsorgungs- / Ablaufdatum angegeben sind. Die Etiketten helfen Ihren Mitarbeitern, das gesuchte Dokument zu finden, und die Haftnotizen geben Aufschluss darüber, ob die Informationen aktuell sind.

Der Aktenschrank wird so beliebt, dass Sie bald keine neuen Dokumente mehr darin ablegen können. Sie müssen entscheiden, was Sie wegwerfen und was Sie behalten möchten. Obwohl Sie alle abgelaufenen Dokumente wegwerfen, ist der Schrank immer noch mit Papier überfüllt. Wie entscheiden Sie, welche nicht abgelaufenen Dokumente verworfen werden sollen? Verwerfen Sie das älteste Dokument? Sie können die am wenigsten häufig verwendeten oder die am wenigsten kürzlich verwendeten verwerfen. In beiden Fällen benötigen Sie ein Protokoll, in dem aufgeführt ist, wann auf jedes Dokument zugegriffen wurde. Oder Sie könnten anhand einer anderen Determinante entscheiden, welche Dokumente verworfen werden sollen. Die Entscheidung ist rein persönlich.

Um die obige reale Analogie mit der Computerwelt in Beziehung zu setzen, fungiert der Aktenschrank als Cache: ein Hochgeschwindigkeitsspeicher, der gelegentlich gewartet werden muss. Die Dokumente im Cache sind zwischengespeicherte Objekte, die alle den von Ihnen, dem Cache-Manager, festgelegten Standards entsprechen . Das Löschen des Caches wird als Löschen bezeichnet. Da zwischengespeicherte Elemente nach Ablauf einer bestimmten Zeit gelöscht werden, wird der Cache als zeitgesteuerter Cache bezeichnet.

In diesem Artikel erfahren Sie, wie Sie einen zu 100 Prozent reinen Java-Cache erstellen, der einen anonymen Hintergrundthread verwendet, um abgelaufene Elemente zu löschen. Sie werden sehen, wie Sie einen solchen Cache erstellen, während Sie die Kompromisse verstehen, die mit verschiedenen Designs verbunden sind.

Erstellen Sie den Cache

Genug Aktenschrank-Analogien: Gehen wir weiter zu Websites. Website-Server müssen sich auch mit dem Caching befassen. Server erhalten wiederholt Informationsanfragen, die mit anderen Anfragen identisch sind. Für Ihre nächste Aufgabe müssen Sie eine Internetanwendung für eines der weltweit größten Unternehmen erstellen. Nach vier Monaten Entwicklungszeit, einschließlich vieler schlafloser Nächte und viel zu vieler Jolt-Colas, geht die Anwendung mit 1.000 Benutzern in Entwicklungstests. Nach dem Entwicklungstest folgt ein Zertifizierungstest für 5.000 Benutzer und ein anschließender Produktions-Rollout für 20.000 Benutzer. Nachdem Sie jedoch Fehler aufgrund von Speichermangel erhalten haben, während nur 200 Benutzer die Anwendung testen, werden die Entwicklungstests angehalten.

Um die Ursache des Leistungsabfalls zu erkennen, verwenden Sie ein Profilierungsprodukt und stellen fest, dass der Server mehrere Kopien von Datenbanken lädt ResultSet, von denen jede mehrere tausend Datensätze enthält. Die Aufzeichnungen bilden eine Produktliste. Darüber hinaus ist die Produktliste für jeden Benutzer identisch. Die Liste hängt nicht vom Benutzer ab, wie dies der Fall gewesen wäre, wenn die Produktliste aus einer parametrisierten Abfrage resultiert hätte. Sie entscheiden schnell, dass eine Kopie der Liste alle gleichzeitigen Benutzer bedienen kann, und speichern sie im Cache.

Es stellt sich jedoch eine Reihe von Fragen, zu denen folgende Komplexitäten gehören:

  • Was ist, wenn sich die Produktliste ändert? Wie kann der Cache die Listen ablaufen lassen? Woher weiß ich, wie lange die Produktliste im Cache verbleiben soll, bevor sie abläuft?
  • Was ist, wenn zwei unterschiedliche Produktlisten vorhanden sind und sich die beiden Listen in unterschiedlichen Intervallen ändern? Kann ich jede Liste einzeln ablaufen lassen oder müssen alle die gleiche Haltbarkeit haben?
  • Was ist, wenn der Cache leer ist und zwei Anforderer den Cache genau zur gleichen Zeit versuchen? Wenn beide es leer finden, erstellen sie dann ihre eigenen Listen und versuchen dann beide, ihre Kopien in den Cache zu legen?
  • Was ist, wenn Elemente monatelang im Cache liegen, ohne dass darauf zugegriffen werden kann? Werden sie nicht die Erinnerung auffressen?

Um diese Herausforderungen zu bewältigen, müssen Sie einen Software-Caching-Service erstellen.

In der Aktenschrank-Analogie wurde bei der Suche nach Dokumenten immer zuerst der Schrank überprüft. Ihre Software muss dasselbe Verfahren implementieren: Eine Anforderung muss den Caching-Dienst überprüfen, bevor eine neue Liste aus der Datenbank geladen wird. Als Softwareentwickler sind Sie dafür verantwortlich, vor dem Zugriff auf die Datenbank auf den Cache zuzugreifen. Wenn die Produktliste bereits in den Cache geladen wurde, verwenden Sie die zwischengespeicherte Liste, sofern sie nicht abgelaufen ist. Befindet sich die Produktliste nicht im Cache, laden Sie sie aus der Datenbank und zwischenspeichern Sie sie sofort.

Hinweis: Bevor Sie mit den Anforderungen und dem Code des Caching-Dienstes fortfahren, sollten Sie die folgende Seitenleiste "Caching versus Pooling" lesen. Es erklärt das Pooling, ein verwandtes Konzept.

Bedarf

In Übereinstimmung mit guten Entwurfsprinzipien habe ich eine Anforderungsliste für den Caching-Service definiert, die wir in diesem Artikel entwickeln werden:

  1. Jede Java-Anwendung kann auf den Caching-Dienst zugreifen.
  2. Objekte können im Cache abgelegt werden.
  3. Objekte können aus dem Cache extrahiert werden.
  4. Zwischengespeicherte Objekte können selbst bestimmen, wann sie ablaufen, wodurch maximale Flexibilität ermöglicht wird. Caching-Services, bei denen alle Objekte mit derselben Ablaufformel ablaufen, bieten keine optimale Verwendung zwischengespeicherter Objekte. Dieser Ansatz ist in großen Systemen unzureichend, da sich beispielsweise eine Produktliste täglich ändern kann, während sich eine Liste der Filialstandorte möglicherweise nur einmal im Monat ändert.
  5. Ein Hintergrundthread, der mit niedriger Priorität ausgeführt wird, entfernt abgelaufene zwischengespeicherte Objekte.
  6. Der Caching-Dienst kann später durch die Verwendung eines am wenigsten verwendeten (LRU) oder am wenigsten häufig verwendeten (LFU) Spülmechanismus erweitert werden.

Implementierung

Um die Anforderung 1 zu erfüllen, verwenden wir eine zu 100 Prozent reine Java-Umgebung. Durch die Bereitstellung von Public getund setMethoden im Caching-Service erfüllen wir auch die Anforderungen 2 und 3.

Bevor wir mit der Erörterung von Anforderung 4 fortfahren, möchte ich kurz erwähnen, dass wir Anforderung 5 erfüllen, indem wir einen anonymen Thread im Cache-Manager erstellen. Dieser Thread beginnt im statischen Block. Außerdem erfüllen wir Anforderung 6, indem wir die Punkte identifizieren, an denen später Code hinzugefügt wird, um die LRU- und LFU-Algorithmen zu implementieren. Ich werde später in diesem Artikel näher auf diese Anforderungen eingehen.

Nun zurück zu Anforderung 4, wo die Dinge interessant werden. Wenn jedes zwischengespeicherte Objekt selbst bestimmen muss, ob es abgelaufen ist, müssen Sie eine Möglichkeit haben, das Objekt zu fragen, ob es abgelaufen ist. Das bedeutet, dass alle Objekte im Cache bestimmten Regeln entsprechen müssen. Sie erreichen dies in Java, indem Sie eine Schnittstelle implementieren.

Beginnen wir mit den Regeln, die die im Cache platzierten Objekte regeln.

  1. Alle Objekte müssen eine öffentliche Methode namens haben isExpired(), die einen booleschen Wert zurückgibt.
  2. Alle Objekte müssen über eine öffentliche Methode verfügen getIdentifier(), die ein Objekt zurückgibt, das das Objekt von allen anderen im Cache unterscheidet.

Hinweis: Bevor Sie direkt in den Code springen, müssen Sie verstehen, dass Sie einen Cache auf viele Arten implementieren können. Ich habe mehr als ein Dutzend verschiedene Implementierungen gefunden. Enhydra und Caucho bieten hervorragende Ressourcen, die mehrere Cache-Implementierungen enthalten.

Den Schnittstellencode für den Caching-Service dieses Artikels finden Sie in Listing 1.

Listing 1. Cacheable.java

/ ** * Titel: Caching Beschreibung: Diese Schnittstelle definiert die Methoden, die von allen Objekten implementiert werden müssen, die in den Cache gestellt werden möchten. * * Copyright: Copyright (c) 2001 * Firma: JavaWorld * Dateiname: Cacheable.java @author Jonathan Lurie @version 1.0 * / public interface Cacheable {/ * Da alle Objekte ihre eigenen Ablaufzeiten bestimmen müssen, wird der Algorithmus von der abstrahiert Caching-Service, wodurch maximale Flexibilität geboten wird, da jedes Objekt eine andere Ablaufstrategie anwenden kann. * / public boolean isExpired (); / * Diese Methode stellt sicher, dass der Caching-Dienst nicht für die eindeutige Identifizierung von Objekten im Cache verantwortlich ist. * / public Object getIdentifier (); }}

Jedes im Cache platzierte Objekt - Stringbeispielsweise a - muss in ein Objekt eingeschlossen werden, das die CacheableSchnittstelle implementiert . Listing 2 ist ein Beispiel für eine generische Wrapper-Klasse mit dem Namen CachedObject; Es kann jedes Objekt enthalten, das in den Caching-Dienst gestellt werden muss. Beachten Sie, dass diese Wrapper-Klasse die Cacheablein Listing 1 definierte Schnittstelle implementiert .

Listing 2. CachedManagerTestProgram.java

/ ** * Titel: Caching * Beschreibung: Ein generischer Cache-Objekt-Wrapper. Implementiert die zwischenspeicherbare Schnittstelle * verwendet eine TimeToLive-Strategie für den Ablauf von CacheObject. * Copyright: Copyright (c) 2001 * Firma: JavaWorld * Dateiname: CacheManagerTestProgram.java * @author Jonathan Lurie * @version 1.0 * / öffentliche Klasse CachedObject implementiert Cacheable {// ++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++ / * Diese Variable wird verwendet, um festzustellen, ob das Objekt abgelaufen ist. * / private java.util.Date dateofExpiration = null; private Objektkennung = null; / * Dies enthält den realen "Wert". Dies ist das Objekt, das gemeinsam genutzt werden muss. * / public Object object = null; // +++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++ public CachedObject (Objektobjekt, Objekt-ID, Int-MinutenToLive) {this.object = obj; diese.bezeichner = id; // MinutenToLive von 0 bedeutet, dass es auf unbestimmte Zeit weiterlebt. if (MinutenToLive! = 0) {dateofExpiration = new java.util.Date (); java.util.Calendar cal = java.util.Calendar.getInstance (); cal.setTime (dateofExpiration); cal.add (cal.MINUTE, minuteToLive); dateofExpiration = cal.getTime (); }} // +++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++ public boolean isExpired () {// Denken Sie daran, wenn die Minuten zum Leben Null sind, dann lebt es für immer! if (dateofExpiration! = null) {// Ablaufdatum wird verglichen. if (dateofExpiration.before (new java.util.Date ())) {System.out.println ("CachedResultSet.isExpired: Aus Cache abgelaufen! EXPIRE TIME:" + dateofExpiration.toString () + "CURRENT TIME:" + ( new java.util.Date ()). toString ()); return true; } else {System.out.println ("CachedResultSet.isExpired:Nicht aus dem Cache abgelaufen! "); Return false;}} else // Dies bedeutet, dass es für immer lebt! Return false;} // +++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++++ public object getIdentifier () {return identifier;} // ++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++}

Die CachedObjectKlasse macht eine Konstruktormethode verfügbar, die drei Parameter akzeptiert:

public CachedObject (Objekt obj, Objekt-ID, int minuteToLive) 

Die folgende Tabelle beschreibt diese Parameter.

Parameterbeschreibungen des CachedObject-Konstruktors
Name Art Beschreibung
Obj Objekt Das Objekt, das gemeinsam genutzt wird. Es wird als Objekt definiert, um maximale Flexibilität zu ermöglichen.
Id Objekt Identhält eine eindeutige Kennung, die den objParameter von allen anderen im Cache befindlichen Objekten unterscheidet . Der Caching-Service ist nicht dafür verantwortlich, die Eindeutigkeit der Objekte im Cache sicherzustellen.
minutesToLive Int The number of minutes that the obj parameter is valid in the cache. In this implementation, the caching service interprets a value of zero to mean that the object never expires. You might want to change this parameter in the event that you need to expire objects in less than one minute.

The constructor method determines the expiration date of the object in the cache using a time-to-live strategy. As its name implies, time-to-live means that a certain object has a fixed time at the conclusion of which it is considered dead. By adding minutesToLive, the constructor's int parameter, to the current time, an expiration date is calculated. This expiration is assigned to the class variable dateofExpiration.

Jetzt muss die isExpired()Methode einfach feststellen, ob das dateofExpirationvor oder nach dem aktuellen Datum und der aktuellen Uhrzeit liegt. Wenn das Datum vor der aktuellen Uhrzeit liegt und das zwischengespeicherte Objekt als abgelaufen gilt, gibt die isExpired()Methode true zurück. Wenn das Datum nach der aktuellen Uhrzeit liegt, ist das zwischengespeicherte Objekt nicht abgelaufen und isExpired()gibt false zurück. Wenn dateofExpirationnull ist, was bei minutesToLiveNull der Fall wäre , gibt die isExpired()Methode natürlich immer false zurück, was darauf hinweist, dass das zwischengespeicherte Objekt für immer lebt.