Mit der Caching-Technik können Sie skalierbarere Anwendungen erstellen und die Ergebnisse einiger Abfragen in einem schnellen In-Memory-Speicher speichern. Falsch implementiertes Caching kann jedoch den Eindruck des Benutzers von Ihrer Anwendung erheblich beeinträchtigen. Dieser Artikel enthält einige grundlegende Konzepte zum Caching, verschiedene Regeln und Tabus, die ich aus mehreren früheren Projekten gelernt habe.
Verwenden Sie kein Caching.
Ist Ihr Projekt schnell und hat keine Leistungsprobleme?
Vergessen Sie das Caching. Ernsthaft :)
Dies wird die Lesevorgänge aus der Datenbank ohne Vorteile erheblich verkomplizieren.
Es stimmt, Mohamed Said am Anfang dieses Artikels führt einige Berechnungen durch und beweist, dass in einigen Fällen durch die Optimierung der Anwendung für Millisekunden eine Menge Geld in Ihrem AWS-Konto gespart werden kann. Wenn die projizierten Einsparungen für Ihr Projekt mehr als 1,86 US-Dollar betragen, ist Caching möglicherweise eine gute Idee.
Wie funktioniert es
Wenn eine Anwendung einige Daten aus der Datenbank 'post_' . $id
möchte, z. B. die Post-Entität anhand ihrer ID, generiert sie einen eindeutigen Caching-Schlüssel für diesen Fall ( 'post_' . $id
ist durchaus geeignet) und versucht, den Wert anhand dieses Schlüssels im schnellen Schlüsselwertspeicher (memcache, Redis oder eine andere). Wenn der Wert vorhanden ist, wird er von der Anwendung verwendet. Wenn nicht, nimmt es es aus der Datenbank und speichert es mit diesem Schlüssel für die zukünftige Verwendung im Cache.

Es ist keine gute Idee, diesen Wert für immer im Cache zu belassen, da diese Post-Entität aktualisiert werden kann, die Anwendung jedoch immer den alten zwischengespeicherten Wert erhält.
Daher fragen Caching-Funktionen normalerweise, wann dieser Wert gespeichert werden soll.
Nach Ablauf dieser Zeit „vergessen“ Memcache oder Redis dies und die Anwendung übernimmt einen neuen Wert aus der Datenbank.
Ein Beispiel:
public function getPost($id): Post { $key = 'post_' . $id; $post = \Cache::get($key); if($post === null) { $post = Post::findOrFail($id); \Cache::put($key, $post, 900); } return $post; }
Hier habe ich die Post-Entität für 15 Minuten in den Cache gestellt (seit Version 5.8 verwendet Laravel Sekunden in diesem Parameter, bevor es Minuten gab). Die Cache
Fassade verfügt auch über eine praktische remember
für diesen Fall. Dieser Code macht genau das Gleiche wie der vorherige:
public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); }
In der Laravel-Dokumentation finden Sie ein Cache-Kapitel, in dem erläutert wird, wie Sie die erforderlichen Treiber für Ihre Anwendung und die Hauptfunktionen installieren.
Zwischengespeicherte Daten
Alle Standard-Laravel-Treiber speichern Daten als Zeichenfolgen. Wenn Sie aufgefordert werden, eine Instanz des Eloquent-Modells zwischenzuspeichern, wird die Funktion serialize verwendet, um die Zeichenfolge vom Objekt abzurufen. Die Funktion unserialize stellt den Status eines Objekts wieder her, wenn es aus dem Cache abgerufen wird .
Fast alle Daten können zwischengespeichert werden. Zahlen, Zeichenfolgen, Arrays, Objekte (wenn sie korrekt serialisiert werden können, lesen Sie die Funktionsbeschreibungen unter den vorherigen Links).
Eloquente Entitäten und Sammlungen können einfach zwischengespeichert werden und sind die beliebtesten Werte im Laravel-Anwendungscache. Die Verwendung anderer Typen ist jedoch ebenfalls weit verbreitet. Die Cache::increment
Methode ist beliebt für die Implementierung verschiedener Zähler. Atomic Locks sind auch sehr nützlich, wenn Entwickler unter Rennbedingungen kämpfen.
Was soll zwischengespeichert werden?
Die ersten Kandidaten für das Caching sind Anforderungen, die sehr oft ausgeführt werden, aber ihr Ausführungsplan ist nicht der einfachste. Das beste Beispiel sind die Top 5 Artikel auf der Hauptseite oder die neuesten Nachrichten. Das Zwischenspeichern solcher Werte kann die Leistung der Hauptseite erheblich verbessern.
Normalerweise ist das Abrufen von Entitäten anhand der ID mithilfe von Model::find($id)
sehr schnell. Wenn diese Tabelle jedoch stark mit zahlreichen Aktualisierungs-, Einfüge- und Löschabfragen belastet ist, bietet die Reduzierung der Anzahl ausgewählter Abfragen eine gute Frist für die Datenbank. Entitäten mit hasMany
Beziehungen, die jedes Mal geladen werden, sind ebenfalls gute Kandidaten für das Caching. Als ich an einem Projekt mit mehr als 10 Millionen Besuchern pro Tag arbeitete, haben wir fast jede ausgewählte Anfrage zwischengespeichert.
Cache-Ungültigmachung
Der Schlüsselabfall nach einer bestimmten Zeit hilft beim Aktualisieren der Daten im Cache, dies geschieht jedoch nicht sofort. Der Benutzer kann die Daten ändern, aber für einige Zeit wird er weiterhin die alte Version davon in der Anwendung sehen. Der übliche Dialog zu einem meiner vergangenen Projekte:
: , ! : , 15 ( , )...
Dieses Verhalten ist für Benutzer sehr unpraktisch, und die offensichtliche Entscheidung, alte Daten aus dem Cache zu löschen, wenn wir sie aktualisieren, fällt mir schnell ein. Dieser Vorgang wird als Behinderung bezeichnet. Für einfache Schlüssel wie "post_%id%"
ist die "post_%id%"
nicht sehr schwierig.
Beredte Ereignisse können helfen, oder wenn Ihre Anwendung spezielle Ereignisse wie PostPublished
oder UserBanned
, kann dies noch einfacher sein. Beispiel mit eloquenten Ereignissen. Zuerst müssen Sie Ereignisklassen erstellen. Der Einfachheit halber werde ich für sie eine abstrakte Klasse verwenden:
abstract class PostEvent { private $post; public function __construct(Post $post) { $this->post = $post; } public function getPost(): Post { return $this->post; } } final class PostSaved extends PostEvent{} final class PostDeleted extends PostEvent{}
Natürlich muss sich laut PSR-4 jede Klasse in einer eigenen Datei befinden. Richten Sie die Post Eloquent-Klasse ein (mithilfe der Dokumentation ):
class Post extends Model { protected $dispatchesEvents = [ 'saved' => PostSaved::class, 'deleted' => PostDeleted::class, ]; }
Erstellen Sie einen Listener für diese Ereignisse:
class EventServiceProvider extends ServiceProvider { protected $listen = [ PostSaved::class => [ ClearPostCache::class, ], PostDeleted::class => [ ClearPostCache::class, ], ]; } class ClearPostCache { public function handle(PostEvent $event) { \Cache::forget('post_' . $event->getPost()->id); } }
Dieser Code entfernt die zwischengespeicherten Werte nach jeder Aktualisierung oder Löschung von Post-Entitäten. Das Ungültigmachen von Entitätslisten wie Top-5-Artikeln oder aktuellen Nachrichten wird etwas komplizierter. Ich habe drei Strategien gesehen:
Strategie nicht deaktivieren
Berühren Sie diese Werte einfach nicht. Normalerweise bringt dies keine Probleme mit sich. Es ist in Ordnung, dass die neuen Nachrichten etwas später in der Liste der letzteren erscheinen (natürlich, wenn dies kein großes Nachrichtenportal ist). Für einige Projekte ist es jedoch sehr wichtig, dass diese Listen neue Daten enthalten.
Strategie finden und entschärfen
Jedes Mal, wenn Sie eine Publikation aktualisieren, können Sie versuchen, sie in den zwischengespeicherten Listen zu finden. Wenn diese vorhanden ist, löschen Sie diesen zwischengespeicherten Wert.
public function getTopPosts() { return \Cache::remember('top_posts', 900, function() { return Post::()->get(); }); } class CheckAndClearTopPostsCache { public function handle(PostEvent $event) { $updatedPost = $event->getPost(); $posts = \Cache::get('top_posts', []); foreach($posts as $post) { if($updatedPost->id == $post->id) { \Cache::forget('top_posts'); return; } } } }
Es sieht hässlich aus, aber es funktioniert.
Strategie "Geschäfts-ID"
Wenn die Reihenfolge der Elemente in der Liste unwichtig ist, können Sie im Cache nur ID-Einträge speichern. Nach Erhalt der ID können Sie eine Liste der Schlüssel der Form 'post_'.$id
erstellen und alle Werte mit der Cache::many
Methode Cache::many
, die in einer Anforderung viele Werte aus dem Cache abruft (dies wird auch als Multi-Get bezeichnet).
Die Ungültigmachung des Cache wird nicht umsonst als eine der beiden Schwierigkeiten bei der Programmierung bezeichnet und ist in einigen Fällen sehr schwierig.
Beziehungs-Caching
Das Zwischenspeichern von Entitäten mit Beziehungen erfordert eine erhöhte Aufmerksamkeit.
$post = Post::findOrFail($id); foreach($post->comments...)
Dieser Code führt zwei SELECT
Abfragen aus. Entität nach id
und Kommentare nach post_id
. Wir implementieren Caching:
public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); } $post = getPost($id); foreach($post->comments...)
Die erste Anfrage wurde zwischengespeichert und die zweite nicht. Wenn der Cache-Treiber Post in den Cache schreibt, werden noch keine comments
geladen. Wenn wir sie auch zwischenspeichern möchten, müssen wir sie manuell laden:
public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { $post = Post::findOrFail($id); $post->load('comments'); return $post; }); }
Beide Anforderungen werden jetzt zwischengespeichert, aber wir müssen die Werte von 'post_'.$id
ungültig machen 'post_'.$id
jedes Mal, wenn ein Kommentar hinzugefügt wird. Es ist nicht sehr effizient, daher ist es besser, den Kommentar-Cache separat zu speichern:
public function getPostComments(Post $post) { return \Cache::remember('post_comments_' . $post->id, 900, function() use ($post) { return $post->comments; }); } $post = getPost($id); $comments = getPostComments($post); foreach($comments...)
Manchmal sind Essenz und Einstellung stark miteinander verbunden und werden immer zusammen verwendet (Reihenfolge mit Details, Veröffentlichung mit Übersetzung in die gewünschte Sprache). In diesem Fall ist das Speichern in einem Cache ganz normal.
Eine einzige Wahrheitsquelle für Cache-Schlüssel
Wenn das Projekt eine Ungültigmachung implementiert, werden Cache-Schlüssel an mindestens zwei Stellen generiert: zum Aufrufen von Cache::get
/ Cache::remember
und zum Aufrufen von Cache::forget
. Ich bin bereits auf Situationen gestoßen, in denen dieser Schlüssel an einem Ort geändert wurde, aber nicht an einem anderen, und die Behinderung brach. Der übliche Rat für solche Fälle sind Konstanten, aber Cache-Schlüssel werden dynamisch generiert, daher verwende ich spezielle Klassen, die Schlüssel generieren:
final class CacheKeys { public static function postById($postId): string { return 'post_' . $postId; } public static function postComments($postId): string { return 'post_comments' . $postId; } } \Cache::remember(CacheKeys::postById($id), 900, function() use ($id) { $post = Post::findOrFail($id); });
Schlüssellebensdauern können zur besseren Lesbarkeit auch in Konstanten dargestellt werden. Diese 900 oder 15 * 60 erhöhen die kognitive Belastung beim Lesen von Code.
Verwenden Sie den Cache nicht für Schreibvorgänge
Bei der Implementierung von Schreibvorgängen, z. B. beim Ändern des Titels oder des Textes einer Veröffentlichung, ist es verlockend, die getPost
geschriebene Methode getPost
verwenden:
$post = getPost($id); $post->title = $newTitle; $post->save();
Bitte nicht. Der Wert im Cache ist möglicherweise veraltet, auch wenn die Ungültigmachung korrekt durchgeführt wurde. Eine kleine Racebedingung und Veröffentlichung verlieren die von einem anderen Benutzer vorgenommenen Änderungen. Optimistische Sperren helfen zumindest dabei, Änderungen nicht zu verlieren, aber die Anzahl fehlerhafter Anforderungen kann erheblich zunehmen.
Die beste Lösung besteht darin, für Lese- und Schreibvorgänge eine völlig andere Entitätsauswahllogik zu verwenden (Hallo, CQRS). Bei Schreibvorgängen müssen Sie immer den neuesten Wert aus der Datenbank auswählen. Und vergessen Sie nicht die Sperren (optimistisch oder pessimistisch) für wichtige Daten.
Ich denke, das reicht für einen einleitenden Artikel. Caching ist ein sehr komplexes und langwieriges Thema mit Fallen für Entwickler, aber der Leistungsgewinn überwiegt manchmal alle Schwierigkeiten.