Caching-ul este pârghia arhitecturală cu cel mai mare leverage și cu cea mai mare taxă. Leverage-ul e enorm: un layer de cache bine plasat poate absorbi între 90 și 99 la sută din traficul de citire care altfel ar lovi baza de date, iar costul hardware-ului de cache e o fracțiune din costul bazei de date pe care o protejează. Taxa este complexitatea pe care o introduce: fiecare valoare cache-uită este o copie care poate fi în dezacord cu sursa, fiecare TTL este o presupunere despre cât de stale e acceptabil, fiecare invalidare este o problemă de coordonare, iar ziua în care expiră o cheie fierbinte este ziua în care baza de date cade.
Lecția aceasta tratează caching-ul ca problemă de system design. Cele trei niveluri unde trăiesc cache-urile, cele patru pattern-uri canonice pentru a ține cache-ul în sincron cu sursa adevărului, strategiile de invalidare care există de-a lungul axei consistență-versus-corectitudine și problema de cache stampede care produce cea mai comună întrerupere legată de cache. Specificele sunt Redis, CloudFront și Memcached pentru că acestea sunt uneltele dominante în 2026, dar pattern-urile sunt agnostice față de unealtă.
Cele trei niveluri de caching
Un sistem rezonabil arhitecturat are cache-uri pe trei layere logice, fiecare absorbind o clasă diferită de trafic.
Nivelul CDN. Rețelele de content delivery (CloudFront, Cloudflare, Fastly, Akamai) cache-uiesc răspunsurile la locații edge apropiate de utilizatori. Conținutul cache-uit este orice e mai degrabă static: pagini HTML, imagini, bundle-uri JavaScript, CSS, segmente video, fișiere de fonturi, răspunsuri API cu TTL-uri lungi. Hit-urile pe CDN nu ating niciodată serverele de origine; sunt servite dintr-un nod geografic apropiat de utilizator, în zeci de milisecunde, nu sute. Nivelul CDN este cel mai rapid cache pentru că este cel mai aproape de consumator.
Economia nivelului CDN este favorabilă. După cum a tratat lecția 68, egress-ul CDN este de obicei mai ieftin decât egress-ul direct de la origine, iar rata de cache-hit reduce și mai mult încărcarea originii. O echipă care pune un CDN în fața asset-urilor statice vede de obicei și facturi mai mici și latență mai mică, fără cost de consistență (asset-urile sunt imutabile sau au reguli de revalidare bine înțelese).
Nivelul de cache al aplicației. Redis sau Memcached, plasat între aplicație și baza de date. Aplicația verifică întâi cache-ul; pe hit, returnează valoarea cache-uită; pe miss, interoghează baza de date și (în majoritatea pattern-urilor) populează cache-ul pentru următorul apelant. Cache-ul aplicației este calul de povară: cache-uiește rezultate de query costisitoare, valori calculate, date de sesiune, feature flags, contoare de rate-limit, leaderboard-uri și coada lungă de „lucruri pe care aplicația le citește mai des decât le scrie”.
Redis domină acest nivel în 2026. Memcached există încă, mai simplu și mai rapid pentru cazul pur key-value, dar structurile de date mai bogate ale Redis (sorted sets, streams, hyperloglogs, pub/sub) și opțiunile sale de persistență l-au făcut alegerea implicită. Ofertele gestionate (ElastiCache, MemoryDB, Upstash, Redis Cloud) preiau taxa operațională de a-l rula.
Nivelul de cache al bazei de date. Acesta e adesea invizibil. Postgres are un cache de rezultate de query pentru prepared statements; buffer pool-ul cache-uiește paginile citite recent; materialized views cache-uiesc rezultate pre-calculate. MySQL are propriul buffer pool și (până la 8.0) avea un query cache. Nivelul de cache al bazei de date este cel mai aproape de date și cel mai limitat ca dimensiune, dar pentru workload-ul pe care baza de date însăși îl gestionează, aceste cache-uri fac muncă reală fără ca echipa de aplicație să fie nevoită să se gândească la ele.
Cele trei niveluri formează o ierarhie. O cerere care lovește CDN-ul nu ajunge niciodată la aplicație; o cerere care lovește cache-ul aplicației nu ajunge niciodată la baza de date; o cerere care lovește buffer pool-ul bazei de date nu ajunge niciodată la disc. Fiecare layer absoarbe trafic pe care următorul ar trebui altfel să-l gestioneze. Sarcina arhitecturală este să alegi ce trăiește la care layer și cum rămâne fiecare layer consistent cu sursa.
Cele patru pattern-uri canonice
Odată ce un cache există, întrebarea este cum să ții valorile cache-uite consistente cu sursa de bază. Patru pattern-uri sunt canonul de manual. Fiecare face un trade-off diferit între simplitate, consistență și throughput de scriere.
Cache-aside, numit și lazy loading. Aplicația este responsabilă atât pentru citiri, cât și pentru scrieri. La o citire, aplicația verifică întâi cache-ul. Pe hit, returnează valoarea cache-uită. Pe miss, interoghează baza de date, populează cache-ul cu rezultatul și returnează. La o scriere, aplicația scrie în baza de date și fie invalidează intrarea din cache, fie o actualizează. Acesta este pattern-ul cel mai comun în sistemele reale și cel pe care exemplele Redis îl demonstrează cel mai des.
Avantajele: cache-ul este un layer separat, opțional; codul aplicației are controlul; modul de eșec este grațios (o întrerupere de cache înseamnă citiri mai lente, nu citiri rupte). Dezavantajele: există o fereastră între scrierea în baza de date și invalidarea cache-ului în care o valoare stale poate fi citită; codul aplicației are logica de cache împletită în el; cache miss-urile plătesc costul complet al bazei de date.
Read-through. Cache-ul aduce date din baza de date transparent. Aplicația cere cache-ului o cheie; cache-ul, pe miss, aduce din baza de date, se populează singur și returnează. Codul aplicației nu vede apelul către baza de date direct; cache-ul îl abstractizează.
Avantajul: mai puțin cod de gestionare a cache-ului în aplicație. Dezavantajul: cuplaj mai strâns între cache și baza de date; cache-ul trebuie să știe să citească din fiecare sursă de date pe care o cache-uiește; modurile de eșec sunt mai opace (un eșec de cache devine un eșec de bază de date din punctul de vedere al aplicației). Read-through este mai comun în bibliotecile de caching care stau la nivelul de acces la date (anumite ORM-uri, anumite cache-uri sidecar) decât în deployment-uri Redis brute.
Write-through. Aplicația scrie în cache, iar cache-ul scrie în baza de date sincron. Ambele sunt actualizate înainte ca scrierea să fie confirmată. Consistența puternică este păstrată: o citire după o scriere vede noua valoare fie în cache, fie în baza de date, niciodată cea veche.
Avantajul: consistență. Dezavantajul: latența de scriere este suma scrierii în cache plus scrierii în baza de date, mai lentă decât oricare singur. Write-through este potrivit pentru workload-uri intensive pe citire unde consistența este critică și throughput-ul de scriere nu este bottleneck-ul.
Write-behind, numit și write-back. Aplicația scrie în cache; cache-ul confirmă imediat și asincron face flush în baza de date. Cel mai rapid pattern de scriere, pentru că aplicația vede doar latența cache-ului.
Avantajul: throughput de scriere. Dezavantajul: dacă cache-ul cade înainte ca flush-ul să se finalizeze, datele se pierd. Write-behind este potrivit pentru workload-uri care tolerează pierderi ocazionale de date (incrementări de contoare, evenimente de analytics, click tracking) și nepotrivit pentru orice unde o scriere lipsă este o problemă de corectitudine.
O precauție practică: sistemele reale le combină. Un deployment Redis dat ar putea cache-ui un set de chei cache-aside, alt set de chei write-through și un al treilea set ca buffer write-behind pentru un event log. Pattern-urile nu sunt alegeri exclusive pentru întregul sistem; sunt unelte per use case.
Problema invalidării
Gluma lui Phil Karlton, „există doar două lucruri grele în computer science: cache invalidation și naming things”, este un meme, dar jumătatea cu invalidarea cache-ului este reală. Problema este că cache-ul ține o copie a unor date pe care sursa le poate schimba și trebuie să existe un mecanism care aduce cache-ul înapoi în sincron. Mecanismele există pe un spectru de la „ușor și cu pierderi” la „greu și corect”.
Expirare bazată pe TTL. Fiecare valoare cache-uită este stocată cu un time-to-live (60 de secunde, 5 minute, 1 oră). Când TTL-ul se scurge, cache-ul aruncă valoarea, iar următoarea citire repopulează din sursă. Cel mai simplu pattern și aproape întotdeauna cel cu care să începi.
Trade-off-ul este staleness acceptabil: între o scriere în baza de date și următoarea expirare a TTL-ului, cache-ul servește o valoare stale. Pentru multe use case-uri (un leaderboard, o listă de categorii, un feed homepage) asta este în regulă. Pentru altele (un sold de cont, o verificare de permisiune) nu.
Invalidare explicită. Când aplicația scrie în baza de date, spune și cache-ului că o anumită cheie nu mai este validă. Cache-ul evictă intrarea; următoarea citire repopulează. Mecanismul este fie un apel direct de delete pe cache, fie un mesaj publish-subscribe la care orice layer de caching se abonează.
Avantajul este corectitudinea: o scriere este urmată de o invalidare, iar citirile ulterioare văd noua valoare. Dezavantajul este că fiecare cale de cod care scrie în baza de date trebuie să-și amintească să invalideze, iar suprafața pentru bug-uri este mare. Un singur apel uitat este un bug de cache stale care poate persista pe durata de viață a intrării cache-uite.
Versionare de cache. În loc să invalidezi chei individuale, incrementezi o cheie de versiune. Fiecare intrare cache-uită are o ștampilă de versiune; la citire, cache-ul verifică dacă versiunea intrării se potrivește cu cheia curentă de versiune; dacă nu, intrarea este tratată ca un miss. Pentru a „invalida tot ce e marcat cu category 5”, incrementezi cheia de versiune pentru category 5; toate intrările cache-uite de dinaintea incrementării sunt acum stale.
Pattern-ul este util pentru a invalida grupuri de intrări greu de enumerat. Costul este o citire de cheie de versiune la fiecare citire de cache, ceea ce este ieftin dacă cheile de versiune însele sunt cache-uite local.
Invalidare event-driven. Un stream de change-data-capture din baza de date (teritoriul lecției 46) alimentează un topic Kafka; serviciile de cache se abonează și invalidează cheile afectate când sosește o schimbare relevantă. Acesta este pattern-ul cel mai curat arhitectural: sursa adevărului emite evenimente de schimbare, fiecare consumator interesat (cache-uri, indecși de search, replici downstream) ascultă, iar invalidarea este automată.
Costul este infrastructura: o conductă CDC, un topic, plumberia de consumator. Pentru un sistem care deja rulează CDC din alte motive, costul marginal de a alimenta cache-ul e mic. Pentru un sistem fără CDC, a-l construi specific pentru invalidarea cache-ului rareori merită.
Recomandarea pragmatică este pe niveluri. Folosește TTL ca baseline implicit (corectează în cele din urmă orice inconsistență). Adaugă invalidare explicită pentru cheile a căror staleness este genuin dăunătoare. Apucă-te de invalidare CDC-driven când echipa are deja infrastructura și nevoile de corectitudine ale workload-ului justifică asta.
Cache stampede-ul
Cea mai comună întrerupere legată de cache în sistemele de producție este cache stampede-ul, numit și dogpile sau thundering herd. Pattern-ul este suficient de consistent între companii încât are propriul folclor.
Setup-ul. O cheie fierbinte (feed-ul de homepage, listingul de produs popular, contorul global) este cache-uită cu un TTL. TTL-ul se scurge. În acel moment exact, sute sau mii de cereri concurente citesc cheia. Fiecare cerere ratează cache-ul. Fiecare cerere interoghează baza de date. Baza de date, care a fost dimensionată pentru încărcarea post-cache, vede o creștere bruscă de query-uri identice de la fiecare cititor concurent simultan. Se saturează. Latența urcă la secunde. Conexiunile se acumulează. Nivelul aplicației face timeout. Cache-ul se repopulează în cele din urmă, dar prin ceva care sosește prin pager-ul on-call-ului.
Cauza fundamentală este un eșec de coordonare. Nu există protocol care să spună „dacă mulți cititori ratează simultan, doar unul ar trebui să reumple”. Comportamentul implicit este „toată lumea reumple independent”.
Mitigările sunt bine cunoscute și merită implementate pentru orice cheie de cache cu trafic serios.
Single-flight refresh, numit și request coalescing. Când apare un cache miss, se ia un lock pe cheie. Cititorul care deține lock-ul aduce din baza de date și populează cache-ul. Alți cititori care ratează aceeași cheie așteaptă scurt eliberarea lock-ului, apoi citesc din cache-ul acum populat. Doar un query către baza de date se întâmplă per eveniment de miss, indiferent de concurență. Implementări există pentru Redis (folosind SET NX ca primitivă de lock), pentru framework-uri de aplicație (pachetul singleflight din Go, Caffeine din Java) și pentru bibliotecile de caching în general.
Refresh probabilistic timpuriu. Înainte ca TTL-ul să se scurgă complet, o verificare probabilistică reîmprospătează valoarea. Probabilitatea crește pe măsură ce TTL-ul se apropie de expirare. Cu acest pattern, cache-ul se repopulează la câteva citiri ghinioniste aproape de expirare în loc să aștepte ca toată lumea să rateze simultan. Lucrarea originală este Vattani et al., „Optimal Probabilistic Cache Stampede Prevention” (PVLDB 2015).
Refresh în fundal. Un job programat reîmprospătează valoarea cache-uită înainte ca TTL-ul să se scurgă, indiferent dacă cineva o cere. Cheile fierbinți nu expiră niciodată din punctul de vedere al cititorilor. Pattern-ul este potrivit pentru chei fierbinți previzibile (homepage-ul, leaderboard-ul, agregatul zilnic) și nepotrivit pentru coada lungă (milioane de chei per-utilizator ar fi imposibil de reîmprospătat pe un program).
Stale-while-revalidate. O intrare de cache trecută de TTL este returnată cititorului oricum, în timp ce un proces de fundal o reîmprospătează. Cititorul primește o valoare ușor stale; cache-ul rămâne cald. CDN-urile implementează asta nativ (directiva stale-while-revalidate a CloudFront, funcționalitatea similară a Fastly). Cache-urile de aplicație o pot implementa manual.
flowchart TD
R1[Reader 1] --> C{Cache lookup}
R2[Reader 2] --> C
R3[Reader 3] --> C
C -->|hit| HIT[Return cached value]
C -->|miss| LOCK{Acquire single-flight lock}
LOCK -->|got lock| DB[(Database query)]
LOCK -->|waiting| WAIT[Wait briefly]
DB --> POP[Populate cache, release lock]
POP --> HIT
WAIT --> C
Diagramă de creat: o versiune prietenoasă cu animația care arată scenariul de stampede în stânga (fiecare cititor lovește baza de date simultan când TTL-ul expiră) și pattern-ul single-flight în dreapta (un cititor umple cache-ul în timp ce ceilalți așteaptă). Punctul vizual este asimetria dintre cele două: încărcarea bazei de date din stânga atinge un vârf proporțional cu numărul de cititori concurenți; în dreapta, este constantă indiferent de concurență.
Dimensionarea cache-ului și eviction-ul
Un cache este prin definiție mai mic decât sursa pe care o cache-uiește. Când cache-ul este plin, ceva trebuie să cedeze. Politica de eviction decide ce.
LRU (least recently used). Evictează intrarea care nu a fost citită cea mai lungă perioadă. Implicită pentru majoritatea cache-urilor; se potrivește cazului comun unde citirile recente prezic citirile din viitorul apropiat. Implementată nativ în Redis (maxmemory-policy allkeys-lru) și Memcached.
LFU (least frequently used). Evictează intrarea cu cele mai puține accese. Mai bună pentru workload-uri cu chei fierbinți care ar trebui să rămână indiferent de accesul recent. Opțiunea este allkeys-lfu din Redis.
Random. Evictează o intrare aleasă aleator. Surprinzător de competitivă cu LRU în unele workload-uri și ieftin de implementat. allkeys-random din Redis.
Bazată pe TTL. Evictează intrarea care expiră cel mai curând. Utilă când intrările au TTL-uri semnificative și cache-ul ar trebui menținut proaspăt.
Alegerea contează rar în starea de echilibru, dar alegerea greșită produce eviction-uri corelate care arată ca eșecuri de cache. Un workload cu chei fierbinți evictate sub un LRU naiv din cauza unei inundații scurte de citiri pe chei reci este o sursă recurentă de alerte „cache-ul este degradat”.
Un exemplu lucrat
O pagină tipică de produs e-commerce ilustrează cum se îmbină layerele.
Schela HTML (header, footer, navigare, bundle-uri JavaScript) este servită de CloudFront din locații edge, cu un TTL de o oră și revalidare pe invalidare bazată pe tag-uri când se livrează un deployment.
Datele produsului (nume, descriere, URL-uri de imagini, preț de bază) sunt aduse de aplicație din Redis, care ține rândul de produs cheia după ID-ul produsului cu un TTL de 60 de secunde. La un cache miss, aplicația interoghează Postgres, populează Redis sub un lock single-flight și returnează. Când un merchandiser actualizează descrierea produsului, conducta de publicare scrie în Postgres și invalidează explicit cheia Redis.
Prețul (care depinde de locația utilizatorului, valuta și promoțiile active) este prea dinamic pentru a cache-ui rezultatul, dar input-urile (lista de promoții, cursul valutar) sunt ele însele cache-uite la nivelul aplicației cu TTL-uri mai scurte și invalidare explicită când o promoție este activată.
Recenziile (coadă lungă de trafic de citire, conținut care se schimbă lent) sunt cache-uite atât la nivelul aplicației, cât și la nivelul CDN, cu CDN-ul servind cea mai mare parte a traficului și nivelul aplicației acționând ca backstop pentru cache miss-uri.
Inventarul (numărul de unități rămase) este citit direct din Postgres fără caching, pentru că costul staleness-ului este prea mare (overselling-ul este mai scump decât încărcarea bazei de date) iar pattern-ul de citire este moderat. O echipă diferită ar putea să-l cache-uiască cu un TTL de 1 secundă, acceptând inexactitatea scurtă ca preț pentru încărcarea redusă.
Forma exemplului este forma fiecărei arhitecturi reale de caching: chei diferite la layere diferite cu politici diferite, alese per use case în funcție de costul staleness-ului, încărcarea de citire și costul unui miss.
Ce pregătește lecția aceasta
Modulul 9 este despre optimizarea costurilor, iar caching-ul este una dintre cele mai mari pârghii ale sale. O echipă care cache-uiește bine plătește pentru mai puțină capacitate de bază de date, mai puțin compute, mai puțin egress de rețea și mai puțină infrastructură sensibilă la latență per total. Următoarele lecții din Modulul 9 acoperă layout-ul de stocare, optimizarea de query-uri și disciplina FinOps care face costul o preocupare de inginerie de prim rang. Caching-ul este prima dintre acele pârghii, cea mai vizibilă arhitectural și cea mai probabilă să se compună de-a lungul anilor de operare.
Citări și lecturi suplimentare
- Andrei Vattani, Flavio Chierichetti, Keegan Lowenstein, „Optimal Probabilistic Cache Stampede Prevention”, PVLDB 2015,
http://www.vldb.org/pvldb/vol8/p886-vattani.pdf(consultat 2026-05-01). Lucrarea care a formalizat abordarea de refresh probabilistic timpuriu. - Documentația Redis, „Eviction policies”,
https://redis.io/docs/latest/operate/oss_and_stack/management/config/(consultat 2026-05-01). Referința de configurare pentru politicile discutate mai sus. - Documentația Redis, „Distributed locks with Redis”,
https://redis.io/docs/latest/develop/use/patterns/distributed-locks/(consultat 2026-05-01). Referința pentru pattern-ul single-flight la nivel Redis. - Wiki-ul Memcached,
https://github.com/memcached/memcached/wiki(consultat 2026-05-01). Pentru vărul mai simplu al Redis și baseline-ul încă relevant pentru caching key-value brut. - AWS, „ElastiCache for Redis caching strategies”,
https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Strategies.html(consultat 2026-05-01). O parcurgere practică a celor patru pattern-uri canonice într-un context AWS gestionat. - Blog Cloudflare, „Cache stampede protection”,
https://blog.cloudflare.com/(consultat 2026-05-01). Scris din perspectiva vendorului despre stale-while-revalidate și pattern-uri de edge cache. - Citatul „două lucruri grele” al lui Phil Karlton este atribuit pe scară largă; contextul original din era Netscape este documentat în wiki-ul c2 și pe bliki-ul lui Martin Fowler,
https://www.martinfowler.com/bliki/TwoHardThings.html(consultat 2026-05-01). - Michael Nygard, „Release It!”, ediția a doua (Pragmatic Bookshelf, 2018). Pattern-ul de stampede este acoperit sub tratamentul mai larg al „stability patterns”, alături de pattern-urile de bulkhead și circuit-breaker relevante pentru lecția 69.