"Només hi ha dues coses difícils en informàtica: la invalidació de caché i anomenar les coses." Aquesta cèlebre broma de Phil Karlton resumeix la paradoxa de la caché: és la tècnica més eficaç per accelerar un sistema i, alhora, una de les fonts de bugs més subtils. Una caché desa còpies de dades costoses d'obtenir en un lloc d'accés ràpid, per no recalcular-les ni tornar-les a demanar a la base de dades cada vegada. Ben feta servir, redueix la latència de mil·lisegons a microsegons i descarrega dràsticament l'emmagatzematge principal. Mal feta servir, serveix dades obsoletes, amaga errors i provoca caigudes en cascada. En aquesta lliçó estudiarem els nivells de caché, els patrons de lectura i escriptura, les estratègies d'invalidació i TTL, problemes clàssics com el cache stampede, i veurem exemples concrets amb Redis.
Contingut
- Què és una caché i per què funciona
- Nivells de caché
- Patrons de lectura: cache-aside i read-through
- Patrons d'escriptura: write-through i write-behind
- Invalidació, TTL i polítiques d'expulsió
- Problemes de la caché i com mitigar-los
- Exemple pràctic amb Redis
- Què és una caché i per què funciona
Una caché és un magatzem intermedi, ràpid i de capacitat limitada, que desa còpies de dades per servir-les sense repetir la feina d'obtenir-les de l'origen. Funciona gràcies a dos principis:
- Localitat temporal: una dada consultada ara probablement es tornarà a consultar aviat.
- Principi de Pareto: un petit percentatge de les dades concentra la majoria dels accessos (els productes més venuts, els usuaris més actius).
Les dues mètriques fonamentals són el hit ratio (percentatge de peticions servides des de la caché) i la latència. Un hit evita anar a l'origen; un miss implica el cost complet més el de desar en caché.
- Nivells de caché
La caché apareix en moltes capes d'una arquitectura. De més proper a l'usuari a més proper a la dada:
| Nivell | On viu | Exemple | Abast |
|---|---|---|---|
| Navegador / client | Dispositiu de l'usuari | Capçaleres Cache-Control |
Un usuari |
| CDN | Xarxa de vora | Cloudflare, CloudFront | Global, contingut estàtic |
| Gateway / proxy invers | Frontal del sistema | Nginx, Varnish | Totes les peticions |
| Caché d'aplicació (local) | Memòria del procés | Caffeine, Guava | Una instància |
| Caché distribuïda | Servei extern compartit | Redis, Memcached | Totes les instàncies |
| Caché de base de dades | Motor de BD | Buffer pool | Interna |
La distinció clau per a l'arquitecte és entre la caché local (in-process: raidíssima però no compartida i es perd en reiniciar) i la caché distribuïda (Redis: lleugerament més lenta per la xarxa, però compartida entre totes les instàncies i persistent). En sistemes amb diverses instàncies, la caché distribuïda evita que cadascuna mantingui còpies incoherents.
- Patrons de lectura: cache-aside i read-through
3.1 Cache-Aside (Lazy Loading)
És el patró més comú. L'aplicació gestiona la caché explícitament: mira primer a la caché i, si no hi és, va a la base i desa el resultat.
public Producto obtenerProducto(long id) {
String clave = "producto:" + id;
Producto cacheado = cache.get(clave); // 1. és a la caché?
if (cacheado != null) {
return cacheado; // HIT: retornem sense tocar la BD
}
Producto producto = repositorio.buscarPorId(id); // 2. MISS: anem a la BD
if (producto != null) {
cache.set(clave, producto, Duration.ofMinutes(10)); // 3. desem amb TTL
}
return producto;
}Pas a pas:
- Pas 1: es consulta la caché. Si hi ha hit, es retorna immediatament.
- Pas 2: en cas de miss, s'acudeix a l'origen de dades.
- Pas 3: es desa en caché amb un TTL (temps de vida) de 10 minuts per a futures peticions.
Avantatge: només s'emmagatzema en caché el que de veritat es demana (lazy). Inconvenient: la lògica de caché es barreja amb la de negoci i el primer accés sempre és lent (cache miss obligatori).
3.2 Read-Through
La caché s'encarrega de carregar la dada de l'origen quan falta; l'aplicació només parla amb la caché. La diferència amb cache-aside és de responsabilitat: aquí el codi de càrrega viu dins de la capa de caché (configurada amb un "cache loader"), no al servei.
// La caché sap com carregar el que no té; el servei només demana
LoadingCache<Long, Producto> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.build(id -> repositorio.buscarPorId(id)); // loader: s'invoca només en miss
Producto p = cache.get(42L); // si no hi és, Caffeine crida el loader per nosaltres| Aspecte | Cache-Aside | Read-Through |
|---|---|---|
| Qui carrega de l'origen | L'aplicació | La caché (loader) |
| Acoblament | Lògica de caché al servei | Encapsulada a la caché |
| Control | Màxim | Menor, més net |
- Patrons d'escriptura: write-through i write-behind
Quan les dades canvien, cal decidir com s'actualitza la caché.
4.1 Write-Through
Cada escriptura va a la caché i a la base de dades de manera síncrona, en la mateixa operació.
public void actualizarPrecio(long id, BigDecimal nuevo) {
Producto p = repositorio.buscarPorId(id);
p.setPrecio(nuevo);
repositorio.guardar(p); // 1. escriu a la BD
cache.set("producto:" + id, p, Duration.ofMinutes(10)); // 2. i a la caché
}- Avantatge: la caché mai no queda obsoleta respecte a la base; la lectura següent sempre és coherent.
- Inconvenient: cada escriptura paga el cost d'actualitzar tots dos magatzems, augmentant la latència d'escriptura.
4.2 Write-Behind (Write-Back)
L'escriptura va primer a la caché i es persisteix a la base de manera asíncrona, en diferit (en lots, després d'un retard).
- Avantatge: escriptures molt ràpides; permet agrupar i absorbir pics.
- Inconvenient greu: si la caché cau abans de bolcar a la base, es perden dades. Només apte quan es tolera aquesta pèrdua o s'assegura la durabilitat de la caché.
| Patró | Latència d'escriptura | Risc de pèrdua | Coherència caché-BD |
|---|---|---|---|
| Write-through | Alta (síncrona doble) | Baix | Forta |
| Write-behind | Baixa (asíncrona) | Alt si cau la caché | Eventual |
- Invalidació, TTL i polítiques d'expulsió
El repte central: quan deixen de ser vàlides les dades emmagatzemades en caché? Hi ha dos enfocaments complementaris.
5.1 Expiració per TTL
A cada entrada se li assigna un Time To Live: després d'aquest temps, la caché la considera caducada i la recarrega en el següent accés. És simple i autonetejant.
- TTL curt: dades més fresques, menor hit ratio.
- TTL llarg: millor hit ratio, major risc de servir dades obsoletes.
L'elecció depèn de quanta obsolescència toleri el negoci. Un catàleg pot tolerar minuts; un saldo, segons o res.
5.2 Invalidació explícita
Quan una dada canvia, esborrem o actualitzem la seva entrada de caché immediatament:
public void actualizarProducto(Producto p) {
repositorio.guardar(p);
cache.delete("producto:" + p.getId()); // invalida; el proper accés recarregarà
}Esborrar (en lloc d'actualitzar) sovint és més segur: evita desar en caché un valor a mig calcular.
5.3 Polítiques d'expulsió (eviction)
Com que la caché té capacitat limitada, quan s'omple ha d'expulsar entrades:
| Política | Criteri | Idònia quan |
|---|---|---|
| LRU (Least Recently Used) | Expulsa la menys usada recentment | Hi ha localitat temporal (l'habitual) |
| LFU (Least Frequently Used) | Expulsa la menys freqüent | Hi ha dades "calentes" estables |
| FIFO | Expulsa la més antiga | Casos simples |
| TTL/Random | Per caducitat o atzar | Quan el patró és uniforme |
- Problemes de la caché i com mitigar-los
- Cache Stampede (estampida / thundering herd): quan una entrada molt popular caduca, milers de peticions simultànies pateixen miss alhora i colpegen la base de dades a l'uníson, podent tombar-la. Mitigacions: (a) un lock o single-flight perquè només una petició recalculi mentre les altres esperen; (b) recàlcul anticipat (refrescar abans que caduqui); (c) TTL amb jitter (afegir aleatorietat perquè no caduquin totes alhora).
- Cache Penetration: consultes de claus que no existeixen a la base; mai no s'emmagatzemen en caché i sempre colpegen l'origen. Mitigació: emmagatzemar el "no existeix" (valor nul amb TTL curt) o fer servir un Bloom filter.
- Cache Avalanche: moltes entrades caduquen simultàniament (p. ex., totes amb el mateix TTL fixat en arrencar). Mitigació: TTL amb jitter i esglaonat.
- Dades obsoletes (stale): la dada va canviar a la base però la caché encara serveix la vella. És el cost inherent; es gestiona amb la combinació de TTL adequat i invalidació explícita.
- Exemple pràctic amb Redis
Redis és la caché distribuïda més usada: un magatzem clau-valor en memòria, raidíssim i compartit entre instàncies. Vegem cache-aside contra Redis amb protecció anti-stampede.
public Producto obtener(long id) {
String clave = "producto:" + id;
String json = redis.get(clave); // 1. consulta a Redis
if (json != null) {
return deserializar(json); // HIT
}
// 2. MISS: intentem adquirir un lock per evitar l'estampida
String lockKey = "lock:" + clave;
boolean conseguido = redis.set(lockKey, "1", SetParams.setParams().nx().px(3000));
if (!conseguido) {
Thread.sleep(50); // un altre fil està recalculant: esperem
return obtener(id); // reintentem: probablement ja hi sigui
}
try {
Producto p = repositorio.buscarPorId(id); // 3. només UN fil va a la BD
int ttl = 600 + new Random().nextInt(60); // 4. TTL amb jitter (600-660s)
redis.setex(clave, ttl, serializar(p)); // desa amb expiració
return p;
} finally {
redis.del(lockKey); // 5. alliberem el lock
}
}Anàlisi del codi:
- Pas 1: lectura directa de Redis amb
GET. Si hi ha valor, hit i deserialitzem. - Pas 2: davant d'un miss, intentem
SET ... NX PX 3000.NXsignifica "només si no existeix", així que només un fil aconsegueix el lock;PX 3000li dóna una caducitat de 3 s perquè el lock no quedi penjat si el fil mor. - Si no aconseguim el lock, esperem una mica i reintentem: per aleshores, el fil "guanyador" probablement ja haurà poblat la caché.
- Pas 3: només el fil amb lock consulta la base, evitant l'estampida.
- Pas 4: desem amb
SETEXi un TTL amb jitter (600 a 660 s) perquè no caduquin totes les claus alhora (anti-allau). - Pas 5: alliberem el lock al
finally, passi el que passi.
I la invalidació en actualitzar:
public void actualizar(Producto p) {
repositorio.guardar(p);
redis.del("producto:" + p.getId()); // invalida; el proper GET recarregarà des de la BD
}Errors habituals i consells
- Emmagatzemar en caché dades que canvien constantment. Si una dada canvia més ràpid que no es llegeix, la caché gairebé mai no encerta i afegeix complexitat sense benefici. Emmagatzema en caché el que es llegeix molt i canvia poc.
- No posar TTL. Una caché sense expiració acumula dades obsoletes indefinidament. Posa sempre un TTL, encara que sigui llarg, com a xarxa de seguretat.
- TTL idèntics per a totes les claus. Provoca allaus. Afegeix jitter.
- Caché local en sistemes multiinstància sense coordinar. Cada instància té la seva còpia; en invalidar-ne una, les altres continuen servint el vell. Fes servir caché distribuïda o un canal d'invalidació (pub/sub).
- Tractar la caché com a font de veritat. La caché és una còpia descartable; la base de dades és l'autoritat. El teu sistema ha de funcionar (més lent) encara que la caché es buidi sencera.
- Consell: mesura el hit ratio en producció. Una caché amb hit ratio baix no està ajudant; revisa què emmagatzemes en caché i els TTL.
Exercicis
Exercici 1. Explica la diferència entre cache-aside i read-through en termes de "qui és responsable de carregar la dada de l'origen".
Exercici 2. Un producte molt popular té TTL de 600 s. Just en caducar, 5.000 peticions arriben en el mateix segon. Descriu quin problema passa i proposa dues mitigacions concretes.
Exercici 3. Vols emmagatzemar en caché el saldo d'un compte bancari que s'ha de veure sempre actualitzat després d'una transferència. Quin patró d'escriptura i invalidació faries servir i per què? Quin patró evitaries?
Solucions
Solució 1. En cache-aside, la responsabilitat de carregar la dada de l'origen recau en l'aplicació: el codi comprova la caché, i en cas de miss consulta la base i la repobla manualment. En read-through, aquesta responsabilitat l'assumeix la mateixa caché mitjançant un loader configurat; l'aplicació només demana la dada a la caché, que internament la carrega si falta.
Solució 2. El problema és una cache stampede (thundering herd): en caducar l'entrada, les 5.000 peticions pateixen miss simultani i colpegen la base de dades alhora, podent saturar-la. Dues mitigacions: (a) un lock single-flight amb Redis SET NX perquè només una petició recalculi mentre les altres esperen i reutilitzen el resultat; (b) TTL amb jitter i/o refresc anticipat del valor abans que caduqui, de manera que mai no quedi una finestra de miss massiu.
Solució 3. Faria servir write-through (escriure caché i base de dades de manera síncrona) o, més senzill i segur, invalidació explícita: després de persistir la transferència, esborrar la clau del saldo perquè la següent lectura el recarregui actualitzat des de la base. Evitaria write-behind, perquè la seva persistència asíncrona pot perdre dades si la caché cau, cosa inadmissible en un saldo bancari. A més convindria un TTL molt curt com a xarxa de seguretat.
Conclusió
Has completat el recorregut per la caché: saps què és i per què funciona, en quins nivells apareix (de la CDN a Redis), com triar entre patrons de lectura (cache-aside, read-through) i d'escriptura (write-through, write-behind), i com gestionar la invalidació combinant TTL, esborrat explícit i polítiques d'expulsió. Coneixes a més els perills clàssics (stampede, penetration, avalanche, dades obsoletes) i com mitigar-los amb locks, jitter i caché de negatius, il·lustrat amb un exemple real en Redis. Amb aquesta lliçó tanques el Mòdul 7, en el qual has après a decidir on desar les dades (SQL vs NoSQL), com accedir-hi netament (Repository, Unit of Work, DAO), com gestionar-les en sistemes distribuïts (database per service, Sagas, CQRS) i com accelerar-ne la lectura sense sacrificar la coherència (caché). El següent mòdul ens porta fora del centre de dades propi per explorar l'arquitectura al núvol i el desplegament.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
