CodeGym /Kurse /Docker for Spring /Redis nach stabilem app + postgres

Redis nach stabilem app + postgres

Docker for Spring
Level 19, Lektion 0
Verfügbar

1. Stabiles Fundament app+postgres

Wenn Sie eine neue Infrastrukturabhängigkeit hinzufügen (Redis, Message Broker, was auch immer), fügen Sie nicht einfach „noch einen Container“ hinzu. Sie erhöhen die Anzahl der Stellen, an denen etwas schiefgehen kann: Netzwerk, Konfiguration, Profile, Readiness, Ports, Image‑Versionen, Startreihenfolge. Wenn der Basisstack noch wackelt, verwandelt die neue Abhängigkeit die Diagnose in einen Detektiv ohne Ende: Es gibt Hinweise, der Täter ist unbekannt.

In der Docker/Compose‑Umgebung gibt es eine unangenehme Mathematik: Je mehr Dienste, desto mehr Kombinationen von Problemen. Solange wir nur einen Container app hatten, war alles relativ linear: „startet / startet nicht“. Als postgres hinzukam, wurde es interessant: „Startet die Anwendung nicht wegen des Profils? Wegen des Hosts? Wegen des Passworts? Wegen der Readiness? Wegen der Migration?“. Das ist bereits normal, erfordert aber Disziplin.

Wenn wir Redis früher eingeführt hätten, in dem Moment, als app + postgres noch nicht auf „stabil hochfahrbar“ gebracht war, hätten Sie die perfekte Lehrkatastrophe erlebt. Die Anwendung stürzt ab, und Sie verstehen nicht, was genau schiefgeht: keine Verbindung zur Datenbank, fehlgeschlagene Migrationen, Redis nicht erreichbar, oder Sie haben einfach versehentlich localhost geschrieben. Eine solche Lernweise endet meist damit, dass der Student den Fehler durch eine zufällige Menge von Änderungen „besiegt“, aber nicht erklären kann, was genau er getan hat. Hier trainieren wir genau die gegenteilige Fähigkeit: Erklärbarkeit und Reproduzierbarkeit.

Man kann es sich wie eine Wohnungsrenovierung vorstellen. Zuerst macht man die Wände gerade und die Elektrik sicher, und erst dann kauft man ein Smart Home und eine Beleuchtung, die die Farbe nach Stimmung wechselt. Wenn man mit der Beleuchtung beginnt, blinkt sie zwar sehr schön vor den schiefen Wänden – aber das Wohnen ist trotzdem anstrengend.

Was bedeutet „stabiles app + postgres“

„Stabil“ bedeutet nicht „einmal nach drei Stunden Schmerz gestartet und ich traue mich nicht, den Computer anzufassen“. Im Kontext des Kurses ist Stabilität die Fähigkeit, den Start zu wiederholen und das gleiche Ergebnis zu erzielen, sowie im Fehlerfall schnell die Problemklasse zu erkennen. Das macht die lokale Umgebung zu einem Entwicklungswerkzeug und nicht zu einem persönlichen, fragilen Artefakt auf einem einzelnen Laptop.

Wichtig ist, dass app + postgres in drei Dimensionen vorhersagbar ist: Start, Daten, Beobachtbarkeit. Start bedeutet, dass die Anwendung die Datenbank nicht „überholt“ und nicht bei dem Versuch, sich zu verbinden, abstürzt. Daten bedeutet, dass Migrationen angewendet werden, das Schema im erwarteten Zustand ist und ein Reset so funktioniert, wie Sie es verstehen (nicht „irgendetwas wurde gelöscht, aber ich weiß nicht was“). Beobachtbarkeit bedeutet, dass anhand der Logs und Health‑Endpunkte ersichtlich ist, was passiert, und Sie das System nicht anhand des Gesichtsausdrucks diagnostizieren.

Um das etwas greifbarer zu machen, halten wir fest, „wie ein gesunder Stack aussieht“ – nicht als Liste von Zaubersprüchen, sondern als Tabelle beobachtbarer Signale.

Was wir prüfen Wo ist es sichtbar Wie sieht „ok“ aus
PostgreSQL ist wirklich bereit docker compose ps, Health‑Status, Logs von postgres Status
healthy
, in den Logs keine Endlos‑Neustarts
Anwendung startet nach DB‑Bereitschaft Logs von app Keine endlosen Connection refused, erfolgreicher Start sichtbar
Flyway hat Migrationen angewendet Logs von app Migrationen angewendet sichtbar, Anwendung stürzt nicht mit Validate failed ab
API antwortet
GET /api/catalog/items
GET /actuator/health
HTTP 200, Health grün
Reset der Umgebung verständlich docker compose down vs down -v Sie verstehen, wann Daten erhalten bleiben und wann Sie sie bewusst löschen

Beachten Sie: Hier gibt es keine „magischen Erwartungen“ wie „soll schnell sein“ oder „soll schön sein“. Stabilität bedeutet, dass Sie die Umgebung morgen, auf einem anderen Rechner, auf einem anderen Betriebssystem hochfahren können und die gleichen Eigenschaften erhalten.

Und erst nach diesem Fundament ergibt es Sinn, Redis hinzuzufügen. Denn Redis ist eine Schicht über Lesevorgängen, und die Lesevorgänge müssen bereits zur Wahrheitsquelle durchdringen können. Das zu cachen, was nicht stabil gelesen werden kann, ist wie Klebeband auf ein undichtes Rohr zu kleben und sich zu freuen, dass das Wasser „ein bisschen anders“ fließt.

2. Redis als Cache‑Schicht

Wenn ein Anfänger zum ersten Mal „Redis“ hört, entsteht oft das Bild einer „sehr schnellen Datenbank, die PostgreSQL ersetzt und alles blitzschnell macht“. Das ist nicht völlig falsch, aber im Rahmen unseres Kurses ist es eine zu breite Sichtweise – und sie stört. Heute brauchen wir Redis nicht als neue Welt, sondern als Infrastrukturerweiterung: Wir fügen ihn dem Compose‑Stack hinzu und nutzen ihn als Cache für wiederholte Lesevorgänge.

Ein Cache ist ein gespeichertes Leseergebnis, das ohne erneuten Zugriff auf den primären Speicher zurückgegeben werden kann. In unserem Projekt ist der primäre Speicher im postgres‑Modus PostgreSQL. Sie bleibt die Wahrheitsquelle: Wenn Redis morgen verschwindet, dürfen die Geschäftsdaten nicht damit „verschwinden“. Das ist ein wichtiger Punkt: Redis speichert abgeleitete Daten, nicht primäre.

Um Verwirrung zu vermeiden, ist es hilfreich, sich die Rollenunterschiede vor Augen zu halten, besonders in einem Lernprojekt:

Frage PostgreSQL (im postgres‑Modus) Redis (im cache‑Modus)
Was speichert es? Primäre Geschäftsdaten des Katalogs Leseergebnisse (Cache)
Was passiert, wenn es gelöscht wird? Sie verlieren Daten (und das tut weh) Der Cache verschwindet, wird aber durch erneute Lesevorgänge wieder aufgebaut
Wofür wird es benötigt? Datenkorrektheit und ‑konsistenz Beschleunigung wiederholter Lesevorgänge und Entlastung des primären Speichers
Wie wird es „aktiviert“? postgres‑Profil, DataSource‑Config, Migrationen cache‑Profil, Redis‑Host/Port, Caching auf Read‑Methoden

Diese Sichtweise hilft, die Ebene nicht übermäßig zu verkomplizieren. Wir bauen keine „Redis‑zentrische Architektur“ und machen den Container-Ready Catalog Service nicht zu einem Redis‑Lehrbuch. Wir fügen Redis so hinzu, wie er oft in echten Teams auftaucht: nach der Datenbank, als nächste Infrastrukturschicht, die über ein Profil aktiviert wird, um den Hauptpfad nicht zu stören.

3. Cache Hit / Cache Miss

Caching klingt wie Magie: „gespeichert – und es wurde schnell“. Aber es wird genau in dem Moment verständlich, in dem man zwei Wörter aussprechen kann: Cache Miss und Cache Hit. Miss bedeutet, dass das gewünschte Ergebnis noch nicht im Cache liegt und wir gezwungen sind, zum primären Speicher zu gehen. Hit bedeutet, dass das Ergebnis bereits im Cache liegt und wir es zurückgeben, ohne zur Datenbank zu gehen.

Ein entscheidendes Detail ist der Cache Key, also der Wert, nach dem wir den Eintrag im Cache suchen. Beim Lesen eines Elements anhand der ID ist das fast immer die id selbst. Beim Lesen der „gesamten Liste“ ist der Schlüssel tatsächlich derselbe, weil die Anfrage dieselbe ist. Wichtig ist hier die Mechanik: gleiche Anfrage → gleicher Key → Chance auf Hit.

Schauen wir uns ein minimales Modell in unserer Domäne an, noch ohne Spring‑Annotationen und ohne Redis – einfach damit das Gehirn die „Mechanik“ sieht, nicht die „magischen Wörter“.

import com.example.catalog.catalog.model.CatalogItem;

// Zwei identische Lesevorgänge hintereinander: aus Sicht des Speichers sind es zwei separate Anfragen
CatalogItem first = storage.findById(10L);
CatalogItem second = storage.findById(10L); // der zweite Zugriff geht erneut zum primären Speicher

// Die Daten sind identisch, aber die Arbeit wurde zweimal erledigt
System.out.println(first.getSku());   // z. B.: "SKU-10"
System.out.println(second.getSku());  // z. B.: "SKU-10"

Hier sind beide Lesevorgänge inhaltlich identisch, aber aus Sicht der Anwendung sind es zwei unabhängige Zugriffe auf den Speicher. Wenn storage unter der Haube PostgreSQL ist, bitten wir die Datenbank zweimal, Arbeit zu erledigen. Der Cache taucht genau an der Stelle auf: „Kann man beim zweiten Mal darauf verzichten?“.

Nun ein vereinfachtes „manuelles“ Cache‑Modell (ja, das ist kein Produktionscode, aber eine ehrliche Mechanik):

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

class SimpleItemCache {
    // Lokaler Cache im Prozessspeicher: Schlüssel = Element-ID, Wert = das Element selbst
    private final Map<Long, CatalogItem> cache = new HashMap<>();

    CatalogItem get(Long id, Supplier<CatalogItem> loader) {
        // Wenn kein Eintrag vorhanden (Miss) – rufe den Loader auf (Zugriff auf primären Speicher) und lege das Ergebnis in den Cache
        // Wenn Eintrag vorhanden (Hit) – gib ihn zurück, ohne den Loader aufzurufen
        return cache.computeIfAbsent(id, key -> loader.get());
    }
}

In diesem Modell ist id der Cache Key. Wenn die id noch nicht vorgekommen ist, rufen wir den loader auf, holen die Daten aus dem primären Speicher und legen sie in den Cache. Wenn sie bereits vorhanden ist, geben wir sie aus der Map zurück. Das ist „Hit/Miss“ in Reinform, ohne Bindung an Redis und Spring.

Denselben Gedanken kann man als einfachen Ablauf visualisieren:

flowchart TD
    A[Anfrage: item id=10] --> B{Im Cache?}
    B -- nein --> C[Aus PostgreSQL lesen]
    C --> D[Ergebnis in Cache legen]
    D --> E[Antwort geben]
    B -- ja --> E[Antwort aus Cache geben]

Und hier taucht eine wichtige „Anti‑Erwartung“ auf, die bei Anfängern das Bild zerstört: Die erste Anfrage ist fast immer ein Miss. Die Beschleunigung zeigt sich meist bei wiederholten Lesevorgängen. Deshalb nehmen wir im Unterrichtstag genau diese wiederholten Read‑Szenarien: damit Hit/Miss nicht nur „irgendwo in der Theorie“ sichtbar wird, sondern im Verhalten.

Dabei ist Caching nicht kostenlos. Es wirft Fragen auf: „Wann soll geleert werden?“, „Was tun bei Preisänderungen?“, „Was, wenn der Cache veraltet ist?“. Wir machen heute bewusst keinen separaten Architekturkurs daraus. Aber eine Regel sollten Sie sich schon jetzt merken: Caching beginnt am einfachsten mit einfachen, häufigen Lesevorgängen, nicht mit Operationen, die Daten aktiv ändern.

4. Erste Read‑Szenarien für den Cache

Man möchte am liebsten sofort alles cachen: die Liste, das Element, den Export, die Statistik, das Wetter in der Stadt des Autors und den Bitcoin‑Kurs. Dieses Verlangen ist normal, es heißt „Syndrom des großen Schalters: Beschleunigung einschalten“. Aber in einem echten Projekt und besonders in einem Lernprojekt darf man das nicht tun, weil man das Verständnis für Ursache‑Wirkungs‑Zusammenhänge verliert. Wir müssen den Effekt des Caches an einem verständlichen kleinen Ausschnitt sehen.

Im Container-Ready Catalog Service gibt es zwei natürliche Kandidaten, die keine Neuerfindung der Domäne erfordern und leicht mit einfachen HTTP‑Anfragen überprüft werden können. Das sind das Lesen der gesamten Artikel‑Liste und das Lesen eines Artikels anhand seiner ID. Sie sind maximal „sauber“: Es sind Read‑Operationen, die man zweimal hintereinander identisch wiederholen und ehrlich sehen kann: „Beim ersten Mal schwer, beim zweiten Mal leicht“.

Ein Mini‑Modell des wiederholten Lesens per ID sieht so aus (in der Terminologie der Service‑Schicht):

import com.example.catalog.catalog.model.CatalogItem;

// Zweimal dieselbe ID lesen: für den Cache ist das derselbe Key
CatalogItem a = queryService.findById(10L);
CatalogItem b = queryService.findById(10L); // key = 10L

// Ergebnis identisch, aber beim zweiten Aufruf erwarten wir einen "Hit"
System.out.println(a.getTitle()); // z. B.: "USB-C Cable"
System.out.println(b.getTitle()); // z. B.: "USB-C Cable"

Und für die Liste gibt es eine ähnliche Logik: Wenn die Lesemethode „dieselbe“ ist, ist der Caching‑Sinn klar – nicht dasselbe immer wieder neu berechnen.

import java.util.List;

// Wiederholung desselben Read‑Szenarios: Eine solche Anfrage hat normalerweise denselben Cache‑Key
List<CatalogItem> first = queryService.findAll();
List<CatalogItem> second = queryService.findAll(); // dasselbe Read‑Szenario erneut

Warum beschränken wir uns auf zwei Szenarien? Weil uns jetzt nicht die maximale Cache‑Abdeckung wichtig ist, sondern die kontrollierte Erweiterung des Compose‑Stacks und ein sorgfältiges Konfigurationsmodell. Wenn wir anfangen, die Hälfte der Anwendung zu cachen, verbringen wir den ganzen Tag mit Diskussionen über „warum das und jenes nicht aktualisiert wurde“ und machen den Cache versehentlich zum zentralen Thema. Wichtiger ist es, die Cache‑Schicht selbst an einem kleinen, wiederholbaren Ausschnitt zu sehen.

5. Reihenfolge beim Hinzufügen von Redis

In den ersten Schritten in der Infrastruktur gibt es eine sehr häufige Falle: „Lass uns gleich den ganzen Zoo hochfahren“. Das sieht nach Fortschritt aus: viele Container, viele Logs, das Gefühl professioneller Entwicklung. Tatsächlich ist es oft nur ein beschleunigter Weg ins Chaos. Wir bauen den Stack bewusst schichtweise auf: Jede Schicht muss verstanden sein, bevor die nächste kommt.

Wenn app + postgres instabil ist, bringt Caching keinen technischen Wert. Erstens, weil es nichts zu cachen gibt, solange die Lesevorgänge aus der Wahrheitsquelle nicht vorhersagbar funktionieren. Zweitens, weil Sie Symptome nicht ehrlich interpretieren können. Die Anwendung ist abgestürzt – liegt es an der Datenbank? An einer fehlgeschlagenen Migration? Ist Redis nicht erreichbar? Das Profil falsch? Der Port falsch? Und jeder neue Dienst fügt mindestens eine neue Problemklasse hinzu: Netzwerk, Konfiguration, Readiness.

Es gibt noch einen praktischen Punkt: Ein Cache ist nicht einfach ein „Beschleuniger“. Ein Cache ist eine Vereinbarung, dass ein Leseergebnis wiederverwendet werden kann. Wenn Sie noch keine Disziplin bei Migrationen und Reset/Recovery etabliert haben, bekommen Sie leicht eine Situation, in der ein Teil der Daten „scheinbar“ erhalten blieb (im Cache), die Datenbank aber bereits zurückgesetzt wurde, und Sie beginnen, Gespenster zu diagnostizieren. Deshalb haben wir zuerst den Lebenszyklus der Datenbank stabilisiert und fügen erst dann eine Schicht hinzu, die abgeleitete Ergebnisse speichert.

Die Reihenfolge ist nicht zufällig. Zuerst brauchen wir die Wahrheitsquelle und ihren Lebenszyklus, dann den Cache als Beschleuniger für wiederholte Lesevorgänge, und erst danach ergibt es Sinn, den Stack weiter auszubauen.

6. Redis im Compose‑Stack

Nachdem die Idee des Caches und der Grund „warum nicht früher“ klar sind, wird deutlich, dass uns im Compose keine Revolution erwartet, sondern eine sorgfältige Erweiterung: Zu unserem bereits vorhersagbaren Stand app + postgres kommt ein dritter Dienst hinzu – Redis. Das Wichtigste dabei ist, dass sich das Hauptprinzip des Kurses nicht ändert: Ein und dasselbe Application‑Image wird nur durch die Laufzeitkonfiguration unterschiedlich gestartet.

Auf der Architekturebene des Stacks sieht das sehr ruhig aus: Der Client schickt weiterhin HTTP an app, die Anwendung speichert Daten weiterhin in PostgreSQL, und Redis erscheint als eine Schicht, die manchmal schneller antwortet, weil das Ergebnis bereits „griffbereit“ liegt. Wenn Redis nicht verfügbar ist (z. B. nicht gestartet), sollte der ideale Lernservice sich vorhersagbar verhalten: entweder den Cache‑Modus nicht aktivieren oder explizit melden, dass die Cache‑Abhängigkeit nicht erfüllt ist. „Einfach irgendwie weiterlaufen“ ist ein schlechtes Szenario.

Schematisch wird unsere Welt so aussehen:

flowchart LR
    Client[HTTP‑Client] --> App[Spring Boot App]
    App -->|Source of Truth| PG[(PostgreSQL)]
    App -->|Cache Layer| R[(Redis)]

Der Nutzen dieses Schritts für den Docker‑Kurs liegt darin, dass Sie eine sehr praktische Fähigkeit trainieren: Die Umgebung erweitern, ohne den Basispfad zu zerstören. Wir bauen kein separates Image „für Redis“, schreiben keinen „Docker‑Modus des Codes“ und ersetzen nicht die Hälfte der Konfigurationen. Wir fügen eine Abhängigkeit so hinzu, wie Sie es später in einem echten Projekt tun werden: Der Dienst erscheint in Compose, die Adresse kommt über den Service‑Name, die Aktivierung erfolgt über ein Profil, die Beobachtbarkeit über Healthchecks und Logs.

Danach ist kein allgemeines Gespräch über den Nutzen des Caches mehr nötig, sondern eine ganz praktische Verkabelung: Caching in Spring minimal aktivieren und Redis als containerisierte Abhängigkeit anbinden. Das Fundament für diesen Schritt haben wir bereits: stabiler app + postgres, Readiness und ein verständlicher Lebenszyklus der Umgebung.

7. Typische Fehler beim Start des Redis‑Tages

Die ersten Versuche, Redis hinzuzufügen, gehen fast immer mit dem Gefühl einher: „Ich habe alles nach Anleitung gemacht, aber es wurde nicht magisch schneller“. Das ist normal: Ein Cache ist etwas, das man beobachten muss, nicht einfach „daran glauben“. Wenn man vorher versteht, warum Redis genau jetzt hinzugefügt wird und welche Rolle es spielt, kann man die meisten Fallen ganz ohne Schmerz umgehen (oder zumindest in weichen Hausschuhen darauf treten).

Fehler Nr. 1: Redis hinzufügen, wenn app + postgres noch instabil ist.
Wenn Ihr Basisstack nur jedes zweite Mal hochkommt, dann beschleunigt Redis nichts, außer dem Auftauchen neuer Fehler. In einer solchen Situation reparieren Sie gleichzeitig die Readiness der Datenbank, Migrationen und die Verbindung zu Redis, und Ihr Gehirn wechselt schnell in den Modus „Ich verstehe gar nichts“. Die richtige Reihenfolge ist: Zuerst einen wiederholbaren Start von app + postgres erreichen, dann erweitern.

Fehler Nr. 2: Glauben, dass Redis PostgreSQL ersetzt.
In unserem Tag ist Redis ein Cache, nicht die Wahrheitsquelle. Wenn Sie anfangen, es wie eine „zweite Datenbank“ zu behandeln, werden Sie sehr schnell darauf stoßen, dass Daten „nicht stimmen“, „verschwunden sind“ oder „nicht aktualisiert wurden“. Die Wahrheitsquelle bleibt PostgreSQL, Redis speichert ein abgeleitetes Leseergebnis, das neu erstellt werden kann.

Fehler Nr. 3: Versuchen, alles auf einmal zu cachen, um „den Effekt genau zu spüren“.
Je breiter Sie das Caching verteilen, desto schwieriger wird es zu verstehen, was Sie eigentlich beobachten. In einem Lernprojekt ist das besonders kritisch: Wir wollen Hit/Miss an genau zwei konkreten Read‑Szenarien sehen, nicht einen Minikurs über Cache‑Invalidierung und Datenkonsistenz aufbauen.

Fehler Nr. 4: Einen Cache Hit erwarten, aber unterschiedliche Abfragen testen.
Wenn Sie einmal id=10 gelesen haben und ein zweites Mal id=11, sind das zwei verschiedene Schlüssel, und einen ehrlichen Hit gibt es nicht. Genauso kann man nicht „erste Anfrage Liste ohne Filter, zweite Anfrage mit Filter“ vergleichen und sich wundern, dass der Cache nicht angesprochen hat. Für einen Smoke‑Test brauchen Sie denselben wiederholten Read.

Fehler Nr. 5: Eine Beschleunigung bei der ersten Anfrage erwarten.
Die erste Anfrage ist meistens ein Miss: Man muss zu PostgreSQL gehen und erst dann das Ergebnis speichern. Wenn Sie den Cache am „Geschwindigkeitsgefühl“ beim ersten Aufruf messen, werden Sie mit hoher Wahrscheinlichkeit eine falsche Schlussfolgerung ziehen. Der Cache zeigt sich bei wiederholten Lesevorgängen, und das ist normal: Er sagt nicht die Zukunft voraus, er erinnert sich an die Vergangenheit.

1
Aufgabe
Docker for Spring, Level 19, Lektion 0
Gesperrt
Wiederholtes Lesen aus PostgreSQL ohne Cache
Wiederholtes Lesen aus PostgreSQL ohne Cache
1
Aufgabe
Docker for Spring, Level 19, Lektion 0
Gesperrt
Zwei wiederholbare Read-Szenarien vor dem Erscheinen von Redis
Zwei wiederholbare Read-Szenarien vor dem Erscheinen von Redis
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION