Cache (čti „keš") je rychlá schránka, kam si odkládáš kopie často potřebných dat, abys je nemusel pokaždé znovu složitě získávat. Je to nejlevnější způsob, jak zrychlit systém a ulevit databázi — a zároveň nejspolehlivější způsob, jak si nadělat těžko dohledatelné bugy. Ne nadarmo se vtipkuje, že „v informatice jsou jen dva těžké problémy: invalidace cache a pojmenování věcí."
Pár pojmů na úvod
- Cache hit = data, která jsi hledal, byla v cache (rychlé, hotovo).
- Cache miss = v cache nebyla, musíš si je dojít pomaleji (do databáze) a uložit do cache na příště.
- Hit ratio = kolik procent dotazů obslouží cache. Vysoké = cache se vyplácí, nízké = nemá smysl.
- Stale data = zastaralá data v cache (mezitím se zdroj změnil, ale cache to ještě neví).
- Invalidace = odstranění (nebo přepsání) zastaralého záznamu z cache.
- TTL (Time To Live) = jak dlouho záznam v cache platí, než vyprší.
Proč cache vůbec funguje
Skoro každá aplikace mnohem víc čte, než zapisuje, a navíc se pár populárních věcí čte pořád dokola (úvodní stránka, oblíbený produkt). Cache drží tyhle „horké" kousky blíž a rychleji (v paměti RAM místo pomalejší databáze nebo sítě). Je to jako mít lísteček s číslem, co voláš často, nalepený na monitoru, místo abys ho pokaždé hledal v celém adresáři.
Kde všude cache je (vrstvy)
Cache není jen jedna věc — je jich po cestě požadavku hned několik:
Čím blíž uživateli, tím rychlejší a levnější — ale tím těžší je udržet ji aktuální (invalidace).
Strategie cachování — kdy se cache plní a maže
Cache-aside (nejčastější)
Aplikace se nejdřív zeptá cache, a teprve při miss jde do databáze:
Výhoda: cachuje se jen to, co se opravdu čte. Nevýhoda: úplně první přístup je vždy miss, a při souběhu hrozí nesoulad, když se zápis a invalidace prokříží.
// Čtení přes cache-aside
async function getUser(id) {
const key = `user:${id}`
const cached = await cache.get(key)
if (cached)
return JSON.parse(cached) // HIT
const user = await db.findUser(id) // MISS → do databáze
await cache.set(key, JSON.stringify(user), { ttl: 300 }) // TTL = pojistka proti stale
return user
}
// Zápis: nejdřív do DB, pak klíč SMAŽ (neaktualizuj) — míň příležitostí k nesouladu
async function updateUser(id, data) {
await db.updateUser(id, data)
await cache.del(`user:${id}`)
}
Write-through a write-behind (jen ať víš, že existují)
- Write-through — zápis jde zároveň do cache i databáze. Cache je vždy aktuální, ale zápis je o něco pomalejší.
- Write-behind — zápis jde nejdřív jen do cache a do databáze se uloží až později. Rychlé zápisy, ale riziko ztráty dat, když cache spadne dřív, než se stihne uložit. Jen pro pokročilé.
Invalidace — proč je to ta těžká část
Když se zdrojová data změní, ale v cache zůstane stará verze, uživatel uvidí zastaralou (stale) hodnotu — třeba špatnou cenu. Jak tomu bránit:
- TTL (vypršení) — nejjednodušší: záznam po nastaveném čase sám zmizí. Délku zvol podle toho, jak moc ti vadí chvilkově stará data.
- Explicitní invalidace při zápisu — při změně dat klíč z cache smažeš. Přesné, ale musíš trefit všechny klíče, kde data žijí — na jeden zapomeneš a máš záhadný bug se starými daty.
Pravidlo: při zápisu raději klíč smaž (invaliduj), než ho zkoušej aktualizovat — míň příležitostí k nesouladu.
⚠️ I „zapiš do DB → smaž klíč" má zákeřný stale-set race: souběžné čtení udělá miss, načte starou hodnotu z DB a uloží ji do cache až po tom, co ji zapisovatel smazal → v cache trvale zůstane stará. Pojistka, kterou používá skoro každý: vždy dej klíčům i TTL (krátkou expiraci), ať se případná zaseklá stará hodnota sama po chvíli zahodí. Není to neprůstřelné, ale chytí to drtivou většinu těchhle závodů.
Eviction — co dělat, když je cache plná
Cache má omezenou velikost, takže když se zaplní, musí něco vyhodit. Nejčastější pravidlo je LRU (Least Recently Used) — vyhoď to, co se nejdéle nepoužilo. (Existuje i LFU = nejméně časté apod.)
Nebezpečné jevy (ptají se na ně i na pohovorech)
- Cache stampede (nával) — populární klíč vyprší a tisíce požadavků naráz minou cache a vrhnou se na databázi → databáze lehne. Obrana: nech hodnotu po vypršení přepočítat jen jednoho (zámek / single-flight), ostatní počkají; nebo cache lehce předehřej před vypršením.
- Hot key (horký klíč) — jeden klíč (viral příspěvek) dostává neúměrně moc provozu a přetíží uzel, na kterém leží. Obrana: kopie klíče na víc uzlů, nebo malá lokální cache přímo v aplikaci. ⚠️ Lokální (in-process) cache má ale vlastní invalidační problém: když data změníš, jak řekneš všem N instancím, ať smažou svou kopii? Buď krátkým TTL, nebo pub/sub invalidací (rozešleš všem „smaž klíč X"). Lokální cache vyřeší hot key, ale zavádí per-instance stale problém.
- Cache penetration (proražení) — někdo se opakovaně ptá na neexistující klíče (často útok), takže každý dotaz projde cache a jde rovnou do databáze. Obrana: cachovat i „nic tu není" (s krátkým TTL), nebo bloom filter existujících klíčů (rychlá kontrola „tohle určitě neexistuje" ještě před databází). Samotné „cachuj null" totiž útočník obejde generováním stále nových neexistujících klíčů.
CDN — cache blízko uživateli
CDN (Content Delivery Network) je síť serverů rozmístěných po světě geograficky blízko uživatelům.
Drží kopie statického obsahu (obrázky, JS, CSS) i cachovatelných odpovědí API, takže uživatel dostane
data z nejbližšího serveru místo z tvého původního. Co a jak dlouho se cachuje, řídí hlavičky jako
Cache-Control (viz Foundations). Známé CDN služby jsou třeba Cloudflare nebo Fastly.
Co cachovat (a co ne)
- ✅ Drahé výpočty, často čtená a málo se měnící data, výsledky cizích API, statické soubory.
- ❌ Hodně proměnlivá data, kde stará hodnota škodí (zůstatek účtu, stav skladu při placení), unikátní data pro každý request (cache by stejně nikdy netrefila) a citlivá data bez kontroly přístupu (riziko, že je dostane nesprávný uživatel).
Failure modes — jak to v praxi praská
- Stale data → invalidace minula nějaký klíč. Nejčastější problém.
- Stampede → vypršení horkého klíče položí databázi.
- Cache jako jediný bod selhání → spadne Redis a všechno se hrne na databázi. Systém to musí přežít (zpomalit, ne zkolabovat).
- Nesoulad cache vs databáze → špatné pořadí zápisu a invalidace při souběhu.
🛠️ Cvičení
- Cache-aside. Naskicuj v pseudokódu čtení přes cache-aside a zápis s invalidací. Pak ukaž, kde vznikne nesoulad, když invaliduješ ve špatném pořadí vůči zápisu.
- Spočítej hit ratio. Z 10 000 požadavků jich 8 500 obslouží cache. Jaké je hit ratio a co to vypovídá o tom, jestli se ta cache vyplácí?
- Zabraň stampede. Populární klíč vyprší a tisíce požadavků se naráz vrhnou na databázi. Navrhni dvě obrany.
- Co cachovat. U pěti věcí — zůstatek účtu, seznam zemí, profil uživatele, přihlašovací token, výsledek drahého reportu — rozhodni cachovat/necachovat a s jakým TTL.
- Cache penetration. Útočník se opakovaně ptá na neexistující klíče → každý dotaz jde do databáze. Navrhni obranu.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Cache-aside — čtení: zkus cache, při miss načti z DB, ulož a vrať; zápis: zapiš do DB a klíč invaliduj (smaž). Nesoulad vznikne při souběhu, když čtenář udělá miss, načte starou hodnotu z DB a uloží ji do cache až po tom, co ji zapisovatel smazal (stale-set race) — stará hodnota pak v cache trvale zůstane. Past: vždy dej klíčům i krátké TTL, ať se zaseklá stará hodnota sama zahodí.
- Spočítej hit ratio — 8 500 / 10 000 = 85 %. To je vysoké číslo, cache obslouží většinu dotazů a evidentně se vyplácí (odlehčuje databázi). Past: hit ratio sám o sobě nestačí — i při 85 % můžou těch zbylých 15 % miss tvořit drahé dotazy, takže sleduj i latenci.
- Zabraň stampede — (1) single-flight: po vypršení nech hodnotu přepočítat jen jeden požadavek přes zámek, ostatní počkají na výsledek; (2) cache lehce předehřej (refresh) ještě před vypršením, ať klíč nikdy nevyprázdní naráz. Pointa: ať hodnotu dopočítá jeden, ne celý dav. Past: zámek měj s TTL, ať se neuvolní nikdy, kdyby držitel umřel.
- Co cachovat — zůstatek účtu ❌ (proměnlivý, stará hodnota škodí), seznam zemí ✅ (dlouhé TTL, dny), profil uživatele ✅ (krátké/střední TTL, invaliduj při změně), přihlašovací token ❌ (citlivé, riziko úniku jinému uživateli), výsledek drahého reportu ✅ (krátké TTL podle čerstvosti). Past: u sdílených dat dej pozor na cachování citlivého obsahu bez kontroly přístupu.
- Cache penetration — cachuj i „nic tu není" (negativní výsledek s krátkým TTL) a/nebo nasaď bloom filter existujících klíčů, který útok zachytí ještě před databází. Past: samotné „cachuj null" útočník obejde generováním stále nových neexistujících klíčů, proto bloom filter.
🧠 Otázky & odpovědi
Jak funguje cache-aside a kde má slabinu?
Při čtení: zkus cache → když je tam (HIT), vrať; když není (MISS), načti z databáze, ulož do cache a vrať. Při zápisu: zapiš do databáze a invaliduj (smaž) klíč. Cachuje se jen to, co se opravdu čte. Slabina: úplně první přístup je vždy miss, a při souběhu hrozí nesoulad, když se zápis a invalidace prokříží ve špatném pořadí (proto se raději invaliduje než aktualizuje).
Co je cache stampede a jak ho řešíš?
Když vyprší „horký" klíč, tisíce souběžných požadavků ho najednou minou a všechny se vrhnou na databázi → přetížení. Obrany: nech hodnotu po vypršení přepočítat jen jeden požadavek (zámek / single-flight), ostatní počkají; nebo cache lehce předehřej ještě před vypršením. Cíl: po vypršení ať hodnotu dopočítá jeden, ne celý dav.
Proč při zápisu raději cache smazat než aktualizovat?
Aktualizace cache při zápisu vytváří víc příležitostí k nesouladu (dvě souběžné aktualizace, špatné pořadí vůči databázi) a ke starým datům. Smazání klíče je jednodušší: další čtení udělá miss a načte čerstvou hodnotu z databáze. Míň stavů, které se můžou rozejít.
Co je hot key a jak ho zvládneš?
Jeden klíč (viral příspěvek, populární produkt) dostává neúměrně moc provozu a přetíží uzel, na kterém leží. Řešení: udělej kopie klíče na víc uzlů, dej malou lokální cache přímo do aplikace (před sdílený Redis), nebo provoz jinak rozlož. Jde o to nesoustředit všechno na jedno místo. (Pozor: lokální cache si zase přidá per-instance invalidační problém — řeš krátkým TTL nebo pub/sub invalidací, viz výše.)
Co se nemá cachovat?
Hodně proměnlivá data, kde stará hodnota škodí (zůstatek účtu, stav skladu při placení), data unikátní pro každý požadavek (cache by stejně nikdy netrefila) a citlivá data bez kontroly přístupu (hrozí, že je dostane nesprávný uživatel). Cache dává smysl pro často čtená, málo se měnící a draze získávaná data.
