Zurück zur Startseite
Leistungen

Backend Engineering, Infrastruktur

Branche

B2B-Marktplatz

Jahr

2021-2024

Ein Cache für zweihundert Unternehmensprofile

Das Ticket lautete: 'Ich habe meine Beschreibung vor 10 Minuten aktualisiert und es zeigt immer noch die alte.' Es kam drei- bis viermal pro Woche bei einem B2B-Marktplatz an, der mehr als 200 Unternehmensprofile aus einer Django-Instanz ausliefert. Jedes Profil ist öffentlich, wird ständig besucht und vom Unternehmen selbst bearbeitet. Caching war Pflicht. Ihm zu vertrauen war das Problem.

DIE HERAUSFORDERUNG

Zwei Arten, falsch zu liegen

Ohne Cache kostete jeder Profilaufruf acht bis zwölf Datenbank-Queries. Der erste Cache nutzte Djangos Per-View-Cache mit der URL als Schlüssel und hielt, bis zwei Unternehmen ihre Profile im selben TTL-Fenster bearbeiteten. Das Speichern von Unternehmen A invalidierte den Cache, aber viel zu breit: Es warf die Seiten von B, C und jedem anderen Mandanten mit raus, und alle 200 Profile waren auf einen Schlag kalt. Die Notlösung, längere TTLs mit manueller Invalidierung, war auf andere Weise schlimmer. Editoren speicherten, luden neu und sahen ihren alten Text. Da fingen die Tickets an.

DIE LÖSUNG

Der Schlüssel trägt die Version

Jedes Profil liegt unter einem zusammengesetzten Schlüssel im Cache: Unternehmens-Slug, Locale und ein acht Zeichen langer Hash des last_modified-Zeitstempels. Django aktualisiert diesen Zeitstempel bei jedem Speichern, also berechnet der nächste Request einen neuen Hash und einen neuen Schlüssel. Der alte Eintrag wird nie gelöscht. Er wird unerreichbar, weil niemand mehr nach diesem Schlüssel fragt, und die TTL des Caches räumt ihn später weg. Es gibt keinen Invalidierungscode, keinen cache.delete()-Aufruf und kein Rennen zwischen dem Löschen der alten und dem Schreiben der neuen Seite. Die Alternative auf dem Tisch war Tag-basierte Invalidierung: ein sekundärer Index, welche Schlüssel zu welchem Mandanten gehören, und beim Speichern löschen. Das hätte eine Struktur gebracht, die mit dem Cache synchron bleiben muss, einen Aufräumjob für veraltete Tags und einen neuen Fehlerfall, in dem Index und Cache sich widersprechen. Der Versionsschlüssel ist zustandslos. Der Preis sind ein paar verwaiste Einträge, die auf ihre TTL warten.

Der Generator für den zusammengesetzten Schlüssel:

Python
import hashlib

def company_cache_key(company, locale):
    """Composite cache key with version hash.

    The hash changes whenever the company saves any field,
    because Django auto-updates last_modified.
    No explicit invalidation needed.
    """
    ts = company.last_modified.isoformat()
    version = hashlib.md5(ts.encode()).hexdigest()[:8]
    return f"company:{company.slug}:{locale}:{version}"

Einen Mandanten bearbeiten, die anderen 23 beobachten

Jede Kachel ist eine Unternehmensseite; Traffic trifft sie (grün) oder verfehlt sie (amber). Klick auf eine Kachel, um eine Bearbeitung zu speichern, und schau auf das Schlüssel-Panel: Der Zeitstempel ändert sich, der Hash ändert sich, und nur diese Kachel wird kalt. Wechsle auf den URL-Schlüssel des ersten Versuchs, um zu sehen, was ein einziges Speichern mit allen anderen gemacht hat.

Schlüssel-Strategie
  • Hit
  • Miss
  • Gespeichert, neuer Schlüssel

Mandanten-Seiten

Ausgewählter Mandantacme-gmbhv1
last_modified
2024-03-14T09:12:05
Cache-Schlüssel
company:acme-gmbh:de:f14c0a46

Der Hash ist der last_modified-Zeitstempel. Neuer Zeitstempel, neuer Schlüssel, und den alten fragt niemand mehr an.

Bereit
Requests
60
Hits
42
Misses
18
Hit Rate
70 %
Warme Einträge
18 / 24
Verwaiste Schlüssel
0

Noch keine Bearbeitung gespeichert.

Aufwändige Abschnitte wie Galerien und Karteneinbettungen werden als Fragmente mit eigenen mandantenspezifischen Schlüsseln gecacht. Eine Textänderung lässt die Galerie im Cache, was die Datenbanklast gegenüber reinem Seiten-Caching um 60 % gesenkt hat:

HTML
{% load cache %}

{# Gallery fragment: cached independently #}
{# Uses gallery_modified, not company.last_modified #}
{% cache 3600 gallery company.slug gallery_hash %}
  {% for image in company.gallery_images.all %}
    <img src="{{ image.url }}" alt="{{ image.alt }}" loading="lazy">
  {% endfor %}
{% endcache %}

{# Map embed: rarely changes, long TTL #}
{% cache 86400 map company.slug company.address_hash %}
  {% include "company/_map_embed.html" %}
{% endcache %}
DAS ERGEBNIS

Die Editoren hörten auf, Tickets zu schreiben

Ein Editor speichert jetzt, lädt neu und sieht die Änderung, weil das Speichern selbst einen neuen Schlüssel erzeugt hat. Die Tickets wegen veralteter Inhalte blieben aus. Besucher bekommen ein gecachtes Profil in etwa einem Dreißigstel der ungecachten Zeit. In 18 Monaten Produktion gab es keinen Vorfall mit veralteten Daten und keine Seite, die an den falschen Mandanten ging. Der ganze Cache, rund 800 Einträge für 200 Unternehmen, zwei Locales und zwei Fragmenttypen, belegt unter 40 MB.

WICHTIGE KENNZAHLEN

12msØ Ladezeit, gecachte Seite
94%Cache Hit Rate nach dem Aufwärmen
1DB-Queries pro gecachter Seite
KUNDENSTIMME

Editoren speichern, laden neu und sehen ihre Änderung. Die Tickets wegen veralteter Inhalte, früher drei- bis viermal pro Woche, sind ausgeblieben. Seitdem hat niemand im Team den Invalidierungscode angefasst, weil es keinen gibt.

Technical Lead

B2B-Marktplatz, Platform-Team

FÜR DEIN PROJEKT

Wann es passt
Viele Mandanten teilen sich einen Cache und pflegen ihre eigenen Inhalte: Unternehmensprofile, Händlerseiten, Portale. Einzige Voraussetzung ist eine last_modified-Spalte, die das ORM bei jedem Speichern aktualisiert.
Was du prüfen solltest
Jeder Input, der die gerenderte Seite verändert, gehört in den Schlüssel. Hier fehlte anfangs die Locale, und das deutsche Profil eines Unternehmens ging an englischsprachige Besucher, bis das Locale-Segment ergänzt war. Fragmente mit eigenem Änderungsdatum brauchen eigene Schlüssel.
Was es braucht
Das Cache-Backend, das du schon betreibst, eine indizierte Zeitstempel-Spalte und TTL-Spielraum für verwaiste Einträge. Hier sind das rund 800 aktive Einträge in unter 40 MB eines 256-MB-Caches. Kein sekundärer Index, kein Aufräumjob.

FAQ

TECHNOLOGIE-STACK

Django
Python
PostgreSQL

Daniel hat bei der Gründung und Umsetzung der Unternehmen Solvster und VIARES einen entscheidenden Beitrag geleistet. Er war verantwortlich für den gesamten technischen Aufbau der Onlinepräsenz, inklusive der Serversetups, der Entwicklung und Optimierung der Website sowie der Sicherstellung der Funktionalität. Besonders beeindruckt hat mich, wie proaktiv und lösungsorientiert er immer an Verbesserungen gearbeitet hat. Daniel war stets sehr responsiv und zuverlässig - es war eine Freude, mit ihm zusammenzuarbeiten. Seine Expertise und sein Engagement haben maßgeblich zum Erfolg beider Projekte beigetragen!

Dietmar Eglhofer - Geschäftsführer, Solvster & VIARES

Schließe dich Dietmar an und starte dein nächstes Projekt mit Zuversicht.

Schreibe mirSchreibe mir