9.6 Cache mint eszköz

← 9. Optimalizációs alapok

9.6 Cache stratégiák mint performance eszköz

Áttekintő

A cache olyan, mint egy jó pincér, aki emlékszik a törzsvendégek rendelésére: nem megy vissza a konyhába minden alkalommal megkérdezni, mert tudja a választ. Az eredmény? Gyorsabb kiszolgálás, kevesebb terhelés a konyhán.

A szoftverfejlesztésben ugyanez a logika: ha valamit már egyszer kiszámoltunk vagy lekértünk, érdemesebb megjegyezni az eredményt, mint újra elvégezni a munkát. Ez a cache alapgondolata – és az egyik leghatásosabb performance eszköz, amit minimális erőfeszítéssel bevezethetsz.

A junior fejlesztők két szélső hibát szoktak elkövetni:

1. Mindent cache-elnek. Mert hallották, hogy a cache gyors. Aztán a felhasználók régi adatot látnak, tranzakciók elromlanak, és senki nem érti, miért.

2. Semmit nem cache-elnek. Mert nem tudnak róla, vagy mert félnek tőle. Az adatbázis feleslegesen fut ugyanarra a lekérdezésre másodpercenként százszor.

Ez az alfejezet azt tanítja meg, hogy mikor, mit és hogyan érdemes cache-elni – és mikor kell elengedni a gondolatot.


Részletes leírás

Mi a cache és miért gyors?

A cache egy gyors, ideiglenes tároló, amelybe az adatokat közelebb hozza a felhasználóhoz – vagy közelebb hozza az alkalmazáshoz, elkerülve a drága újraszámítást.

A gyorsaság oka mindig ugyanaz: elkerülünk valami lassút.

  • Böngészőben tárolt CSS fájl → nem kell az interneten keresztül letölteni
  • Memóriában tárolt adatbázis eredmény → nem kell SQL lekérdezést futtatni
  • CDN-en tárolt kép → nem kell a szerveredtől Brazíliából letölteni

A cache nem varázslat – csak azt csinálja, hogy a következő kérésnél egy gyorsabb forrásból ad választ.


A cache-elés alapkérdése: érdemes-e?

Nem minden adatot érdemes cache-elni. Mielőtt bevezetsz egy cache megoldást, két kérdést tegyél fel:

1. Mennyire változik az adat?

  • Statikus tartalom (logo, CSS, egy blogpost szövege) → szinte soha nem változik → ideális cache-re
  • Felhasználó kosara, banki egyenleg, valós idejű részvényárfolyam → másodpercenként változhat → nem szabad cache-elni (vagy csak nagyon rövid ideig)

2. Mennyibe kerül újra előállítani?

  • Egy egyszerű adatbázis lekérdezés, ami 5 ms → talán nem éri meg a cache bonyolultságát
  • 500 termék árát összesítő, 3 táblát JOIN-oló lekérdezés, ami 2 másodperc → azonnal érdemes cache-elni

Ha az adat sokat változik ÉS olcsó előállítani → ne cache-elj.
Ha az adat ritkán változik VAGY drága előállítani → cache ideális jelölt.


HTTP cache fejlécek – amit a böngésző elment

Mielőtt a szervered egyáltalán válaszol, a böngésző ellenőrzi: „van már nekem ebből?" Ha igen, nem is fordul a hálózathoz.

Ez a böngésző beépített cache rendszere, amelyet a szerver HTTP fejlécekkel irányít.

Cache-Control fejléc:

Cache-Control: max-age=86400

Ez azt mondja a böngészőnek: „ezt a választ 86400 másodpercig (1 napig) tárolhatod, és ez idő alatt nem kell újra kérned."

Legfontosabb értékek:

DirektívaMit jelent
max-age=36001 óráig érvényes a cache
no-cacheMindig kérdezze meg a szervert (de elmentheti a választ)
no-storeSemmit ne tároljon (érzékeny adatoknál)
publicCDN-ek is tárolhatják
privateCsak a böngésző tárolhatja (felhasználóhoz kötött tartalom)

Mikor mit használj:

  • Statikus fájlok (CSS, JS, képek, fontok): Cache-Control: public, max-age=31536000 – egy év, mert a fájlnév tartalmazza a hash-t (pl. app.a3b4c5.js), frissüléskor új URL lesz
  • API válasz, amely naponta egyszer frissül: Cache-Control: private, max-age=3600
  • Kosár tartalma, bejelentkezett felhasználó adatai: Cache-Control: no-store

ETag – feltételes kérések:

ETag: "abc123"
If-None-Match: "abc123"

Az ETag egy ujjlenyomat az erőforráshoz. Ha a böngésző újra kéri az erőforrást, elküldi az ETag-et. Ha az adat nem változott, a szerver 304 Not Modified választ küld – tartalom nélkül, csak egy kis fejléc. A böngésző tovább használja a cachelt verziót.

Ez különösen hasznos, ha nem tudod pontosan megjósolni, mikor változik az adat.


Szerver oldali cache típusok

A böngésző cache a kliens gépén van – de a szerveren is lehet cache-elni. Ez segít, ha sok különböző felhasználó kéri ugyanazt az adatot.

1. In-memory cache (memóriában tárolt)

A legegyszerűbb és leggyorsabb: az adat a szerver memóriájában van.

# Példa: egyszerű Python dict-alapú cache
cache = {}

def get_product(product_id):
    if product_id in cache:
        return cache[product_id]  # gyors, memóriából
    
    product = db.query("SELECT * FROM products WHERE id = ?", product_id)
    cache[product_id] = product  # elmentjük a jövőbeli kérésekre
    return product
// JavaScript példa
const cache = new Map();

async function getProduct(productId) {
    if (cache.has(productId)) {
        return cache.get(productId);
    }
    
    const product = await db.query('SELECT * FROM products WHERE id = $1', [productId]);
    cache.set(productId, product);
    return product;
}
// PHP példa (statikus változóval)
function getProduct(int $productId): array {
    static $cache = [];
    
    if (isset($cache[$productId])) {
        return $cache[$productId];
    }
    
    $product = $db->query("SELECT * FROM products WHERE id = ?", [$productId]);
    $cache[$productId] = $product;
    return $product;
}

Hátránya: Ha a szerver újraindul, a cache kiürül. Ha több szerverpéldány fut, mindegyiknek saját cache-e van – inkonzisztencia veszélye.

2. Dedikált cache szerver (Redis, Memcached)

A valóságban a legtöbb éles alkalmazás Redis-t vagy Memcached-t használ. Ezek különálló, memóriaalapú adatbázisok, kifejezetten gyors olvasásra tervezve.

import redis

r = redis.Redis(host='localhost', port=6379)

def get_product(product_id):
    cached = r.get(f"product:{product_id}")
    if cached:
        return json.loads(cached)
    
    product = db.query("SELECT * FROM products WHERE id = ?", product_id)
    r.setex(f"product:{product_id}", 3600, json.dumps(product))  # 1 óráig érvényes
    return product

A Redis több szerverpéldány között megosztható, tud TTL-t (lejárati idő) kezelni, és ha a szerver újraindul, az adat megmarad (opcionálisan).

3. Query cache / ORM cache

Sok adatbázis-kezelő rendszer és ORM beépített cache mechanizmust kínál. Egy adott SQL lekérdezés eredménye eltárolódik – a következő azonos lekérdezésnél nem fut le újra.

Fontos tudni: az adatbázis saját query cache-e (pl. MySQL query cache) a modern verziókban el lett távolítva, mert rosszul teljesített. Ma az alkalmazás szintjén kell cache-elni.


Cache invalidáció – a nehéz rész

„A számítástechnikában két nehéz dolog van: cache invalidáció és elnevezések." – Phil Karlton

A cache invalidáció azt jelenti: hogyan biztosítjuk, hogy a cache-ből töröljük az elavult adatot, és friss kerüljön helyére?

Stratégia 1: TTL (Time to Live) – lejárati idő

A legegyszerűbb megközelítés: add meg, mennyi ideig érvényes a cache bejegyzés.

r.setex("product:42", 3600, json.dumps(product))  # 1 óra után automatikusan törlődik

Előny: Egyszerű, automatikus.
Hátrány: Legfeljebb 1 óráig látnak a felhasználók elavult adatot. Ha 5 perccel a lejárat előtt frissül az adat, 5 percig régi adatot látnak. Ha kritikus, ez elfogadhatatlan.

Stratégia 2: Aktív invalidálás – töröld, amikor frissül

Amikor az adat megváltozik, töröld a cache bejegyzést is.

def update_product(product_id, new_data):
    db.update("UPDATE products SET ... WHERE id = ?", product_id, new_data)
    r.delete(f"product:{product_id}")  # cache törlése

A következő kérésnél az adat nem lesz a cache-ben → az adatbázisból töltődik be → és újra kerül a cache-be.

Előny: Azonnal friss adat.
Hátrány: Bonyolultabb. Ha az adat több helyen is cache-elve van, mindenhol törölni kell.

Stratégia 3: Cache-busting statikus fájloknál

Statikus fájloknál (JS, CSS, képek) egy bevett megközelítés: a fájl neve tartalmazza a tartalom hash-ét.

<!-- Régi változat: könnyen kerülhet elavult verzió a cache-be -->
<link rel="stylesheet" href="/styles.css">

<!-- Jobb megközelítés: a hash a fájltartalom ujjlenyomata -->
<link rel="stylesheet" href="/styles.a3b4c5d6.css">

Ha a CSS megváltozik, a hash is megváltozik → új fájlnév → a böngésző nem találja a cacheben → letölti az újat. A régi URL-re mutató cache bejegyzés sosem lesz újra kért.


A leggyakoribb cache-hiba: nem gondolsz rá

A junior fejlesztők leggyakoribb problémája nem az, hogy rossz cache stratégiát választanak – hanem hogy egyáltalán nem gondolnak a cache-re, amikor hibát keresnek.

Tipikus szituáció:

„Frissítettem az adatbázisban az árat, de az oldalon még a régi ár jelenik meg."

Kérdések sorban:

  1. Van-e böngésző cache? → Próbáld Ctrl+F5-tel (hard refresh)
  2. Van-e CDN cache? → Esetleg purgálni kell
  3. Van-e szerver oldali cache? → Redis/Memcached TTL lejárt-e?
  4. Van-e az alkalmazásban query cache? → Mikor frissül?

Ha nem tudod, hogy az alkalmazásodban van-e cache – kérdezd meg a seniort, mielőtt tovább debuggolsz.


Trade-off: frissesség vs. sebesség

Nincs univerzálisan helyes cache stratégia – mindig kompromisszum van:

Rövid TTL / nincs cacheHosszú TTL / erős cache
SebességLassabb (mindig friss adat)Gyorsabb
FrissességMindig aktuálisEsetleg elavult
Szerver terhelésMagasabbAlacsonyabb
Ideális mikorValós idejű adatRitkán változó adat

Ökölszabály juniorként:

  • Felhasználóhoz kötött, változó adatok (kosár, egyenleg, profil) → ne cache-elj, vagy nagyon rövid TTL
  • Ritkán változó adatok (termékleírás, blogpost, konfigurációs adatok) → cache-elj, hosszabb TTL
  • Statikus fájlok (CSS, JS, képek, fontok) → mindig cache-elj, akár egy évre

Életszerű példák

Eset 1: A webshop, ami mindig lassú reggel

Egy webshop áruinak listája minden kérésnél 200+ terméket kérdez le az adatbázisból, minden termékhez 3 JOIN-nal. Az adatok naponta egyszer frissülnek (amikor a raktár szinkronizál).

Reggel 9-kor, amikor mindenki megnyitja az oldalt, az adatbázis meghal a terhelés alatt.

Megoldás:

PRODUCT_LIST_CACHE_KEY = "product_list_all"
CACHE_TTL = 60 * 60 * 6  # 6 óra

def get_all_products():
    cached = redis.get(PRODUCT_LIST_CACHE_KEY)
    if cached:
        return json.loads(cached)
    
    products = db.query("""
        SELECT p.*, c.name as category, s.quantity as stock
        FROM products p
        JOIN categories c ON p.category_id = c.id
        JOIN stock s ON p.id = s.product_id
    """)
    
    redis.setex(PRODUCT_LIST_CACHE_KEY, CACHE_TTL, json.dumps(products))
    return products

A 200 egyidejű reggeli kérésből az első lefuttatja a lekérdezést (2 másodperc), a többi 199 Redisből kap választ (2 ms). A raktár szinkronizáláskor a cache bejegyzést töröljük – utána az első kérés frissíti.


Eset 2: A frontend fejlesztő, aki nem ért egy bugot

Anna frissítette a CSS-t, deployta – és a felhasználók fele még mindig a régi stílust látja. Pánik.

Mi történt: a CDN (Content Delivery Network) az összes felhasználóhoz közel tárolja a statikus fájlokat. A CSS fájl a CDN cache-éből érkezett – az nem frissült automatikusan.

Hosszú távú megoldás (amit a senior beállít):

<!-- Build folyamat generálja a hash-es fájlnevet -->
<link rel="stylesheet" href="/styles.3a7f9b2c.css">

Ha a CSS változik, a hash is változik, az URL is változik – és az URL különbözik, a CDN nem találja cache-ben → letölti a friss verziót.

Junior fejlesztőként: ha CSS frissítés után nem látod a változást, próbálj Ctrl+F5-tel hard refresht. Ha az sem segít, kérdezd meg, hogy van-e CDN és kell-e purgálni.


Eset 3: A „látom a régi adatot" bug

Balázs megadott egy árú termékre kedvezményt az admin felületen. Ellenőrzi a webshopot – még mindig a régi ár. Kattint a termékre – már a new ár jelenik meg a részletoldalon, de a listán még a régi.

Diagnózis:

A részletoldal és a listanézet különböző cache kulcsokat használ:

# Részletoldal cache-e frissül, amikor a terméket módosítják
redis.delete(f"product:{product_id}")

# De a lista cache-e nem!
# redis.delete("product_list_all")  # ← ezt elfelejtették

Javítás:

Amikor egy terméket frissítenek, minden érintett cache bejegyzést törölni kell – nemcsak a terméket magát, hanem a listát és minden más cache-t, ami tartalmazza azt.

Ez a cache invalidáció igazi bonyolultsága: nem elég a közvetlen bejegyzést törölni, hanem minden érintett bejegyzést kell.


Példafeladat

Szituáció: Lassú API endpoint és cache stratégia tervezése

Dolgozol egy ingatlan-kereső alkalmazásban. Az alábbi API endpoint másodpercenként ~300 kérést kap, és átlagosan 800 ms-ot válaszol:

GET /api/cities

Ez az endpoint visszaad minden várost (kb. 400 db), amelyekben elérhető ingatlan. Az adatok ritkán frissülnek – hozzávetőleg hetente egyszer (amikor egy új városban kerül hirdetés).

A response kb. így néz ki:

[
  {"id": 1, "name": "Budapest", "listing_count": 1240},
  {"id": 2, "name": "Debrecen", "listing_count": 310},
  ...
]

A listing_count naponta frissül – nem valós idejű.

Feladatok:

  1. Érdemes-e ezt az endpoint-ot cache-elni? Indokold meg!
  1. Milyen TTL-t javasolnál a cache bejegyzésre? Miért?
  1. Hol érdemes cache-elni: böngésző oldalon (HTTP Cache-Control fejléc), szerver oldalon (Redis), vagy mindkettőn? Indokold!
  1. Mi történjen, ha valaki adminként egy új várost ad hozzá? Hogyan biztosítod, hogy a cache ne mutasson elavult adatot?
  1. Milyen kockázata van a cache bevezetésének ebben az esetben? Mi az, amit nem szabad cache-elni?

Mintamegoldás:

1. Érdemes cache-elni?

Igen, egyértelműen. Az adat ritkán változik (heti egyszer), de a kérések száma magas (300/s). Cache nélkül másodpercenként 300 adatbázis lekérdezés fut – feleslegesen. Ez ideális cache jelölt: magas olvasási arány, alacsony frissítési arány.

2. TTL javaslat:

24 óra (max-age=86400) szerver oldali Redis cache-re – mivel a listing_count naponta frissül, ez elfogadható frissességi szint. HTTP cache-re (böngésző) 10-15 perc elegendő (Cache-Control: public, max-age=600), hogy ne kelljen mindig a szerverre fordulni, de az ingatlan-keresőn a friss adatok fontosak.

3. Hol cache-eljünk?

Mindkettőn – de más TTL-lel:

  • Redis (szerver oldal): 24 óra – a 300 kérés/s terhelést ez eliminállja; mindössze néhányszor kérdezi le az adatbázist naponta
  • HTTP Cache-Control (böngésző/CDN): 10-15 perc – a felhasználók böngészője sem kér minden kattintásnál új listát

4. Új város hozzáadásakor:

Az admin mentés után aktívan invalidálni kell a cache-t:

def add_city(name):
    db.insert("INSERT INTO cities (name) VALUES (?)", name)
    redis.delete("cities_list")  # azonnal friss adatot ad a következő kérésnél

HTTP cache-nél: ha CDN-t használunk, ott is purgálni kell a /api/cities végpontot.

5. Kockázat és mi ne kerüljön cache-be:

  • A cache-elt lista nem tartalmaz felhasználóhoz kötött adatot → public cache biztonságos
  • Ha a jövőben felhasználóhoz kötött szűrés kerül az endpointba (pl. „csak városok, ahol van a kedvenclistádon jelölt ingatlan"), azt nem szabad a közös cache-be tenni – felhasználónként külön cache kulcs, vagy egyáltalán ne cache-eljük

Tesztfeladatok

1. feladat

Melyik adat a legalkalmasabb szerveroldalán Redis cache-be tenni?

a) Egy webshop aktuálisan bejelentkezett felhasználó kosarának tartalma
b) Banki egyenleg valós idejű lekérdezése
c) Egy blogplatform főoldalán megjelenő legnépszerűbb 10 cikk listája, amely naponta frissül
d) Egy orvosi rendszer páciensének aktuális vérnyomás-adatai


2. feladat

Mit jelent a Cache-Control: no-store HTTP fejléc?

a) A böngésző mindig kérdezze meg a szervert, de a választ tárolhatja
b) A válasz sehol nem tárolható – sem böngészőben, sem CDN-en
c) A cache 0 másodpercig érvényes
d) Csak a szerver tárolja, a böngésző nem


3. feladat

Egy webshopban frissíted egy termék árát az admin felületen, de a terméklistán még a régi ár jelenik meg. Mi a legvalószínűbb ok?

a) Az adatbázis tranzakció nem commiteált
b) A szerver nem indult újra a változtatás után
c) Szerver oldali cache (pl. Redis) még a régi adatot tartja, és az nem lett invalidálva
d) A böngésző JavaScript kód hibásan jeleníti meg az árat


4. feladat

Miért érdemes a statikus fájlok (CSS, JS) nevébe hash-t tenni a build folyamat során?

a) Hogy a fájl kisebb legyen a tömörítés miatt
b) Hogy a fájlt nem lehet visszafejteni
c) Hogy a fájlnév változásával a böngésző cache automatikusan érvénytelenedik – nem kell manuálisan törölni
d) Hogy a CDN gyorsabban találja meg a fájlt


5. feladat

Melyik cache stratégia a legegyszerűbb implementálni, de a legkisebb kontrollt biztosítja az adat frissessége felett?

a) Aktív invalidálás (törlés frissítéskor)
b) ETag alapú feltételes kérés
c) TTL (lejárati idő) alapú cache
d) Cache-busting hash-es URL-lel


6. feladat

Egy GET /api/user/profile endpoint visszaadja a bejelentkezett felhasználó adatait. Milyen Cache-Control beállítás a legmegfelelőbb?

a) Cache-Control: public, max-age=3600
b) Cache-Control: no-store
c) Cache-Control: public, max-age=86400
d) Cache-Control: private, max-age=86400


7. feladat

Mi az ETag HTTP fejléc funkciója?

a) Megmondja a böngészőnek, hány másodpercig tárolhatja a választ
b) Az erőforrás ujjlenyomata; ha nem változott, a szerver 304 Not Modified választ küld tartalom nélkül
c) Tiltja a CDN-ek általi cache-elést
d) Megmondja, mikor lett utoljára módosítva a fájl


8. feladat

Egy alkalmazásban minden API kérésre egy 2 másodperces, komplex SQL lekérdezés fut le. Az adat 10 percenként frissül. Az alkalmazás 500 kérést kap percenként. Mit javasolnál?

a) Optimalizálni az SQL lekérdezést indexekkel
b) Bevezeti Redis cache-t, TTL = 10 perc – az 500 kérésből csak néhány fut le valódi lekérdezésként
c) Aszinkronná tenni a lekérdezést
d) Az adatbázist felváltani NoSQL megoldással


9. feladat

Párosítsd a szituációt a megfelelő cache megközelítéssel!

SzituációCache megközelítés
A) Statikus CSS fájl, ami ritkán változik1. Cache-Control: no-store
B) Bejelentkezett felhasználó profilja2. Redis, TTL = 1 óra
C) Minden városban megjelenő landing szöveg (hetente frissül)3. Cache-Control: public, max-age=31536000 hash-es URL-lel
D) Valós idejű chat üzenetek4. Cache-Control: private, max-age=300

10. feladat

Miért mondják, hogy a cache invalidáció az egyik legnehezebb probléma a szoftverfejlesztésben?

a) Mert a cache törlése sok CPU-t igényel
b) Mert nehéz pontosan meghatározni, mikor vált elavulttá egy adat, különösen ha több helyen is cache-elve van és az adatok egymástól függnek
c) Mert a Redis és Memcached különböző protokollt használ
d) Mert a böngésző cache nem törölhető programozottan


Megoldások

  1. c – A naponta egyszer frissülő lista ideális cache jelölt: magas olvasási arány, alacsony változási arány
  1. bno-store = sehol nem tárolható, sem böngészőben, sem CDN-en (érzékeny adatoknál)
  1. c – Szerver oldali cache nem lett invalidálva a frissítéskor
  1. c – A hash a fájltartalomból generálódik; ha a tartalom változik, új hash, új URL, a böngésző nem találja cache-ben
  1. c – TTL egyszerű, de az adat az egész TTL-időtartam alatt elavult lehet
  1. b – Személyes adat, soha nem szabad sem böngészőben, sem CDN-en tárolni (no-store)
  1. b – ETag az erőforrás ujjlenyomata; változatlan tartalomnál 304 Not Modified válasz jön tartalom nélkül
  1. b – Redis cache befekteti az egyszer elvégzett munkát; 500 kérés/perc helyett csak pár lekérdezés fut az adatbázison
  1. Párosítás: A→3, B→4, C→2, D→1
  1. b – Nehéz pontosan tudni, mikor avul el az adat, különösen amikor több rendszer, több cache szint és összetartozó adatok vannak
Scroll to Top