Alle Artikel
12. September 2026
7 mins

Der Cache als Schuld

Von Tim Rutte, Cloud & Software ArchitectThemaBackend & Plattformen

Ein Einmachglas mit eingelegten Birnen, oben angeschimmelt; Bügel und Dichtring sind blau.

In fast jedem gewachsenen System gibt es eine Stelle, an der niemand mehr etwas anfassen will, und oft ist es ein Zwischenspeicher. Er wurde vor Jahren eingebaut, weil eine Seite zu langsam war. Er hat funktioniert. Seitdem steht er da, und niemand weiß mehr genau, was er enthält, wann er verworfen wird und was passiert, wenn man ihn abschaltet.

Ein Zwischenspeicher ist kein Fehler. Er ist ein Versprechen: Ich weiß, wann diese Daten falsch sind. Solange dieses Versprechen jemand einlöst, ist er ein Werkzeug. Sobald es niemand mehr einlöst, ist er eine Schuld, die Zinsen in einer eigenen Währung zahlt: falsche Daten, die niemand reproduzieren kann.

Warum aus einem Zwischenspeicher eine Schuld wird

Der Weg dorthin ist immer derselbe, und er ist nicht das Ergebnis von Nachlässigkeit, sondern von Druck.

Eine Seite ist langsam, der Termin steht, die Ursache liegt tief in einer Abfrage, die niemand in zwei Tagen entwirrt. Der Zwischenspeicher davor löst das Problem in zwei Stunden. Das ist in dem Moment die richtige Entscheidung.

Falsch wird sie erst durch das, was danach nicht passiert: Es wird nicht aufgeschrieben, warum er da ist. Kein Eintrag, unter welcher Bedingung er wieder verschwinden kann, keine Messung, wie viel er tatsächlich bringt, und kein Name eines Zuständigen. Zwei Jahre später ist er Teil der Architektur, ohne je entworfen worden zu sein.

Die Kosten entstehen nicht beim Speichern. Sie entstehen bei jeder Änderung danach: Jede neue Funktion muss wissen, welche Zwischenspeicher sie ungültig macht, und jede Fehlersuche beginnt mit der Frage, ob das Problem überhaupt echt ist oder nur alt.

Drei Arten von Zwischenspeicher, und nur eine ist gesund

Vor jedem Abbau steht eine Inventur, und dabei fallen die vorhandenen Zwischenspeicher fast immer in drei Gruppen.

Der begründete. Er steht vor einer teuren Berechnung, hat eine bewusste Gültigkeitsdauer, und wenn er leer ist, wird die Anwendung langsamer, aber nicht falsch. Das ist der gesunde Fall, und den lässt man in Ruhe.

Der tragende. Ohne ihn steht das System. Die Datenbank hält die Last nicht aus, die entsteht, wenn er leer ist. Das ist kein Zwischenspeicher mehr, sondern ein unbewusst gewählter Teil der Architektur, und er hat eine unangenehme Eigenschaft: Er ist genau dann leer, wenn es am schlechtesten passt, nämlich nach einem Neustart unter Last.

Der Geist. Niemand weiß, was er enthält. Er wurde für einen Anwendungsfall gebaut, der seit Jahren anders gelöst ist, und er wird nie verworfen, weil der Vorgang, der ihn verwerfen würde, entfernt wurde. Er ist die Quelle der Fehlermeldungen, die sich „bei mir nicht nachstellen" lassen.

Die Inventur ist keine Analysephase, sie ist ein Nachmittag: alle Stellen finden, an denen geschrieben wird, und für jede drei Angaben festhalten. Was steht drin, wie lange, und wer merkt es, wenn es falsch ist.

Verwerfen: die teure Lösung ist selten die richtige

Die verbreitete Annahme lautet, dass genaues Verwerfen besser ist als Ablaufzeit. Das stimmt in der Theorie und kostet in der Praxis am meisten.

Ereignisbasiertes Verwerfen heißt: Jede Schreiboperation im System muss wissen, welche Schlüssel sie ungültig macht. Das ist eine Kopplung zwischen Stellen, die sonst nichts miteinander zu tun haben, und sie wird bei jeder neuen Funktion neu vergessen. Die Fehler daraus zeigen sich nur bei bestimmter Reihenfolge und tauchen deshalb nie im Test auf.

Die ehrliche Frage ist nicht „wie verwerfen wir genau", sondern: Wie alt dürfen diese Daten sein, damit die Fachseite es akzeptiert? Bei Kategoriebäumen, Konfigurationen, Übersichtsseiten und Suchergebnissen liegt die Antwort öfter bei „zwei Minuten" als bei „sofort", und damit ist eine Ablaufzeit die vollständige Lösung.

Wo die Antwort tatsächlich „sofort" lautet, ist die Frage meist eine andere: Dann gehört der Wert nicht in einen Zwischenspeicher, sondern die darunterliegende Abfrage schnell genug gemacht.

Für den tragenden Fall gibt es eine Maßnahme, die oft fehlt und bei jedem Neustart unter Last zählt. Läuft ein heißer Schlüssel ab, laufen tausend gleichzeitige Anfragen dieselbe teure Abfrage, und die Datenbank geht genau in dem Moment zu Boden, in dem sie gebraucht wird. Es reicht, wenn eine davon rechnet:

// singleflight bündelt gleichzeitige Anfragen nach demselben Schlüssel:
// eine rechnet, die anderen warten auf deren Ergebnis.
var gruppe singleflight.Group

func (s *Katalog) Baum(ctx context.Context, id string) (*Baum, error) {
    if b, ok := s.cache.Get(id); ok {
        return b, nil
    }

    // Ohne diese Zeile schlagen bei einem abgelaufenen heissen Schluessel
    // alle gleichzeitigen Anfragen zusammen auf der Datenbank auf.
    wert, err, _ := gruppe.Do(id, func() (any, error) {
        b, err := s.db.BaumLaden(ctx, id)
        if err != nil {
            return nil, err
        }
        s.cache.Set(id, b, 2*time.Minute)
        return b, nil
    })
    if err != nil {
        return nil, err
    }
    return wert.(*Baum), nil
}

Das ist wenige Zeilen und ersetzt die Diskussion über das Vorwärmen des Zwischenspeichers in den meisten Fällen vollständig.

Wo alte Daten kein Schönheitsfehler sind

Bei den meisten Werten ist ein veralteter Stand ärgerlich. Bei drei Arten von Werten ist er ein Vorfall, und diese drei gehören in jeder Inventur zuerst angesehen: Preise, Bestände und Berechtigungen.

Der gefährlichste Fall ist der dritte, weil er sich als Optimierung tarnt. Eine Berechtigungsprüfung ist teuer, also wird das Ergebnis zwischengespeichert. Ab da wirkt ein Entzug von Rechten erst, wenn der Eintrag abläuft:

// Das hier steht in mehr Systemen, als einem lieb ist.
// Wer entlassen wird, darf noch eine Stunde lang alles.
$darf = $this->cache->get(
    "rechte_{$nutzerId}_{$aktion}",
    fn () => $this->rechte->pruefen($nutzerId, $aktion),
    3600,
);

Die Behebung ist nicht, die Gültigkeitsdauer zu verkürzen. Sie ist, die richtige Sache zwischenzuspeichern: nicht die Entscheidung, sondern die Daten, aus denen die Entscheidung entsteht, und die Entscheidung selbst bei jeder Anfrage zu treffen.

// Die Rollen des Nutzers sind zwischengespeichert und werden beim
// Entzug gezielt verworfen. Die Entscheidung faellt jedes Mal neu.
$rollen = $this->cache->get(
    "rollen_{$nutzerId}",
    fn () => $this->rechte->rollen($nutzerId),
    300,
);

return $this->regeln->erlaubt($rollen, $aktion, $objekt);

Der Unterschied kostet fast nichts an Rechenzeit und entfernt eine ganze Klasse von Vorfällen. Die Regel dahinter gilt allgemein: Entscheidungen werden nicht zwischengespeichert, Daten schon.

Sobald mehrere Kunden auf demselben System sitzen, wird ein Schlüssel ohne Mandantenkennung zum Datenleck: Mandantenfähigkeit nachrüsten.

Wie man die Schuld zurückzahlt

Abbauen heißt nicht abschalten. Es heißt, für jeden Eintrag der Inventur eine von drei Entscheidungen zu treffen, und die Reihenfolge ist dabei wichtig.

Zuerst die Geister. Für jeden Zwischenspeicher, den niemand erklären kann, wird die Trefferquote gemessen. Ein Zwischenspeicher mit zwei Prozent Trefferquote kostet Komplexität und bringt nichts; er wird entfernt, nicht dokumentiert. Das ist der billigste Teil der Arbeit und oft der größte.

Dann die tragenden. Sie werden nicht entfernt, sondern sichtbar gemacht: Sie bekommen eine Messung, wie das System mit leerem Zwischenspeicher antwortet, und ein Bündeln gleichzeitiger Anfragen wie oben. Wer das nicht messen will, hat die Messung trotzdem, nur zu einem Zeitpunkt seiner Wahl nicht.

Zuletzt die begründeten. Sie bekommen einen Kommentar mit drei Sätzen: warum es sie gibt, wie lange sie gelten, und unter welcher Bedingung sie verschwinden dürfen. Die letzte Angabe ist die, die sonst fehlt.

Der Versuch, der die Diskussion am schnellsten beendet, ist das gezielte Abschalten eines einzelnen Zwischenspeichers auf einem Teil des Verkehrs, für eine Stunde, mit Messung davor und danach. In der Hälfte der Fälle ist der Unterschied nicht messbar. Das ist dann kein Argument für einen kühnen Umbau, sondern die Erlaubnis, eine Stelle wegzunehmen, an der bisher jede Änderung teurer war.

Dieselbe Übung für Funktionen statt für Zwischenspeicher steht in Funktionen entfernen, die niemand mehr nutzt.

Die Regel, damit es nicht nachwächst

Der Abbau hält nicht, wenn die nächste knappe Woche den nächsten unbegründeten Zwischenspeicher erzeugt. Eine Regel reicht, und sie ist an einem Nachmittag eingeführt: Ein neuer Zwischenspeicher braucht vier Angaben, sonst wird er nicht zusammengeführt. Was drin steht, wie lange, wer ihn verwirft, und woran man erkennen würde, dass er überflüssig ist.

Diese vier Angaben sind kein Vorgang, sie sind vier Zeilen im Änderungsantrag. Und die vierte ist die wichtigste: Sie ist der Unterschied zwischen einer Entscheidung, die später jemand überprüfen kann, und einer, die zu Architektur wird, weil niemand sie mehr versteht.

Wer wissen will, wo in seinem System die Zwischenspeicher stehen und was sie kosten, fängt bei den technischen Schulden an, die er ohnehin schon kennt: Die Stelle, an der niemand etwas anfassen will, ist selten eine andere. Wie ich solche Systeme weiterentwickle, steht auf einer eigenen Seite.

Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.