Ein Cache Stampede entsteht, wenn viele gleichzeitige Requests denselben abgelaufenen Cache-Key verfehlen und die Daten parallel neu berechnen. Dieser Laravel-Leitfaden zeigt Gegenmaßnahmen mit atomaren Locks, stale-while-revalidate, TTL-Jitter und sicheren Fallbacks.
Ein Cache Stampede entsteht, wenn viele gleichzeitige Requests feststellen, dass derselbe häufig gelesene Cache-Eintrag fehlt oder abgelaufen ist, und alle versuchen, ihn gleichzeitig neu zu erzeugen. Statt die Datenbank zu schützen, leitet der Cache plötzlich eine Welle doppelter Arbeit an sie weiter. Das Problem wird auch Thundering Herd oder Dog-Pile-Effekt genannt.
Stellen Sie sich vor, eine Laravel-Anwendung speichert die Abfrage „meistverkaufte Produkte“ zehn Minuten lang. Solange der Key existiert, erhalten Tausende Requests schnell das Ergebnis. Läuft der Key ab und treffen 200 Requests gleichzeitig ein, sehen alle einen Cache Miss, führen dieselbe Abfrage aus und schreiben dasselbe Ergebnis zurück. Eine Operation, die einmal laufen sollte, wird 200-mal ausgeführt.
Diese Lastspitze kann Datenbankverbindungen erschöpfen, CPU belasten, Latenzen erhöhen und Timeouts in abhängigen Diensten auslösen. Langsame Antworten halten mehr Requests offen, erhöhen die Parallelität und können einen einzigen abgelaufenen Key in einen kaskadierenden Ausfall verwandeln.
Cache::remember ist für gewöhnliches Caching ein hervorragender Standard:
$products = Cache::remember('products:top', 600, function () {
return Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get();
});Laravel prüft den Key, führt die Closure bei einem Miss aus, speichert das Ergebnis und gibt es zurück. Lesen und Neuberechnen bilden jedoch keine atomare Operation. Wenn mehrere PHP-Worker den Miss sehen, bevor der erste die Abfrage beendet, können alle in die Closure eintreten. Bei wenig Traffic ist der Code korrekt, bei hoher Parallelität bleibt ein heißer, ablaufender Key gefährdet.
Ein heißer Key läuft ab: Ein stark gelesener Eintrag erreicht seinen TTL während einer Lastspitze.
Viele Keys teilen denselben TTL: Ein gemeinsames Aufwärmen lässt Hunderte Einträge gleichzeitig ablaufen.
Der Cache wird geleert: Deployment oder Wartung entfernt viele beliebte Keys auf einmal.
Kaltstart: Neue Instanzen erhalten Traffic, bevor wichtige Werte vorgewärmt sind.
Langsame Regeneration: Teure SQL-Abfragen oder APIs geben weiteren Requests Zeit, sich anzusammeln.
Ausfall des Cache-Stores: Eine Redis- oder Memcached-Störung verschiebt mehr Arbeit zum Ursprung, als dieser bewältigen kann.
Das Ziel ist Request Coalescing: Nur ein Request berechnet den Wert neu, während andere kurz warten oder einen Fallback verwenden. Laravel stellt verteilte atomare Locks über Cache::lock bereit.
use Illuminate\Support\Facades\Cache;
function topProducts()
{
$key = 'products:top';
if ($cached = Cache::get($key)) {
return $cached;
}
return Cache::lock("lock:{$key}", 15)->block(5, function () use ($key) {
// Ein anderer Worker könnte den Key während des Wartens gefüllt haben.
return Cache::get($key) ?? tap(
Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get(),
fn ($products) => Cache::put($key, $products, now()->addMinutes(10))
);
});
}Die zweite Cache-Prüfung innerhalb des Locks ist entscheidend. Ohne sie würde jeder wartende Request das Lock nacheinander erhalten und den Wert erneut berechnen, obwohl der erste ihn bereits aktualisiert hat. Dieses Muster heißt Double-Checked Locking.
Die Lock-Laufzeit sollte länger als die normale Berechnung dauern, aber begrenzt bleiben, falls ein Worker abstürzt. Setzen Sie mit block außerdem eine kurze Wartezeit. Bei LockTimeoutException sollte die Anwendung alte Daten, eine reduzierte Antwort oder einen kontrollierten Fehler zurückgeben.
Wenn leicht veraltete Daten akzeptabel sind, bietet stale-while-revalidate eine gute Nutzererfahrung. Laravels Cache::flexible definiert ein frisches und ein veraltetes Zeitfenster:
$products = Cache::flexible(
'products:top',
[300, 900],
fn () => Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get()
);In den ersten 300 Sekunden liefert Laravel den frischen Wert. Zwischen 300 und 900 Sekunden wird der alte Wert sofort zurückgegeben und nach der Response eine verzögerte Aktualisierung registriert. Nach 900 Sekunden ist der Wert vollständig abgelaufen und wird synchron berechnet.
Dieses Verfahren hält die Latenz vorhersehbar und eignet sich für Artikellisten, Rankings, Dashboards, Empfehlungen und andere Daten, bei denen wenige Minuten Verzögerung unkritisch sind.
Werden viele Keys gemeinsam mit derselben Laufzeit geschrieben, können sie gemeinsam ablaufen. Fügen Sie einen kleinen Zufallswert hinzu, um Aktualisierungen zeitlich zu verteilen:
$ttl = now()->addSeconds(600 + random_int(0, 120));
Cache::put("product:{$product->id}", $payload, $ttl);TTL-Jitter verhindert nicht allein, dass mehrere Requests um einen einzelnen heißen Key konkurrieren. Er ergänzt Locks und stale-while-revalidate, indem er eine synchronisierte Ablaufwelle vieler Keys verhindert.
Systeme mit hohem Traffic kombinieren häufig einen normalen Key, einen länger gültigen stale Key und ein atomare Lock:
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
function dashboardStats(): array
{
$key = 'dashboard:stats';
$staleKey = 'dashboard:stats:stale';
if ($value = Cache::get($key)) {
return $value;
}
try {
return Cache::lock("lock:{$key}", 20)->block(2, function () use ($key, $staleKey) {
if ($value = Cache::get($key)) {
return $value;
}
$value = app(StatsService::class)->calculate();
$freshTtl = now()->addSeconds(300 + random_int(0, 60));
Cache::put($key, $value, $freshTtl);
Cache::put($staleKey, $value, now()->addHours(2));
return $value;
});
} catch (LockTimeoutException) {
return Cache::get($staleKey, ['status' => 'temporarily_unavailable']);
}
}Ein Worker berechnet die Daten; andere warten kurz oder erhalten die ältere Kopie. Passen Sie Zeiten an Abfragekosten, zulässige Veralterung, Traffic und Fehlerverhalten des Stores an.
Alle Anwendungsserver müssen einen gemeinsamen Cache-Store verwenden, der atomare Locks unterstützt. Laravel unterstützt unter anderem Redis, Memcached, DynamoDB, Datenbank und Datei; verteilte Server müssen jedoch denselben zentralen Store verwenden.
Verwenden Sie pro Ressource, Tenant, Sprache und Filterkombination einen eindeutigen Lock-Namen.
Begrenzen Sie die Lease, damit das System sich von abgestürzten Workern erholt.
Prüfen Sie, ob Datenbank-Locks den bereits überlasteten Engpass verschärfen.
Nutzen Sie wenn möglich die Closure-Form, die das Lock automatisch freigibt.
Behandeln Sie leere Ergebnisse bewusst; verwenden Sie Cache::has oder einen Sentinel.
Vermeiden Sie ein globales Lock, das unabhängige Arbeit serialisiert.
Achten Sie auf periodische Spitzen an TTL-Grenzen: einen plötzlichen Einbruch der Hit Rate, viele identische SQL-Abfragen, steigende Verbindungen, Lock-Contention und erhöhte p95- oder p99-Latenz. Messen Sie die Dauer der teuren Closure, Wartezeiten, Lock-Timeouts und ob ein stale Wert geliefert wurde.
Ein einfacher Lasttest löscht einen heißen Key und sendet gleichzeitig viele Requests. Funktioniert der Schutz, führt nur ein Request die teure Operation aus; die übrigen warten oder erhalten den Fallback.
Verwenden Sie Cache::remember für günstige Keys mit geringer Parallelität.
Verwenden Sie Cache::lock, wenn die Berechnung nur einmal laufen darf.
Verwenden Sie Cache::flexible, wenn leicht veraltete Daten akzeptabel sind.
Fügen Sie TTL-Jitter hinzu, wenn viele Keys gemeinsam geschrieben werden.
Wärmen Sie wenige kritische und vorhersehbare heiße Keys proaktiv auf.
Kombinieren Sie Locks, stale Fallback, Jitter und Monitoring auf stark belasteten Pfaden.
Nein. Ein einzelner Miss ist normal. Ein Stampede entsteht, wenn viele Requests denselben Eintrag verfehlen und die teure Arbeit gleichzeitig wiederholen.
Nein. Redis bietet schnellen Zugriff und Lock-Primitiven, doch die Anwendung benötigt weiterhin eine Regenerationsstrategie.
Nein. Locks verursachen Koordination und zusätzliche Latenz. Schützen Sie heiße Keys, deren Berechnung das Backend unter Parallelität gefährden kann.
Wenn stale Daten zulässig sind, beginnen Sie mit Cache::flexible. Muss die Berechnung einmalig sein, schützen Sie den Miss-Pfad mit Cache::lock und prüfen den Key innerhalb des Locks erneut.
Ein Cache Stampede ist ein Parallelitätsproblem, das wie ein Cache-Problem aussieht. Ein längerer TTL allein genügt nicht. Koordinieren Sie die Regeneration, liefern Sie wenn möglich alte Daten, verteilen Sie Ablaufzeiten und messen Sie das Verhalten beim Verschwinden eines heißen Keys. Laravel bietet mit Cache::lock, Cache::flexible und seinen Cache-Treibern die nötigen Werkzeuge, damit ein abgelaufener Key nicht zum Produktionsvorfall wird.
Noch keine freigegebenen Kommentare sichtbar. Neue Antworten können moderiert werden.