Djangos Cache-Backends werfen zuerst die Einträge raus, die am längsten nicht abgerufen wurden. Beliebte Profile werden oft angefragt und bleiben warm; selten besuchte fliegen raus und werden beim nächsten Request neu aufgebaut. Das Working Set liegt hier bei rund 40 MB in einem 256-MB-Cache, der Verdrängungsdruck ist bei normalem Traffic minimal.
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
Dietmar Eglhofer - Geschäftsführer, Solvster & VIARESDaniel 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!
Schließe dich Dietmar an und starte dein nächstes Projekt mit Zuversicht.
Frei für neue ProjekteSchreibe mir