Bootcamp i backendudvikling med Node.js · Lektion

Cache-aside-, write-through- og TTL-strategier

Vælg det rette cachemønster og den rette udløbspolitik for at afveje aktualitet mod belastning af databasen.

Lektion 1 af 413 trin

Cache-aside-, write-through- og TTL-strategier er en gratis Bootcamp i backendudvikling med Node.js-lektion på CoddyKit. Dette er lektion 1 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Bootcamp i backendudvikling med Node.js, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.

Hvorfor caching findes: flaskehalsen i databasen

Enhver Node.js-backend rammer før eller siden den samme mur: databasen bliver flaskehalsen. Når trafikken vokser, belaster gentagne læsninger af de samme data — brugerprofiler, produktlister og konfigurationsværdier — din DB med identiske forespørgsler.

Caching løser dette ved at gemme resultatet af en dyr operation, så efterfølgende forespørgsler kan betjenes fra et hurtigt lager i hukommelsen i stedet for at ramme databasen igen.

  • Redis er standardlaget til caching i branchen for Node.js — det kører udelukkende i hukommelsen, understøtter avancerede datastrukturer og håndterer hundredtusindvis af operationer pr. sekund.
  • Et cache-hit returnerer data på under 1 ms; en forespørgsel til DB'en kan tage 10–200 ms eller mere under belastning.
  • Ulempen er aktualitet: cachede data afspejler måske ikke den seneste tilstand i DB'en.

Valget af det rigtige cachingmønster afgør, hvor godt dit system afvejer hastighed mod konsistens.

Opret forbindelse til Redis med ioredis

Før du implementerer et cachingmønster, skal du bruge en pålidelig Redis-klient. ioredis er det mest populære valg til Node.js — den understøtter clustering, Sentinel, pipelining og Lua-scripting direkte fra start.

Installér den, og opret en genanvendelig klientinstans:

  • Brug et singleton-modul, så alle dele af din app deler én forbindelsespulje.
  • Konfigurér maxRetriesPerRequest og lazyConnect for robusthed i produktion.
  • Klienten udsender events som connect, error og reconnecting — håndtér altid fejl.
// redis/client.js
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || undefined,
  maxRetriesPerRequest: 3,
  lazyConnect: true,
});

redis.on('connect', () => console.log('[Redis] Connected'));
redis.on('error', (err) => console.error('[Redis] Error:', err.message));
redis.on('reconnecting', () => console.log('[Redis] Reconnecting...'));

module.exports = redis;

Cache-aside-mønsteret (doven indlæsning)

Cache-Aside (også kaldet lazy loading) er det mest almindelige cachemønster. Selve applikationen administrerer cachen — data indlæses først i cachen, når der første gang anmodes om dem.

Flowet er enkelt:

  • Læsning: Tjek cachen først. Ved et cache-hit returneres den cachelagrede værdi. Ved et cache-miss forespørges databasen, resultatet gemmes i cachen, og derefter returneres det.
  • Skrivning: Opdater databasen, og invalidér (slet) derefter cacheposten, så den næste læsning henter friske data.

Cache-Aside er modstandsdygtigt — hvis Redis går ned, forringes din app på en kontrolleret måde ved at falde tilbage til databasen. Det forhindrer også indlæsning af ubrugte data (så du undgår spild af hukommelse).

Ulempen er, at den første anmodning efter et cache-miss altid er langsom. Ved høj samtidighed kan flere anmodninger udløse samtidige databaseforespørgsler efter den samme nøgle — det kaldes et cache-stampede.

Implementering af Cache-Aside i Node.js

Her er en praktisk implementering af Cache-Aside til opslag af en brugerprofil. Bemærk mønsteret: GET → miss? → databaseforespørgsel → SET → returnér.

Vigtige beslutninger, som vises i koden:

  • Cache-nøglen indeholder entitetstypen og ID'et for at undgå kollisioner mellem navnerum.
  • EX angiver en TTL (levetid) i sekunder — data udløber automatisk, selv hvis de aldrig eksplicit bliver invalideret.
  • JSON-serialisering er nødvendig, fordi Redis gemmer strenge.
  • Ved et cache-miss skrives resultatet fra databasen tilbage, før det returneres.
// services/userService.js
const redis = require('../redis/client');
const db = require('../db'); // hypothetical DB module

const USER_CACHE_TTL = 300; // 5 minutes

async function getUserById(userId) {
  const cacheKey = `user:${userId}`;

  // 1. Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    console.log(`[Cache HIT] ${cacheKey}`);
    return JSON.parse(cached);
  }

  // 2. Cache miss — query DB
  console.log(`[Cache MISS] ${cacheKey}`);
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  if (!user) return null;

  // 3. Populate cache with TTL
  await redis.set(cacheKey, JSON.stringify(user), 'EX', USER_CACHE_TTL);

  return user;
}

async function updateUser(userId, data) {
  // Update DB first
  await db.query('UPDATE users SET name=$1 WHERE id=$2', [data.name, userId]);

  // Invalidate cache so next read fetches fresh data
  await redis.del(`user:${userId}`);
  console.log(`[Cache INVALIDATED] user:${userId}`);
}

module.exports = { getUserById, updateUser };

Write-Through-mønsteret

Write-Through anvender den modsatte tilgang til cache-invalidering: hver gang data skrives til databasen, skrives de samtidig til cachen. Cachen afspejler altid databasens tilstand.

Kendetegn ved Write-Through:

  • Læsninger er altid hurtige — cachen er garanteret at indeholde aktuelle data efter hver skrivning.
  • Ingen forældede data — cachen og databasen opdateres atomisk i den samme skriveoperation.
  • Skriveomkostning — hver skrivning medfører to roundtrips (database + cache), hvilket øger skriveforsinkelsen en smule.
  • Problem ved kold start — data, der blev skrevet, før Redis blev introduceret, findes ikke i cachen; kombinér med Cache-Aside-læsninger for at varme cachen op.

Write-Through er ideelt, når forholdet mellem læsninger og skrivninger er højt, og du ikke kan acceptere forældede læsninger (f.eks. lagerbeholdninger, prisdata og sessionstilstand).

Implementering af Write-Through i Node.js

Med Write-Through skriver servicelaget til både databasen og cachen ved enhver ændring. Bemærk, at cachen i modsætning til Cache-Aside fyldes ved skrivning — ikke ved læsning.

Eksemplet nedenfor viser en opdatering af prisen på et produkt, hvor forældede læsninger ville medføre reel økonomisk skade:

// services/productService.js
const redis = require('../redis/client');
const db = require('../db');

const PRODUCT_TTL = 3600; // 1 hour

async function updateProductPrice(productId, newPrice) {
  // 1. Write to database
  const updated = await db.query(
    'UPDATE products SET price=$1, updated_at=NOW() WHERE id=$2 RETURNING *',
    [newPrice, productId]
  );

  // 2. Write-through: update cache immediately
  const cacheKey = `product:${productId}`;
  await redis.set(cacheKey, JSON.stringify(updated), 'EX', PRODUCT_TTL);
  console.log(`[Write-Through] ${cacheKey} updated in cache`);

  return updated;
}

async function getProduct(productId) {
  const cacheKey = `product:${productId}`;

  const cached = await redis.get(cacheKey);
  if (cached) {
    console.log(`[Cache HIT] ${cacheKey}`);
    return JSON.parse(cached);
  }

  // Cold-start fallback (Cache-Aside hybrid)
  console.log(`[Cache MISS - Cold Start] ${cacheKey}`);
  const product = await db.query('SELECT * FROM products WHERE id=$1', [productId]);
  if (product) {
    await redis.set(cacheKey, JSON.stringify(product), 'EX', PRODUCT_TTL);
  }
  return product;
}

module.exports = { updateProductPrice, getProduct };

TTL-strategier: Hvor længe skal data leve?

TTL (Time-To-Live) er udløbsvinduet for en cachepost. Det er afgørende at vælge den rigtigt — er den for kort, overbelaster du databasen; er den for lang, ser brugerne forældede data.

Almindelige TTL-niveauer efter datas omskiftelighed:

  • Statisk konfiguration / feature flags: 3600s–86400s (1 time til 1 dag). Ændres sjældent og er dyrt at beregne.
  • Brugerprofiler / præferencer: 300s–900s (5–15 minutter). Opdateres lejlighedsvis; kortvarige forældede data er acceptable.
  • Produktlister / søgeresultater: 60s–300s (1–5 minutter). Moderat ændringshastighed.
  • Indkøbskurv / sessionsdata: 1800s–3600s (30 minutter til 1 time). Bør overleve en opdatering af browseren, men udløbe naturligt.
  • Data i realtid (aktiekurser, live-resultater): 1s–10s eller INGEN cache — hent friske data hver gang.

En praktisk regel er at begynde med en forsigtig, kort TTL, måle cache-hit-raterne og kun forlænge TTL'en dér, hvor hit-raten berettiger det.

// config/cacheTTL.js
// Centralize TTL constants — one place to tune
module.exports = Object.freeze({
  USER_PROFILE:    5  * 60,       //  5 minutes
  PRODUCT_DETAIL:  3  * 60,       //  3 minutes
  PRODUCT_LIST:    1  * 60,       //  1 minute
  FEATURE_FLAGS:   60 * 60,       //  1 hour
  SESSION:         30 * 60,       // 30 minutes
  LEADERBOARD:     10,            // 10 seconds
});

// Usage example:
// const TTL = require('../config/cacheTTL');
// redis.set(key, value, 'EX', TTL.USER_PROFILE);

Glidende kontra faste TTL-vinduer

Der er to måder at styre udløbstidspunktet på:

  • Fast TTL: Timeren starter, når nøglen angives med SET, og tæller ned uanset adgangen. Det er enkelt og forudsigeligt — brug det til data, der skal udløbe på en bestemt tid på døgnet (f.eks. sessionstokens og vinduer for hastighedsbegrænsning).
  • Glidende TTL (forny ved læsning): Hver gang nøglen læses, nulstilles dens TTL. Aktive brugere holder deres cache varm, mens inaktive poster udløber naturligt. Brug det til ofte tilgåede, populære data.

Redis har ikke indbygget glidende TTL — du implementerer det ved at kalde EXPIRE ved hvert cache-hit for at nulstille nedtællingen.

// Sliding TTL — reset expiry on every successful cache read
async function getUserWithSlidingTTL(userId) {
  const cacheKey = `user:${userId}`;
  const SLIDING_TTL = 300; // 5-minute inactivity window

  const cached = await redis.get(cacheKey);
  if (cached) {
    // Reset TTL on each access — extends life for active users
    await redis.expire(cacheKey, SLIDING_TTL);
    return JSON.parse(cached);
  }

  const user = await db.query('SELECT * FROM users WHERE id=$1', [userId]);
  if (user) {
    await redis.set(cacheKey, JSON.stringify(user), 'EX', SLIDING_TTL);
  }
  return user;
}

Forebyggelse af cache-stampedes med mutex-låse

Et cache-stampede opstår, når en populær nøgle udløber, og hundredvis af samtidige anmodninger oplever et cache-miss på samme tidspunkt — hver anmodning sender en databaseforespørgsel, hvilket overbelaster databasen.

Standardløsningen er en distribueret mutex: kun den første anmodning erhverver en lås og henter data fra databasen. De øvrige venter eller leverer en værdi, der er en smule forældet.

Redis understøtter atomisk erhvervelse af låse via SET key value NX EX timeout:

  • NX — angiv kun, hvis nøglen IKKE findes (atomisk compare-and-set).
  • EX — frigiv automatisk efter timeout, så låse ikke kan holdes på ubestemt tid.
  • Processer uden låsen prøver igen efter en kort pause, indtil låsen frigives, og cachen er varm.
// utils/cacheWithLock.js
const redis = require('../redis/client');

async function getOrFetchWithLock(cacheKey, fetchFn, ttl = 60) {
  // 1. Try cache
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${cacheKey}`;
  const lockAcquired = await redis.set(lockKey, '1', 'NX', 'EX', 10); // 10s lock

  if (lockAcquired) {
    try {
      // 2. We hold the lock — fetch from DB
      const data = await fetchFn();
      await redis.set(cacheKey, JSON.stringify(data), 'EX', ttl);
      return data;
    } finally {
      await redis.del(lockKey); // Always release lock
    }
  } else {
    // 3. Lock held by another request — wait and retry
    await new Promise((r) => setTimeout(r, 50));
    const retried = await redis.get(cacheKey);
    return retried ? JSON.parse(retried) : fetchFn(); // fallback
  }
}

module.exports = { getOrFetchWithLock };

Mønstre til cache-invalidering

Phil Karlton sagde berømt: "Der er kun to svære ting i datalogi: cache-invalidering og at navngive ting." Korrekt invalidering holder cachen konsistent uden at tømme den unødigt.

De vigtigste tilgange:

  • Nøglebaseret invalidering (DEL): Slet den præcise nøgle, når de underliggende data ændres. Hurtigt og målrettet — brug dette til ændringer af enkeltentiteter.
  • Præfiksbaseret invalidering (SCAN + DEL): Find og slet alle nøgler, der matcher et mønster. Nyttigt, når én skrivning invaliderer mange relaterede cacheposter (f.eks. når en kategori opdateres, og alle cacher med produktlister invalideres).
  • Kun TTL (ingen eksplicit invalidering): Lad data udløbe naturligt. Det er acceptabelt, når kortvarige forældede data kan tolereres, og skrivninger forekommer sjældent.
  • Cache-tags: Knyt flere cachenøgler til et tag, og invalidér efter tag. Kræver et indeks over nøglesæt i Redis.
// Prefix-based invalidation using SCAN (safe for production, non-blocking)
async function invalidateProductListCaches(categoryId) {
  const pattern = `products:list:category:${categoryId}:*`;
  let cursor = '0';
  let deleted = 0;

  do {
    // SCAN is non-blocking unlike KEYS — safe for production
    const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
    cursor = nextCursor;

    if (keys.length > 0) {
      await redis.del(...keys);
      deleted += keys.length;
    }
  } while (cursor !== '0');

  console.log(`[Cache] Invalidated ${deleted} product-list keys for category ${categoryId}`);
}

// Call after a category rename or product re-assignment:
// await invalidateProductListCaches(42);

Valg af det rigtige mønster: Beslutningsramme

Intet enkelt mønster passer til alle anvendelser. Her er en hurtig beslutningsramme:

  • Cache-Aside — Standardvalget. Fungerer til alle arbejdsbelastninger med mange læsninger, hvor lejlighedsvise forældede data er acceptable. Cachen vokser kun med data, der faktisk anmodes om, så du sparer hukommelse.
  • Write-Through — Vælg dette, når læsninger langt overstiger skrivninger, OG du har brug for garanteret aktualitet ved læsninger (priser, lagerbeholdning og tilladelser). Accepter den ekstra skriveforsinkelse.
  • Kun TTL (ingen invalidering) — Brug dette til referencedata, der ændres efter en forudsigelig tidsplan: feature flags opdateret hver time og valutakurser opdateret hvert 15. minut. Det er enkelt og kræver kun lidt vedligeholdelse.
  • Hybrid (Cache-Aside-læsninger + Write-Through-skrivninger) — Den mest robuste tilgang til produktion. Write-Through holder cachen varm for populære entiteter, mens Cache-Aside håndterer koldstart for data, der sjældent læses.

Mål din cache-hit-rate (mål: over 90 % for populære kodeveje) ved hjælp af Redis INFO stats → keyspace_hits / keyspace_misses for at validere dit valg.

// Measuring cache hit rate from Node.js
async function getCacheStats() {
  const info = await redis.info('stats');
  const hits   = parseInt(info.match(/keyspace_hits:(\d+)/)[1]);
  const misses = parseInt(info.match(/keyspace_misses:(\d+)/)[1]);
  const total  = hits + misses;
  const hitRate = total > 0 ? ((hits / total) * 100).toFixed(2) : '0.00';

  return {
    hits,
    misses,
    hitRate: `${hitRate}%`,
    recommendation: parseFloat(hitRate) < 90
      ? 'Consider extending TTL or switching to Write-Through'
      : 'Cache performance is healthy',
  };
}

module.exports = { getCacheStats };
// Output example:
// { hits: 9823, misses: 177, hitRate: '98.23%', recommendation: 'Cache performance is healthy' }

Videnstjek: Valg af mønster

Test din forståelse af afvejningerne mellem cachemønstre.

Opsummering af lektionen: Cachemønstre og TTL-strategier

Du har gennemgået det grundlæggende cacheværktøj til Node.js-backends med Redis. Her er det vigtigste:

  • Cache-Aside (Lazy Loading): Tjek cachen → miss → forespørg i databasen → fyld cachen → returnér. Modstandsdygtigt over for Redis-fejl; risiko for cache-stampedes ved høj samtidighed.
  • Write-Through: Hver skrivning til databasen opdaterer også cachen. Garanterer aktuelle data ved læsninger, men øger skriveforsinkelsen. Bedst til data med mange læsninger og få skrivninger, hvor korrekthed er vigtig.
  • TTL-strategier: Tilpas udløbsvinduerne til datas omskiftelighed — sekunder for data i realtid og timer for statisk konfiguration. Brug glidende TTL (EXPIRE ved læsning) til sessioner for aktive brugere.
  • Forebyggelse af stampedes: Brug en Redis-baseret distribueret mutex (SET NX EX) til at serialisere samtidige cache-misses for den samme nøgle.
  • Invalidering: Foretræk målrettet DEL på kendte nøgler; brug SCAN + DEL (aldrig KEYS) til mønsterbaseret invalidering i produktion.
  • Mål først: Hold øje med keyspace_hits / keyspace_misses — en hit-rate under 90 % tyder på en forkert konfigureret TTL eller et forkert valg af mønster.

Den hybride tilgang — Cache-Aside-læsninger med Write-Through-skrivninger — er den mest gennemprøvede opsætning til Node.js-tjenester i produktion.

Gratis at komme i gang

Lær JavaScript med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
92

Ofte stillede spørgsmål

Er lektionen “Cache-aside-, write-through- og TTL-strategier” gratis?

Ja — hele teksten til “Cache-aside-, write-through- og TTL-strategier” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Bootcamp i backendudvikling med Node.js-kurset, skal du opgradere til CoddyKit PRO. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Cache-aside-, write-through- og TTL-strategier”?

Vælg det rette cachemønster og den rette udløbspolitik for at afveje aktualitet mod belastning af databasen. Du øver dig i Bootcamp i backendudvikling med Node.js med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Bootcamp i backendudvikling med Node.js?

Der kræves ingen tidligere erfaring. Bootcamp i backendudvikling med Node.js på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 1 af 4.

Hvor lang tid tager lektionen “Cache-aside-, write-through- og TTL-strategier”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Bootcamp i backendudvikling med Node.js-lektion?

Ja. Alle Bootcamp i backendudvikling med Node.js-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Cache-aside-, write-through- og TTL-strategier
  2. Distribuerede låse og Redlock-algoritmen
  3. Pub/Sub, streams og ratebegrænsning med Redis
  4. Forebyggelse af cache-stampedes og thundering herds
← Tilbage til Bootcamp i backendudvikling med Node.js