Fyra-stegs-mönstret
Defekten finns i fyra sekventiella, icke-atomära steg:
// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Steg 1: kontrollera cache. Steg 2: miss. Steg 3: beräkna. Steg 4: lagra. Alla fyra steg: icke-atomära. Mellan steg 1 och steg 4 kan valfritt antal trådar utföra steg 1 och alla ser null.
The Idempotency Trap
The reasoning that protects this pattern: 'it is OK if two threads compute & store the same value. The result is idempotent. No data corruption occurs.'
This reasoning: correct about correctness. Fatal about cost.
At 1,000 threads on a cache miss: 1,000 threads each execute expensiveCompute(key). If expensiveCompute queries a database, 1,000 database queries fire simultaneously. If it calls an external service, 1,000 HTTP requests fire simultaneously. The system producing correct results collapses under the cost of producing them.
Three Triggers
A Thundering Herd fires when a cache key transitions from warm to cold simultaneously across many threads:
Cold start: service restarts with an empty cache. First request wave: every key misses. All compute simultaneously.
Service restart: rolling restart resets cache across instances. Traffic redistributes to cold instances.
TTL expiry: a high-traffic key expires. N threads all check, all miss, all compute before the first thread stores the result.
Alla tre triggers: korrelerade med trafiktoppar. Herden aktiveras när trafiken når sin topp och cachen är kall. Värsta tänkbara tillfälle.
Elasticsearch Exempel med EnrichCache
Elasticsearch EnrichCache: dokumenterad kommentar lyder 'intentionally non-locking for simplicity...OK if we re-put the same key/value in a race condition.' Vid 10 000 dokument per sekund med en kall enrich-cache: alla 10 000 förfrågningar träffar enrich-indexet samtidigt. Enrich-indexet, som är designat för sporadiska uppslag, utsätts för 10 000 samtidiga frågor. Klustret blir instabilt.
Idempotensresonemanget: korrekt i kodkommentaren. Katastrofalt vid 10 000 dokument per sekund.
Koppling till MOAD-0001
MOAD-0001 (Sedimentary Defect) skapar en O(N²)-flaskhals i system med hög genomströmning. Att åtgärda MOAD-0001 (O(N²) till O(N)) frigör arbetsstationen. Den högre genomströmningen skickar fler förfrågningar vidare. Nedströms-cacher, som tidigare skyddats av MOAD-0001-flaskhalsen, får nu korrelerade trafiktoppar. MOAD-0005 aktiveras i cacher som aldrig tidigare drabbats. Åtgärda ett MOAD; skapa förutsättningar för det andra.
Vad Idempotensfällan får fel
Elasticsearch-kommentaren representerar noggrant ingenjörsresonemang tillämpat på fel fråga. Idempotens: en verklig egenskap värd att resonera kring. Fällan: att stanna analysen vid korrekthet utan att fortsätta till kostnad.
computeIfAbsent & singleflight
Lösningen: gör check-and-compute atomär. En tråd beräknar. Alla andra trådar väntar på det resultatet.
Java: computeIfAbsent
// DEFECT: fyra icke-atomära steg
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// FIX: atomisk kontroll-och-beräkning
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: om nyckeln saknas, beräknas exakt en gång, lagras och returneras. Alla andra trådar som anropar computeIfAbsent för samma nyckel väntar på den första beräkningen. Ingen N-faldig beräkning. Ingen stampede.
Go: singleflight.Group
var g singleflight.Group
func getOrCompute(key string) (Value, error) {
v, err, _ := g.Do(key, func() (interface{}, error) {
return expensiveCompute(key)
})
return v.(Value), err
}
singleflight: om en beräkning för en nyckel redan körs, väntar alla anropare för samma nyckel och delar det enda resultatet. En beräkning, N väntare, ett resultat delas. 'flight'-abstraktionen: deduplicera pågående förfrågningar.
Lås vs singleflight
Ett naivt per-nyckel-lås serialiserar: tråd 1 beräknar, tråd 2 väntar, tråd 3 väntar. När tråd 1 är klar går tråd 2 in och kontrollerar cachen (träff). Tråd 3 går in och kontrollerar cachen (träff). N-1 låsförvärv och cacheläsningar.
singleflight deduplicerar: tråd 1 beräknar, tråd 2 till N väntar alla på tråd 1:s resultat. Inga separata låsförvärv. Inga separata cacheläsningar. En beräkning, ett resultat, distribuerat till N väntare. Färre operationer än ett per-nyckel-lås.
Båda förhindrar stampede. singleflight förhindrar redundant arbete mer fullständigt.
Skriv om mönstret
Tillämpa fixen på ett konkret scenario.
// En användarprofil-cache i en Java-tjänst med hög trafik
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // dyrt: 50 ms DB-fråga
profileCache.put(userId, profile);
}
return profile;
}
Tjänsten startar om varje morgon kl. 02:00. Klockan 08:00 begär 10 000 användare sina profiler samtidigt.