Steigerung der Webanwendungen mit Redis Cache
Emily Parker
Product Engineer · Leapcell

Einführung in effizienten Datenzugriff
In der schnelllebigen Welt der Webanwendungen ist die Benutzererfahrung entscheidend. Langsame Oberflächen und langsame Seitenladezeiten können schnell zu Benutzerfrustration und -abbruch führen. Ein häufiger Flaschenhals bei der Leistung von Webanwendungen ist die Datenbank – der Abruf von Daten beinhaltet oft Festplatten-I/O, Netzwerklatenz und komplexe Abfrageausführung, insbesondere wenn die Anwendung skaliert. Um dem entgegenzuwirken, werden clevere Caching-Strategien unverzichtbar. Dieser Artikel befasst sich damit, wie Redis, ein leistungsstarker In-Memory-Datenspeicher, genutzt werden kann, um den Datenzugriff in Webanwendungen erheblich zu beschleunigen. Wir werden uns speziell zwei beliebte Caching-Muster ansehen: Cache-Aside und Read-Through, ihre Mechanismen, Implementierungen und wie sie zu einer reaktionsschnelleren und skalierbareren Anwendung beitragen.
Kernkonzepte des Caching
Bevor wir uns den Mustern zuwenden, wollen wir ein gemeinsames Verständnis einiger grundlegender Konzepte entwickeln, die dem Caching mit Redis zugrunde liegen.
- Cache: Ein temporärer Speicherbereich, der häufig abgerufene Daten enthält. Sein Zweck ist es, Datenanfragen schneller zu bedienen als der Abruf aus der primären Datenquelle (z. B. einer relationalen Datenbank).
- Redis: Ein Open-Source-Datenspeicher für Datenstrukturen im Arbeitsspeicher, der als Datenbank, Cache und Message Broker verwendet wird. Seine blitzschnelle Leistung ist auf seine In-Memory-Natur und effiziente Datenstrukturen zurückzuführen.
- Cache Hit: Tritt auf, wenn die angeforderten Daten im Cache gefunden werden. Dies ist das gewünschte Ergebnis, da es bedeutet, dass die Daten schnell bedient werden.
- Cache Miss: Tritt auf, wenn die angeforderten Daten nicht im Cache gefunden werden. In diesem Szenario muss die Anwendung die Daten aus der primären Datenquelle abrufen.
- Time-To-Live (TTL): Ein Mechanismus zum automatischen Ablauf von Daten im Cache nach einer bestimmten Dauer. Dies ist entscheidend, um die Frische der Daten zu gewährleisten und die Cache-Größe zu verwalten.
- Eviction Policy: Wenn der Cache seine Kapazität erreicht hat, bestimmt eine Eviction Policy, welche Datenelemente entfernt werden sollen, um Platz für neue zu schaffen (z. B. Least Recently Used - LRU, Least Frequently Used - LFU).
Das Cache-Aside-Muster erklärt
Das Cache-Aside-Muster (auch bekannt als „Lazy Loading“) ist eine der häufigsten und unkompliziertesten Caching-Strategien. Bei diesem Muster ist die Anwendung für die Verwaltung sowohl des Caches als auch des primären Datenspeichers verantwortlich. Der Cache befindet sich neben der Hauptlogik für den Datenzugriff der Anwendung.
So funktioniert Cache-Aside
- Leseanfrage: Wenn die Anwendung Daten benötigt, prüft sie zuerst den Cache.
- Cache Hit: Wenn die Daten im Cache gefunden werden (Cache Hit), werden sie sofort an die Anwendung zurückgegeben.
- Cache Miss: Wenn die Daten nicht im Cache gefunden werden (Cache Miss), ruft die Anwendung die Daten anschließend aus der primären Datenbank ab.
- Cache befüllen: Nach dem Abrufen der Daten aus der Datenbank speichert die Anwendung eine Kopie davon im Cache für zukünftige Anfragen, oft mit einer TTL.
- Daten zurückgeben: Schließlich werden die Daten aus der Datenbank (und jetzt auch aus dem Cache) an die Anwendung zurückgegeben.
- Schreibvorgang: Wenn Daten in der primären Datenbank aktualisiert oder gelöscht werden, muss die Anwendung den entsprechenden Eintrag im Cache explizit ungültig machen oder aktualisieren, um die Datenkonsistenz zu wahren.
Implementierung von Cache-Aside mit Python und Redis
Wir veranschaulichen Cache-Aside mit einer einfachen Python-Flask-Anwendung, die Benutzerdaten abruft. Wir verwenden redis-py
für die Redis-Interaktion.
import redis import json from flask import Flask, jsonify app = Flask(__name__) # Verbindung zu Redis herstellen redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) # Eine Datenbank simulieren def fetch_user_from_db(user_id): print(f"Abrufen von Benutzer {user_id} aus der Datenbank...") # In einer echten App wäre dies eine Datenbankabfrage users_data = { "1": {"id": "1", "name": "Alice Johnson", "email": "alice@example.com"}, "2": {"id": "2", "name": "Bob Williams", "email": "bob@example.com"}, "3": {"id": "3", "name": "Charlie Brown", "email": "charlie@example.com"}, } return users_data.get(str(user_id)) @app.route('/user/<int:user_id>') def get_user(user_id): cache_key = f"user:{user_id}" # 1. Cache prüfen cached_user = redis_client.get(cache_key) if cached_user: print(f"Cache Hit für Benutzer {user_id}") return jsonify(json.loads(cached_user)) # 2. Cache Miss, aus DB abrufen user_data = fetch_user_from_db(user_id) if user_data: # 3. Cache mit TTL (z. B. 60 Sekunden) befüllen redis_client.setex(cache_key, 60, json.dumps(user_data)) print(f"Benutzer {user_id} aus DB abgerufen und gecached") return jsonify(user_data) else: return jsonify({"message": f"Benutzer {user_id} nicht gefunden"}), 404 @app.route('/user/<int:user_id>/update', methods=['POST']) def update_user(user_id): # Aktualisierung des Benutzers in der DB simulieren print(f"Aktualisiere Benutzer {user_id} in der Datenbank...") # Erfolg der DB-Aktualisierung annehmen # Cache-Eintrag nach DB-Aktualisierung ungültig machen cache_key = f"user:{user_id}" redis_client.delete(cache_key) print(f"Cache-Eintrag für Benutzer {user_id} ungültig gemacht") return jsonify({"message": f"Benutzer {user_id} aktualisiert und Cache ungültig gemacht"}), 200 if __name__ == '__main__': app.run(debug=True)
In diesem Beispiel versucht die Funktion get_user
zuerst, Benutzerdaten aus Redis abzurufen. Wenn sie nicht gefunden werden, ruft sie fetch_user_from_db
auf, speichert das Ergebnis mit einer TTL von 60 Sekunden in Redis und gibt es dann zurück. Die Funktion update_user
demonstriert die Cache-Ungültigmachung nach einem Schreibvorgang.
Wann Cache-Aside verwenden
- Leseintensive Workloads: Ideal für Szenarien, in denen Daten viel häufiger gelesen als geschrieben werden.
- Einfache Caching-Logik: Wenn Sie eine granulare Kontrolle darüber wünschen, was und wann gecached wird.
- Anforderungen an die Datenaktualität: Cache-Aside ermöglicht eine explizite Ungültigmachung, was es einfacher macht, nach Schreibvorgängen frische Daten zu gewährleisten.
Das Read-Through-Muster enthüllt
Das Read-Through-Muster steht im Gegensatz zu Cache-Aside und zentralisiert die Caching-Logik innerhalb des Caches selbst. Die Anwendung interagiert nur mit dem Cache, und der Cache ist für den Abruf von Daten aus dem zugrunde liegenden Datenspeicher verantwortlich, falls diese nicht vorhanden sind. Dies beinhaltet typischerweise eine Caching-Bibliothek oder einen Dienst, der die Datenquelle abstrahiert.
So funktioniert Read-Through
- Leseanfrage: Die Anwendung fordert Daten vom Cache an.
- Cache Hit: Wenn die Daten im Cache gefunden werden, werden sie an die Anwendung zurückgegeben.
- Cache Miss: Wenn die Daten nicht gefunden werden, ist der Cache selbst (oder eine damit konfigurierte Komponente) verantwortlich für:
- Abrufen der Daten aus der primären Datenquelle.
- Speichern der abgerufenen Daten im Cache.
- Zurückgeben der Daten an die Anwendung.
- Schreibvorgang: Wenn Daten aktualisiert oder gelöscht werden müssen, aktualisiert die Anwendung typischerweise direkt die primäre Datenquelle. Der Cache muss möglicherweise explizit ungültig gemacht oder aktualisiert werden, ähnlich wie bei Cache-Aside, abhängig von der Architektur des Read-Through-Anbieters. Der Lesezugriff ist jedoch, wo Read-Through durch die Vereinfachung der Anwendungslogik glänzt.
Implementierung von Read-Through (Konzeptionell und Beispiel)
Reines Read-Through erfordert oft eine spezialisierte Caching-Schicht oder ein Framework, das Caching und Datenzugriff integriert. Redis selbst bietet kein „Read-Through“-Verfahren ohne anwendungsseitige Logik. Wir können jedoch den Geist von Read-Through simulieren, indem wir die Cache-Aside-Logik in einem dedizierten Dienst oder einer Wrapper
-Klasse um Redis kapseln.
Lassen Sie uns unser vorheriges Beispiel überarbeiten, um einen stärker „Read-Through-ähnlichen“ Ansatz zu demonstrieren, indem wir die Cache-Logik in einen Dienst abstrahieren.
import redis import json from flask import Flask, jsonify app = Flask(__name__) redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) # Eine Funktion zum Abrufen von Daten aus der Quelle simulieren def get_user_from_source(user_id): print(f"--- Abrufen von Benutzer {user_id} aus der primären Quelle ---") users_data = { "1": {"id": "1", "name": "Alice Johnson", "email": "alice@example.com"}, "2": {"id": "2", "name": "Bob Williams", "email": "bob@example.com"}, "3": {"id": "3", "name": "Charlie Brown", "email": "charlie@example.com"}, } return users_data.get(str(user_id)) class UserService: def __init__(self, cache_client, data_source_fetch_func): self.cache = cache_client self.data_source_fetch_func = data_source_fetch_func def get_user(self, user_id, cache_ttl=60): cache_key = f"user:{user_id}" # Cache prüfen cached_data = self.cache.get(cache_key) if cached_data: print(f"Cache Hit für Benutzer {user_id} (über Read-Through-Dienst)") return json.loads(cached_data) # Cache Miss, aus primärer Quelle abrufen user_data = self.data_source_fetch_func(user_id) if user_data: # Im Cache speichern self.cache.setex(cache_key, cache_ttl, json.dumps(user_data)) print(f"Benutzer {user_id} aus Quelle abgerufen und gecached (über Read-Through-Dienst)") return user_data return None def update_user(self, user_id, new_data): # Aktualisierung in der DB simulieren print(f"--- Aktualisiere Benutzer {user_id} in der primären Quelle ---") # In einer echten App mit Ihrer ORM/DB-Client integrieren # Cache ungültig machen cache_key = f"user:{user_id}" self.cache.delete(cache_key) print(f"Cache-Eintrag für Benutzer {user_id} ungültig gemacht (über Read-Through-Dienst)") return {"message": f"Benutzer {user_id} aktualisiert und Cache ungültig gemacht"} user_service = UserService(redis_client, get_user_from_source) @app.route('/user_rt/<int:user_id>') def get_user_read_through(user_id): user_data = user_service.get_user(user_id) if user_data: return jsonify(user_data) else: return jsonify({"message": f"Benutzer {user_id} nicht gefunden"}), 404 @app.route('/user_rt/<int:user_id>/update', methods=['POST']) def update_user_read_through(user_id): # Der Einfachheit halber machen wir hier nur den Cache ungültig und gehen davon aus, dass die DB-Aktualisierung woanders stattfindet result = user_service.update_user(user_id, {"some_new_data": "value"}) return jsonify(result), 200 if __name__ == '__main__': app.run(debug=True, port=5001)
In diesem Setup kapselt der UserService
die Logik zur Prüfung des Caches, zum Abrufen aus der Datenquelle und zum Befüllen des Caches. Die Flask-Route get_user_read_through
ruft einfach user_service.get_user
auf und muss die zugrunde liegenden Caching-Details nicht kennen. Dies macht den Anwendungscode sauberer und fokussierter auf die Geschäftslogik.
Wann Read-Through verwenden
- Vereinfachter Client-Code: Die Anwendung muss die Logik für Cache-Abfrage/Befüllung nicht implementieren.
- Gekapseltes Caching: Ideal, wenn Sie Caching-Belange von der Hauptanwendungslogik abstrahieren möchten, was saubereren Code und einfachere Wartung fördert.
- Konsistenter Datenzugriff: Alle Datenzugriffe erfolgen über die Caching-Schicht, wodurch sichergestellt wird, dass die Caching-Richtlinien konsistent angewendet werden.
- Komplexe Caching-Anforderungen: Wenn die Abrufloik aus der primären Quelle komplexer ist oder mehrere Schritte umfasst, kann sie sauber innerhalb der Read-Through-Komponente untergebracht werden.
Wichtige Unterschiede und Überlegungen
Funktion | Cache-Aside | Read-Through |
---|---|---|
Logikstandort | Anwendungslogik verwaltet Cache-Interaktionen. | Caching-Schicht/Bibliothek kapselt Cache-Interaktionen. |
Einfachheit (Entw.) | Mehr explizite Kontrolle, potenziell ausführlicher. | Einfacherer Anwendungscode für Lesevorgänge. |
Datenkonsistenz | Anwendung muss Cache nach Schreibvorgängen explizit ungültig machen/aktualisieren. | Ähnliche explizite Ungültigmachung oft erforderlich, kann aber Teil des Vertrags der Caching-Schicht sein. |
Flexibilität | Hohe Flexibilität bei der Caching-Strategie. | Weniger flexibel, Caching-Logik ist Teil des integrierten Systems. |
Kalter Start | Anwendung behandelt Abruf für anfängliche Misserfolge. | Caching-Schicht behandelt Abruf für anfängliche Misserfolge. |
Beide Muster sind sehr effektiv, und die Wahl hängt von Ihrer spezifischen Anwendungsarchitektur, den Vorlieben Ihres Teams und der Komplexität Ihrer Datenzugriffsmuster ab. Cache-Aside bietet mehr Kontrolle, während Read-Through die Verantwortung der Anwendung für das Caching vereinfacht. In der Praxis kombinieren viele Anwendungen Elemente beider oder verwenden ein spezialisiertes Caching-Framework, das eine Read-Through-ähnliche Schnittstelle auf Basis eines Redis-Backends bereitstellt.
Fazit
Caching ist eine entscheidende Technik zum Aufbau von Hochleistungs-und skalierbaren Webanwendungen. Durch die strategische Nutzung von Redis mit Mustern wie Cache-Aside und Read-Through können Entwickler die Datenbanklast erheblich reduzieren, die Latenz minimieren und ein überlegenes Benutzererlebnis liefern. Das Verständnis dieser Muster und ihrer Implementierung befähigt Sie, robuste Caching-Lösungen zu entwickeln, die Ihre Webanwendungen schnell und reaktionsschnell halten. Letztendlich stellt intelligentes Caching mit Redis sicher, dass die Daten Ihrer Webanwendung auch unter hoher Last stets effizient geliefert werden.