Bootcamp i backendutvikling med Node.js · leksjon

Forebygging av cache-stampeder og «thundering herds»

Begrens stampeder med sammenslåing av forespørsler, TTL-er med jitter og sannsynlighetsbasert tidlig utløp.

Leksjon 4 av 413 trinn

Forebygging av cache-stampeder og «thundering herds» er en gratis leksjon i Bootcamp i backendutvikling med Node.js på CoddyKit. Dette er leksjon 4 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Bootcamp i backendutvikling med Node.js, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Hva er en hurtigbufferstorm?

En hurtigbufferstorm (også kalt en thundering herd) oppstår når en populær bufret verdi utløper og mange samtidige forespørsler får treff på hurtigbufferen samtidig. Alle skynder seg til databasen for å beregne den samme verdien på nytt.

  • Én nøkkel utløper → 5 000 pågående forespørsler ser alle et hurtigbuffertreff som uteblir
  • 5 000 identiske spørringer treffer databasen samtidig
  • Databasen blir overbelastet, ventetiden øker kraftig, og noen ganger bryter hele systemet sammen

Ironien er at hurtigbufferen finnes for å beskytte databasen, men utløpsøyeblikket blir det farligste tidspunktet av alle.

Se problemet i kode

Her er en klassisk naiv cache-aside-lesing. Den fungerer fint ved lav trafikk, men ved høy samtidighet starter hvert eneste manglende treff sitt eget kall til loadFromDb.

Legg merke til at det ikke finnes noe som hindrer 1 000 kallere i å kjøre loadFromDb samtidig for den samme nøkkelen.

async function getUser(redis, db, id) {
  const key = 'user:' + id;
  const cached = await redis.get(key);
  if (cached !== null) {
    return JSON.parse(cached);
  }
  // STAMPEDE RISK: every concurrent miss runs this
  const user = await db.loadUser(id);
  await redis.set(key, JSON.stringify(user), 'EX', 60);
  return user;
}

Strategi 1: Samordning av forespørsler

Samordning av forespørsler (også kalt single-flight eller deduplisering av pågående forespørsler) betyr at når mange kallere ønsker den samme nøkkelen samtidig, er det bare én av dem som faktisk beregner verdien. Resten venter på det samme løftet.

  • Oppretthold et minnekart med key → pending promise
  • Den første kalleren starter arbeidet og lagrer løftet
  • Samtidige kallere finner det ventende løftet og venter på det
  • Når det er ferdig, slettes oppføringen

Dette reduserer N databasekall til 1 per prosess.

Implementere Single-Flight

En minimal single-flight-hjelper uten avhengigheter. Alle samtidige kallere for den samme nøkkelen deler ett underliggende løfte. finally-blokken fjerner oppføringen, slik at neste runde beregner verdien på nytt.

Dette kodeeksempelet er fullstendig selvstendig og demonstrerer samordningsatferden med simulert langsomt arbeid.

function createSingleFlight() {
  const inFlight = new Map();
  return function run(key, fn) {
    if (inFlight.has(key)) {
      return inFlight.get(key);
    }
    const p = Promise.resolve()
      .then(fn)
      .finally(() => inFlight.delete(key));
    inFlight.set(key, p);
    return p;
  };
}

async function main() {
  const flight = createSingleFlight();
  let dbCalls = 0;
  const load = () => new Promise(res => {
    dbCalls++;
    setTimeout(() => res('value-' + dbCalls), 50);
  });

  // 5 concurrent callers, same key
  const results = await Promise.all(
    Array.from({ length: 5 }, () => flight('user:42', load))
  );
  console.log('results:', results);
  console.log('actual db calls:', dbCalls);
}

main();

Samordningens begrensning: Den gjelder per prosess

Single-flight i minnet dedupliserer bare innenfor én Node.js-prosess. Hvis De kjører 20 instanser bak en lastbalanserer, kan De fortsatt få opptil 20 samtidige databasekall per utløpt nøkkel — ett per prosess.

  • Utmerket for å redusere N forespørsler → 1 inne i hver instans
  • Ikke tilstrekkelig alene for store horisontalt skalerte flåter
  • For beskyttelse på tvers av prosesser trenger De en distribuert lås i Redis

Kombiner samordning (billig, lokal) med distribuert låsing eller probabilistisk utløp (for hele klyngen) for full dekning.

Strategi 2: Distribuert lås

En distribuert lås lar én enkelt instans (på tvers av hele flåten) få retten til å beregne verdien på nytt. Bruk Redis SET key value NX PX ttl: kommandoen setter nøkkelen bare hvis den ikke finnes, og gjør dette atomisk.

  • Vinneren beregner verdien på nytt og fyller hurtigbufferen
  • Taperne venter og prøver hurtigbufferen på nytt, eller leverer foreldede data
  • Sett alltid en TTL på låsen, slik at en krasjet vinner ikke kan låse nøkkelen for alltid

Dette er motstykket på tvers av prosesser til samordning i prosessen.

async function getWithLock(redis, db, id) {
  const key = 'user:' + id;
  const cached = await redis.get(key);
  if (cached !== null) return JSON.parse(cached);

  const lockKey = 'lock:' + key;
  const token = Math.random().toString(36).slice(2);
  // NX = only if absent, PX = lock TTL in ms
  const won = await redis.set(lockKey, token, 'NX', 'PX', 5000);

  if (won === 'OK') {
    try {
      const user = await db.loadUser(id);
      await redis.set(key, JSON.stringify(user), 'EX', 60);
      return user;
    } finally {
      // release only if we still own the lock
      if (await redis.get(lockKey) === token) await redis.del(lockKey);
    }
  }
  // Lost the race: briefly wait, then read the now-fresh cache
  await new Promise(r => setTimeout(r, 50));
  const retry = await redis.get(key);
  return retry !== null ? JSON.parse(retry) : db.loadUser(id);
}

Strategi 3: TTL med tilfeldig variasjon

Hvis De varmer opp 10 000 nøkler i en løkke med samme TTL, utløper de alle i samme sekund — en synkronisert storm på tvers av mange nøkler samtidig. TTL-variasjon sprer utløpstidspunktene ved å legge til et lite tilfeldig avvik på hver TTL.

  • Grunnleggende TTL på 300 s → faktisk TTL på 270–330 s, randomisert per nøkkel
  • Utløpstidspunktene spres over et tidsvindu i stedet for å samles på samme tidspunkt
  • Billig, krever ingen koordinering og kan kombineres med alle andre strategier

Bruk alltid tilfeldig variasjon på TTL-er når De fyller eller oppdaterer mange relaterte nøkler samtidig.

// Add +/- jitterPct random spread around a base TTL
function jitteredTtl(baseSeconds, jitterPct = 0.1) {
  const spread = baseSeconds * jitterPct;
  const offset = (Math.random() * 2 - 1) * spread; // -spread..+spread
  return Math.max(1, Math.round(baseSeconds + offset));
}

// Demo: 5 keys warmed together get different lifetimes
for (let i = 0; i < 5; i++) {
  console.log('key' + i + ' ttl =', jitteredTtl(300));
}

Strategi 4: Probabilistisk tidlig utløp

Probabilistisk tidlig utløp (XFetch-algoritmen) oppdaterer en nøkkel før den faktisk utløper, med en sannsynlighet som øker når utløpet nærmer seg. Dermed beregner én heldig forespørsel verdien på nytt tidlig, mens den gamle verdien fortsatt leveres til alle andre.

Den klassiske regelen beregner verdien på nytt når:

  • now - delta * beta * ln(random()) ≥ expiry

Her angir delta hvor lang tid den forrige omberegningen tok, og beta (standardverdi 1) justerer hvor aggressiv strategien er. Nøkler som tar lang tid å beregne (stor delta) begynner å oppdateres tidligere, noe som er akkurat det De ønsker.

XFetch i kode

For å bruke XFetch lagrer De, sammen med verdien, varigheten av omberegningen (delta) og det absolutte utløpstidspunktet. Ved hver lesing utfører De den probabilistiske kontrollen. De fleste kallere leverer den bufrede verdien; av og til oppdaterer én av dem verdien før den utløper.

Denne frittstående demonstrasjonen viser at sannsynligheten for tidlig oppdatering nærmer seg 1 når utløpet nærmer seg.

function shouldRecompute(deltaMs, expiryMs, now, beta = 1) {
  // XFetch: earlier refresh as we near expiry, scaled by recompute cost
  const xfetch = now - deltaMs * beta * Math.log(Math.random());
  return xfetch >= expiryMs;
}

const now = Date.now();
const delta = 200;          // last recompute took 200ms
const expiry = now + 1000;  // value expires in 1s

let refreshes = 0;
for (let i = 0; i < 1000; i++) {
  // sample 'now' uniformly across the key's lifetime
  const t = now + Math.random() * 1000;
  if (shouldRecompute(delta, expiry, t)) refreshes++;
}
console.log('early refreshes out of 1000 reads:', refreshes);

Kombinere strategiene

Disse teknikkene er komplementære lag, ikke konkurrenter. En hurtigbufferlesing i produksjon kombinerer ofte flere av dem:

  • Samordning — samler duplikatert arbeid i hver prosess
  • Distribuert lås — én omberegning på tvers av hele flåten
  • TTL med tilfeldig variasjon — desynkroniserer masseutløp
  • Probabilistisk tidlig utløp — oppdaterer ofte brukte nøkler før de i det hele tatt mangler

Start med tilfeldig variasjon + samordning (billig og uten koordinering). Legg til en lås eller XFetch for de mest brukte og kostbare nøklene.

Stale-While-Revalidate

Et praktisk mønster som samler dette: stale-while-revalidate (SWR). Oppretthold to levetider — et kort ferskt vindu og et lengre foreldet vindu. Når dataene er foreldet, leverer De umiddelbart den gamle verdien og starter en bakgrunnsoppdatering (deduplisert med single-flight).

  • Brukerne må nesten aldri vente på en ny beregning utenfor hurtigbufferen
  • Oppdateringen kjører én gang, utenfor forespørselens kritiske bane
  • Kombiner med tilfeldig variasjon, slik at foreldede vinduer ikke avsluttes samtidig

SWR gjør et hardt manglende treff (alle venter) om til et mykt manglende treff (én bakgrunnsoppdatering, alle får svar umiddelbart).

async function swrGet(redis, db, id, freshSec = 60, staleSec = 600) {
  const key = 'user:' + id;
  const raw = await redis.get(key);
  if (raw) {
    const { value, storedAt } = JSON.parse(raw);
    const ageSec = (Date.now() - storedAt) / 1000;
    if (ageSec > freshSec) {
      // stale but usable: refresh in background, serve now
      refreshInBackground(redis, db, id, key, staleSec);
    }
    return value;
  }
  return refreshInBackground(redis, db, id, key, staleSec);
}

Hurtigsjekk

De kjører 30 Node.js-instanser bak en lastbalanserer. Én svært mye brukt nøkkel utløper, og De må garantere at databasen mottar høyst én spørring for å beregne den på nytt. Hvilken tilnærming oppnår dette?

Oppsummering

De har lært hvordan De kan beskytte mot hurtigbufferstormer og thundering herds i Node.js:

  • Samordning av forespørsler samler samtidig duplikatarbeid i ett løfte — men bare per prosess.
  • Distribuerte låser (SET NX PX) utvider denne garantien til hele flåten; gi alltid låsen en TTL.
  • TTL-er med tilfeldig variasjon desynkroniserer masseutløp, slik at mange nøkler aldri utløper på samme tidspunkt.
  • Probabilistisk tidlig utløp (XFetch) oppdaterer ofte brukte og kostbare nøkler før de i det hele tatt mangler.
  • Stale-while-revalidate leverer gamle data umiddelbart og oppdaterer én gang i bakgrunnen.

Legg dem oppå hverandre: tilfeldig variasjon + samordning som grunnlag, deretter låser eller XFetch for de mest brukte nøklene.

Gratis å komme i gang

Lær deg JavaScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
92

Ofte stilte spørsmål

Er leksjonen «Forebygging av cache-stampeder og «thundering herds»» gratis?

Ja – hele teksten i «Forebygging av cache-stampeder og «thundering herds»» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Bootcamp i backendutvikling med Node.js-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Hva lærer jeg i «Forebygging av cache-stampeder og «thundering herds»»?

Begrens stampeder med sammenslåing av forespørsler, TTL-er med jitter og sannsynlighetsbasert tidlig utløp. Du øver på Bootcamp i backendutvikling med Node.js med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Bootcamp i backendutvikling med Node.js?

Ingen tidligere erfaring er nødvendig. Bootcamp i backendutvikling med Node.js på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 4 av 4.

Hvor lang tid tar leksjonen «Forebygging av cache-stampeder og «thundering herds»»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Bootcamp i backendutvikling med Node.js-leksjonen?

Ja. Alle Bootcamp i backendutvikling med Node.js-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Cache-aside, write-through og TTL-strategier
  2. Distribuerte låser og Redlock-algoritmen
  3. Pub/Sub, streamer og ratebegrensning med Redis
  4. Forebygging av cache-stampeder og «thundering herds»
← Tilbake til Bootcamp i backendutvikling med Node.js