Caching in Yurna: schnell bleiben, ohne falsche Daten zu zeigen
Zwischenspeicherung spart Zugriffe, verändert aber die Bedeutung von Aktualität. An einer geänderten Servereinstellung erklärt dieser Artikel Schlüssel, Ablaufzeiten, Entwertung und parallele Leser und ordnet Yurnas Redis- und Speicherfallback nach ihren tatsächlichen Grenzen ein.
1539 Wörter · 8 Min. Lesezeit
- yurna
- caching
- redis
- konsistenz
Schematische Illustration zur Artikelserie; keine privaten Betriebsdaten.
Eine Verwaltungsperson schaltet eine Funktion aus. Das Dashboard bestätigt die Speicherung, doch der Bot reagiert noch mit der alten Einstellung. Vielleicht ist nichts verloren gegangen. Ein Prozess verwendet lediglich einen zwischengespeicherten Wert. Für die Person vor dem Bildschirm ist dieser Unterschied kaum erkennbar. Sie sieht eine widersprüchliche Anwendung. Caching ist deshalb nicht nur eine Leistungsfrage. Es legt fest, wie lange verschiedene Ansichten auseinanderliegen dürfen und welche Aussage eine Bestätigung tatsächlich macht.
Yurnas gemeinsames Datenbankpaket enthält eine kleine Schlüssel-Wert-Abstraktion. Sie verwendet Redis, wenn ein entsprechender Client vorhanden ist, und andernfalls einen lokalen Speicher im Prozess. Der Quellcode benennt ausdrücklich, dass dieser Speicher nicht zwischen Bot und Dashboard geteilt wird und bei einem Neustart verschwindet. Diese Unterschiede sind für einen Cache häufig akzeptabel. Sie dürfen jedoch nicht versehentlich als gleichwertige Grundlage für globale Sperren oder dauerhaft verbindliche Entscheidungen behandelt werden.
Für jeden Wert eine erlaubte Veraltung bestimmen
Ein Servername darf möglicherweise kurz veraltet angezeigt werden, ohne dass jemand eine falsche Handlung ausführt. Eine deaktivierte gefährliche Verwaltungsfunktion hat einen anderen Anspruch. Ein Coinbestand, auf dessen Grundlage eine Übertragung erfolgt, ist wiederum etwas anderes als eine Rangliste zur Unterhaltung. Die Frage lautet deshalb nicht pauschal, ob Yurna cachen sollte. Sie lautet, welche Folgen ein veralteter Wert an der jeweiligen Stelle hätte und wie lange diese Folgen vertretbar sind.
Für eine Anzeige kann eine begrenzte ältere Sicht sinnvoll sein. Für eine Mutation wird die entscheidende Voraussetzung erneut verbindlich geprüft. So kann das Dashboard eine Rollenliste schnell anzeigen, während die tatsächliche Rollenänderung die aktuelle Zulässigkeit kontrolliert. Die beiden Zugriffe müssen nicht dieselbe Aktualitätsstrategie besitzen. Wer Anzeige und Ausführung unterschiedslos durch dieselbe Cachefunktion führt, verwischt eine wichtige Grenze und macht spätere Ausnahmen schwer nachvollziehbar.
Ein Entwurf hält diese Entscheidung pro Datenart knapp fest: Quelle, erlaubtes Alter, Verhalten bei Ausfall und Ereignis zur Entwertung. Das ist keine umfangreiche Verwaltungsübung. Schon vier klare Angaben verhindern, dass eine zufällig gewählte Standarddauer später als fachliche Garantie verstanden wird. Besonders bei gemeinsam verwendeten Paketen sollte die Standarddauer nicht die einzige Dokumentation sein. Die aufrufende Funktion muss wissen, ob sie einen aktuellen Nachweis oder lediglich eine brauchbare Anzeige erhält.
Schlüssel bilden fachliche Grenzen ab
Ein Cacheeintrag für Servereinstellungen braucht den Serverbezug. Eine persönliche Ansicht benötigt zusätzlich die Person oder einen gleichwertigen Berechtigungsbezug. Fehlt diese Information im Schlüssel, können Daten aus unterschiedlichen Kontexten zusammenfallen. Solche Fehler sind schwer zu erkennen, wenn Tests nur einen Server und ein Konto verwenden. Ein zweiter künstlicher Kontext mit absichtlich ähnlichen Namen ist daher ein einfacher, aber wirkungsvoller Prüfaufbau.
Schlüssel sollten außerdem unterscheiden, welche Darstellung gespeichert ist. Eine kurze öffentliche Serverübersicht ist nicht dasselbe wie eine vollständige Verwaltungsansicht. Beide unter demselben allgemeinen Namen zu speichern, kann dazu führen, dass ein Aufrufer unerwartet mehr Daten erhält als vorgesehen. Ein klarer fachlicher Namensraum und eine Formatversion machen solche Unterschiede sichtbar. Die Version hilft auch dann, wenn sich die Struktur des gespeicherten Werts bei einem Update verändert.
// Beispielhafte Schlüssel: keine echten Server- oder Nutzerkennungen.
const settingsKey = (guildRef: string) => `guild-settings:v2:${guildRef}`;
const publicSummaryKey = (guildRef: string) => `guild-summary:v1:${guildRef}`;
const personalViewKey = (guildRef: string, memberRef: string) =>
`member-view:v1:${guildRef}:${memberRef}`;Die Schlüssel ersetzen keine Berechtigungsprüfung. Sie verhindern lediglich, dass verschiedene gültige Kontexte versehentlich denselben Speicherplatz verwenden. Eine geschützte Ansicht wird erst nach Prüfung des aktuellen Zugriffs ausgeliefert. Besonders bei gemeinsamem Redis ist zu bedenken, dass der Cache technisch von mehreren Anwendungen gelesen werden kann. Der Zugriff auf diesen Dienst und die Auswahl gespeicherter Inhalte gehören deshalb zum Entwurf, auch wenn die Anwendung nur eine kleine Hilfsfunktion aufruft.
Lesen bei fehlendem Eintrag
Ein üblicher Ablauf lädt zunächst den Cache. Fehlt der Eintrag, wird die eigentliche Quelle gelesen und das Ergebnis anschließend mit Ablaufzeit gespeichert. Das ist einfach, führt aber bei vielen gleichzeitigen Anfragen zu mehrfacher Arbeit: Alle sehen denselben fehlenden Eintrag und laden parallel dieselbe Quelle. Für seltene Einstellungen ist das möglicherweise unproblematisch. Für teure Discord-Abfragen oder Bilddaten kann eine Zusammenfassung gleichzeitiger Ladevorgänge sinnvoll sein.
Innerhalb eines Prozesses kann ein gemeinsames laufendes Promise die gleichzeitigen Leser bündeln. Über mehrere Prozesse hinweg braucht eine weitergehende Lösung einen geteilten Mechanismus. Auch dabei ist Vorsicht nötig: Eine Ladesperre darf nach einem abgestürzten Prozess nicht unbegrenzt bestehen bleiben. Der zusätzliche Aufwand lohnt sich nur, wenn die vermiedene Arbeit tatsächlich relevant ist. Ein Cache sollte nicht durch vorsorglich komplizierte Koordination schwerer zu betreiben sein als die ursprüngliche Abfrage.
Fehler beim Laden sollten nicht unbemerkt als gültige leere Daten gespeichert werden. Eine leere Rollenliste kann eine echte Antwort sein oder das Ergebnis einer nicht erreichbaren Quelle. Diese Zustände sind verschieden. Ein gezielter negativer Cache kann für nachweislich fehlende Objekte sinnvoll sein, braucht aber meist eine andere Dauer als ein erfolgreicher Treffer. Vorübergehende technische Fehler sollten nicht mehrere Minuten lang wie eine bestätigte Abwesenheit aussehen.
Speichern und Entwerten richtig ordnen
Im Beispiel ändert eine Verwaltungsperson den Ausgabekanal einer Funktion. Der neue Wert wird zuerst in der verbindlichen Quelle gespeichert. Anschließend wird der zugehörige Cacheeintrag entwertet oder gezielt aktualisiert. Eine Entwertung vor dem Speichern kann dazu führen, dass ein anderer Leser noch den alten Datenbankwert lädt und erneut in den Cache schreibt. Die Reihenfolge allein löst allerdings nicht jede Konkurrenz. Auch bereits laufende ältere Lesevorgänge müssen betrachtet werden.
Ein älterer Ladevorgang könnte nach der Änderung zurückkehren und den alten Wert wieder einsetzen. Eine Versionsnummer im gespeicherten Wert hilft, solche Fälle sichtbar zu machen und gegebenenfalls abzuweisen. Der Entwurf kann außerdem nach einer Änderung eine neue Schlüsselversion oder eine bekannte Konfigurationsrevision verwenden. Welche Lösung angemessen ist, hängt von der Änderungsfrequenz und dem erlaubten Alter ab. Wichtig ist, diesen Ablauf einmal bewusst durchzuspielen, statt nur den normalen schnellen Fall zu betrachten.
Die Bestätigung an die Verwaltung sollte den tatsächlich erreichten Stand benennen. Wenn die Datenbank gespeichert ist, aber andere Prozesse den Wert erst beim nächsten Abgleich übernehmen, darf die Oberfläche diese Verzögerung erklären. Bei Einstellungen, die sofort gelten müssen, reicht eine irgendwann ablaufende Kopie nicht. Dann braucht der Betrieb eine passende Benachrichtigung oder eine aktuelle Prüfung am Ausführungspunkt. Eine pauschale Cacheentwertung im Dashboard erreicht einen lokalen Speicher des Bots nicht automatisch.
Redis und lokaler Speicher unterscheiden
Yurnas Abstraktion bietet in beiden Varianten ähnliche Methoden wie Lesen, Schreiben, Löschen und Ablaufzeit setzen. Diese gemeinsame Oberfläche erleichtert die Nutzung. Sie macht die Betriebsformen jedoch nicht identisch. Im lokalen Modus besitzt jeder Prozess seine eigene Map. Eine Löschung in der Admin-API entfernt deshalb nicht gleichzeitig einen Eintrag im Dashboard. Ein Neustart löscht lokale Einträge, während ein externer Dienst seinen eigenen Lebenszyklus hat. Diese Unterschiede gehören in Tests mit mehreren Anwendungen.
Die Redis-Dokumentation zu EXPIRE beschreibt die zeitliche Begrenzung eines Schlüssels. Für die gemeinsame Yurna-Abstraktion muss zusätzlich geprüft werden, ob Randwerte in beiden Implementierungen dieselbe Bedeutung haben. Was bedeutet etwa eine Dauer von null oder ein negativer Wert? Soll der Eintrag verschwinden, unbegrenzt gelten oder als ungültige Eingabe abgelehnt werden? Solche Details sollten als Vertrag definiert sein, bevor mehrere Fachfunktionen unterschiedliche Annahmen darauf aufbauen.
Ein optionaler Cache darf bei seiner Abwesenheit nicht die eigentliche Datenquelle ersetzen. Wenn Redis nicht konfiguriert ist, kann der lokale Modus eine praktische Beschleunigung bieten. Wenn ein konfigurierter Redis-Dienst ausfällt, ist das eine andere Situation, die ausdrücklich behandelt werden muss. Die Anwendung sollte nicht behaupten, dass jeder Fehler automatisch transparent überbrückt wird, nur weil grundsätzlich ein lokaler Modus existiert. Konfiguration und Laufzeitstörung sind verschiedene Pfade im Code.
Browsercache und Anwendungscache auseinanderhalten
Zusätzlich zum serverseitigen Cache kann der Browser Antworten aufbewahren. Eine intern korrekt entwertete Einstellung kann deshalb trotzdem in einer alten Seite erscheinen. Die MDN-Dokumentation zu HTTP-Caching beschreibt die dafür verwendeten HTTP-Regeln. Für ein Dashboard muss bewusst festgelegt werden, welche Antworten wiederverwendbar sind und welche personenbezogenen oder veränderlichen Daten eine engere Behandlung benötigen. Ein interner Cacheeintrag und ein HTTP-Cacheheader lösen unterschiedliche Aufgaben.
Auch der Zustand einer geöffneten Oberfläche kann veralten. Nach dem Speichern sollte sie den bestätigten neuen Wert übernehmen oder gezielt neu laden. Ein allgemeines Neuladen aller Daten ist nicht immer nötig und kann unsichtbare Nebenwirkungen erzeugen, etwa das Verwerfen ungespeicherter Eingaben in einem anderen Abschnitt. Besser ist eine begrenzte Aktualisierung der tatsächlich betroffenen Ansicht. Dadurch bleibt der Zusammenhang zwischen Handlung und sichtbarem Ergebnis klar, ohne sämtliche Zwischenspeicherung abzuschalten.
Bei mehreren offenen Tabs hilft eine sichtbare Konflikterkennung. Ein Tab kann eine ältere Konfiguration bearbeiten, obwohl der Servercache bereits aktuell ist. Dieses Problem wird nicht durch eine kürzere Ablaufzeit gelöst. Die Mutation muss ihre gelesene Grundlage mitgeben und bei Änderungen einen Konflikt erkennen. Caching ist hier nur eine von mehreren Ursachen veralteter Ansichten. Ein gutes Konzept vermeidet, alle zeitlichen Probleme unter demselben technischen Begriff zu verstecken.
Messen, ob der Cache seinen Zweck erfüllt
Eine hohe Trefferquote klingt gut, kann aber wenig aussagen, wenn die falschen Daten lange gespeichert werden. Für den Entwurf werden deshalb mehrere Beobachtungen kombiniert: vermiedene Quellzugriffe, Ladezeiten bei fehlendem Eintrag, Alter ausgelieferter Werte und Fehler bei der Entwertung. Diese Zahlen sollten auf konkrete Funktionen bezogen werden. Eine globale Quote über Ranglisten, Einstellungen und Berechtigungen mischt sehr unterschiedliche Anforderungen und kann problematische Teilbereiche verdecken.
Der Aufwand des Caches selbst zählt ebenfalls. Große Werte kosten beim Serialisieren, Übertragen und Einlesen Zeit. Ein kleiner Datenbankzugriff kann unter Umständen günstiger sein als eine zusätzliche entfernte Cacheabfrage mit umfangreichem JSON. Solche Entscheidungen werden durch Messungen an repräsentativen Testfällen überprüft, nicht durch allgemeine Behauptungen über Redis oder SQLite. Eine Optimierung ist nur dann hilfreich, wenn sie die tatsächlich relevante Arbeit reduziert und ihre Konsistenzkosten vertretbar bleiben.
Für die Servereinstellung wird schließlich eine kontrollierte Zeitfolge getestet: Bot liest alt, Dashboard speichert neu, ein verzögerter alter Lesevorgang beendet sich und der Bot verarbeitet eine weitere Aktion. Danach wird dieselbe Folge im lokalen Modus mit getrennten Prozessen wiederholt. Die erwartete maximale Verzögerung ist vorher festgelegt. Der Test prüft außerdem einen Neustart und eine nicht erreichbare Quelle. So wird sichtbar, welche Aktualität der Cache wirklich liefert. Geschwindigkeit bleibt dann ein erklärbarer Vorteil und kein zufälliger Tausch gegen falsche Zustände.