Arribem a la lliçó que salda tres deutes alhora. El primer el vam contreure a la 10-01: en passar a set treballadors, la memòria cau de divises, el magatzem de sessions i el comptador del limitador es van fragmentar en set còpies, i Marc es desconnecta cada dues peticions mentre un script maliciós gaudeix d'una quota multiplicada per set. El segon ve del mòdul 7: obtenirCataleg() recalcula el catàleg sencer a cada petició per retornar sempre el mateix. El tercer el va deixar la 10-02: l'organitzador de l'Auditorio Ribera continua esperant quatre segons i mig que es generin les 500 entrades de l'estrena del Festival de Jazz.
Els tres es resolen amb la mateixa peça d'infraestructura: Redis.
Contingut
- Què és Redis i quins tipus de dada farem servir
- Aixecar-lo i connectar-s'hi amb
ioredis - Memòria cau del catàleg: el patró de memòria cau a part (cache-aside)
- Claus ben dissenyades i el problema difícil: la invalidació
- Estampida de memòria cau i els seus remeis
- Què no s'ha de posar mai a la memòria cau, i mesura
- Sessions i límit de peticions compartits
- Cues de treball: el canvi de model i el 202
- BullMQ: productor, consumidor i opcions que importen
- Idempotència, treballs programats i observabilitat
- Persistència: Redis pot perdre dades
- Què és Redis i quins tipus de dada farem servir
Redis és un magatzem clau-valor en memòria. Tres característiques defineixen com se'n fa un bon ús: viu a la RAM (lectures i escriptures en microsegons, no en mil·lisegons); executa les ordres en un sol fil, igual que el nostre JavaScript, de manera que cada ordre és atòmica —no hi ha condicions de carrera entre clients— però una ordre lenta els bloqueja tots (per això KEYS * està prohibit en producció); i té estructures de dades, no només cadenes, que és on rau la seva potència de debò.
| Tipus | Ordres clau | Ús a Escena Viva |
|---|---|---|
| Cadena | GET, SET, INCR |
Catàleg serialitzat en JSON; comptadors del limitador |
| Hash | HSET, HGETALL, HINCRBY |
Dades d'una sessió d'usuari; aforament per sessió d'esdeveniment |
| Llista | LPUSH, RPOP, BRPOP |
Base de les cues de treball (BullMQ la fa servir per dins) |
| Conjunt | SADD, SISMEMBER |
Identificadors de treballs ja processats (idempotència) |
| Conjunt ordenat | ZADD, ZRANGEBYSCORE |
Treballs programats per marca de temps; rànquing de més venuts |
| TTL (transversal) | EXPIRE, TTL, SET ... EX |
Caducitat automàtica de tot l'anterior |
El TTL no és un tipus, però és la característica que converteix Redis en una memòria cau: qualsevol clau pot caducar sola, cosa que ens estalvia escriure codi de neteja.
- Aixecar-lo i connectar-s'hi amb
ioredis
ioredisPer a desenvolupament, un contenidor n'hi ha prou:
# Redis 7 amb persistencia AOF activada, al port estandard.
docker run -d --name redis-escena-viva -p 6379:6379 -v redis-dades:/data \
redis:7-alpine redis-server --appendonly yes
docker exec -it redis-escena-viva redis-cli ping # ha de respondre PONGEn un docker-compose.yml seria un servei més al costat de PostgreSQL, amb la seva image, la seva command, el seu port i el seu volum. No hi aprofundim: els contenidors i la composició es tanquen al mòdul 11. Instal·lem el client (npm install ioredis) i connectem, respectant la disciplina del projecte: src/config/index.js és l'únic que llegeix process.env.
'use strict';
const Redis = require('ioredis');
const { configuracio } = require('../config/index.js');
let clientRedis = null;
// Una unica connexio per proces. ioredis multiplexa: no cal pool.
function obtenirClientRedis() {
if (clientRedis) return clientRedis;
clientRedis = new Redis(configuracio.redis.url, {
retryStrategy: (intent) => Math.min(intent * 200, 2000), // espera creixent
maxRetriesPerRequest: null, // requisit de BullMQ
keyPrefix: `${configuracio.entorn}:`, // aisla entorns
});
// Mai no deixem que una fallada de Redis tombi el proces.
clientRedis.on('error', (error) => console.error('[redis]', error.message));
return clientRedis;
}
// tancarClientRedis() fa quit() i s'enganxa a l'aturada ordenada.
module.exports = { obtenirClientRedis, tancarClientRedis };
- Memòria cau del catàleg: el patró de memòria cau a part (cache-aside)
obtenirCataleg() és el candidat perfecte: retorna el mateix a tothom (no depèn de l'usuari ni del rol), es llegeix moltíssim i s'escriu poc, és car de calcular (agrega els 3 esdeveniments, les seves 7 sessions, l'aforament disponible i els preus) i tolera un petit desfasament, perquè la venda real es valida contra la base de dades amb SELECT ... FOR UPDATE (M7).
El patró cache-aside té quatre passos: mirar; si falla, calcular; desar amb TTL; retornar.
'use strict';
const { obtenirClientRedis } = require('../db/redis.js');
const TTL_CATALEG_SEGONS = 60;
const VERSIO_ESQUEMA = 'v2'; // es puja si canvia la forma de l'objecte
// Espai de noms : entitat : versio : parametres normalitzats
const clauDeCataleg = ({ categoria = 'totes', pagina = 1, ordre = 'data' } = {}) =>
`cataleg:esdeveniments:${VERSIO_ESQUEMA}:${categoria}:${ordre}:p${pagina}`;
// Embolcalla el repositori del M7 sense que els controladors se n'assabentin.
const crearCatalegAmbCache = ({ repositoriEsdeveniments, redis = obtenirClientRedis() }) => ({
async obtenirCataleg(parametres) {
const clau = clauDeCataleg(parametres);
let enCache = null;
// 1. Mirar. Redis caigut no pot tombar el cataleg: degradem.
try { enCache = await redis.get(clau); } catch (e) { console.error('[cache]', e.message); }
if (enCache) return { dades: JSON.parse(enCache), origen: 'cache' };
// 2. Fallada: calcular amb el repositori de sempre.
const dades = await repositoriEsdeveniments.obtenirCataleg(parametres);
// 3. Desar amb TTL ('EX' expressa la caducitat en segons).
const json = JSON.stringify(dades);
try { await redis.set(clau, json, 'EX', TTL_CATALEG_SEGONS); }
catch (e) { console.error('[cache]', e.message); }
return { dades, origen: 'base-de-dades' }; // 4. retornar
},
});
module.exports = { crearCatalegAmbCache, clauDeCataleg };L'elegància és en allò que no hem tocat. Gràcies al patró repositori del mòdul 7 i a la injecció de dependències del mòdul 9, aquest objecte exposa la mateixa interfície que src/repositoris/esdeveniments.js. A l'assemblatge canviem què s'injecta —configuracio.cache.activa ? crearCatalegAmbCache({ repositoriEsdeveniments }) : repositoriEsdeveniments— i ni els controladors, ni les rutes, ni les proves se n'assabenten. Una altra decisió important: si Redis falla, l'aplicació continua funcionant, només que més lenta. Una memòria cau no ha de ser mai un punt únic de fallada per a una lectura que es pot recalcular.
- Claus ben dissenyades i el problema difícil: la invalidació
| Part de la clau | Exemple | Per què |
|---|---|---|
| Espai de noms | cataleg: |
Permet esborrats per patró i compartir instància |
| Entitat i identificador | esdeveniments:evt-003 |
Llegible en depurar amb redis-cli |
| Versió de l'esquema | v2 |
Canviar el format invalida tot sense esborrar res |
| Paràmetres normalitzats | :data:p1 |
Dues consultes diferents no comparteixen entrada |
La versió mereix èmfasi. Si demà afegeixes el camp entradesDisponibles a l'objecte del catàleg i despleguis, els set treballadors nous llegiran objectes vells sense aquest camp i fallaran. Pujar VERSIO_ESQUEMA a v3 fa que cap clau antiga no es trobi: desplegament segur, sense FLUSHDB ni finestra de manteniment. I ara la part difícil. Hi ha una broma clàssica de Phil Karlton que diu que en informàtica només hi ha dos problemes difícils: la invalidació de memòries cau i posar noms a les coses. Descriu una cosa real: desar és trivial, saber quan allò desat ha deixat de ser cert no ho és.
| Estratègia | Com | Avantatges | Riscos |
|---|---|---|---|
| TTL curt | La clau caduca als 60 s | Simplíssim; s'autorepara | Dades obsoletes fins a 60 s; recàlculs innecessaris |
| Invalidació explícita | En vendre, s'esborra la clau | Dades fresques gairebé a l'instant | Si t'oblides d'un camí d'escriptura, serveixes dades falses per sempre |
La combinació és la pràctica sana: invalidació explícita com a camí principal, TTL com a xarxa de seguretat.
'use strict';
// Es crida des del mateix lloc que ja escolta 'venda-registrada' del
// GestorDeVendes (domini, modul 2).
const crearInvalidadorDeCataleg = ({ redis }) => ({
async invalidarPerVenda({ esdevenimentId }) {
let cursor = '0';
const claus = [];
do {
// SCAN, mai KEYS: KEYS bloqueja l'unic fil de Redis.
const [seg, lot] = await redis.scan(cursor, 'MATCH', 'cataleg:esdeveniments:*', 'COUNT', 100);
cursor = seg;
claus.push(...lot);
} while (cursor !== '0');
if (claus.length > 0) await redis.del(...claus);
await redis.del(`esdeveniment:${esdevenimentId}:fitxa:v2`); // invalidacio directa
},
});
module.exports = { crearInvalidadorDeCataleg };Si el nombre de variants creix, el SCAN deixa de ser còmode. L'alternativa professional és un conjunt d'índex (en desar una clau se n'afegeix el nom a un SET; en invalidar es llegeixen i s'esborren de cop) o, encara més simple, un comptador cataleg:generacio que formi part de la clau: en incrementar-lo, totes les anteriors queden inabastables i caduquen soles.
- Estampida de memòria cau i els seus remeis
Escenari de l'estrena: la clau del catàleg caduca a les 20:59:31. En aquell mil·lisegon hi ha 1 000 peticions en vol. Les 1 000 fallen a la memòria cau, les 1 000 criden obtenirCataleg(), les 1 000 colpegen PostgreSQL amb la consulta cara. És l'estampida de memòria cau (thundering herd), i sol tombar la base de dades justament en el pitjor moment. El primer remei és el blocatge amb SET NX: només un procés recalcula, i els altres esperen un moment i rellegeixen.
'use strict';
async function obtenirAmbBlocatge({ redis, clau, ttlSegons, calcular }) {
const enCache = await redis.get(clau);
if (enCache) return JSON.parse(enCache);
// NX: nomes s'escriu si no existeix. PX: caduca en 5 s, perque un
// proces que mori a mitges no deixi el blocatge posat per sempre.
if (await redis.set(`${clau}:blocatge`, '1', 'PX', 5000, 'NX')) {
try {
const dades = await calcular();
await redis.set(clau, JSON.stringify(dades), 'EX', ttlSegons);
return dades;
} finally { await redis.del(`${clau}:blocatge`); }
}
// No som els escollits: esperem breument i rellegim. Si encara no
// hi es, calculem igualment: millor lent que un error.
await new Promise((resoldre) => setTimeout(resoldre, 50));
const reintent = await redis.get(clau);
return reintent ? JSON.parse(reintent) : calcular();
}Recàlcul anticipat: es desa al costat del valor el seu instant de caducitat i, quan falta poc, cada lectura té una probabilitat creixent de refrescar-lo en segon pla mentre serveix l'actual, de manera que ningú no espera mai i la clau no arriba a caducar de cop. És més elegant i una mica més de codi; per a Escena Viva, el blocatge SET NX ja n'hi ha prou.
- Què no s'ha de posar mai a la memòria cau, i mesura
| Dada | Cal posar-la a la memòria cau? | Motiu |
|---|---|---|
| Catàleg públic d'esdeveniments | Sí, TTL 60 s | Igual per a tothom, car, tolera desfasament |
| Fitxa d'un esdeveniment | Sí, TTL 300 s | Canvia molt poc |
| Taxes de canvi de divises | Sí, TTL 3 600 s | Ja s'hi posava en memòria (M8); ara compartida |
| Comandes de Lucía | No | Dades personals; podrien acabar en un altre usuari |
| Respostes que depenen del rol | No (o amb el rol a la clau) | Un administrador veu camps que un assistent no |
| Aforament exacte en vendre | Mai | Vendre contra un aforament obsolet és sobrevenda |
| Tokens i contrasenyes | No | Ni a la memòria cau ni als registres (M8) |
El cas de l'aforament mereix aturar-s'hi. Al llistat, mostrar "queden 42 entrades" amb 60 segons de retard és informació orientativa acceptable. Però la decisió de vendre s'ha de prendre contra la base de dades, dins de la transacció amb SELECT ... FOR UPDATE de src/repositoris/compres-sql.js. Si algú "optimitzés" comprarEntrades llegint l'aforament de Redis, a l'estrena vendríem les mateixes butaques a dues persones. La memòria cau serveix per llegir, no per decidir. I un error de seguretat clàssic: posar a la memòria cau una resposta que inclou dades de l'usuari sense posar-ne l'identificador a la clau, amb la qual cosa Marc acaba veient les comandes de Lucía. Regla defensiva: si la resposta depèn de qui pregunta, o no es posa a la memòria cau, o el "qui" forma part de la clau.
Mateixa prova de càrrega de la lliçó 10-01, ara amb memòria cau:
Mètrica (GET /api/esdeveniments, 50 connexions, 7 treballadors) |
Sense memòria cau | Amb cache-aside |
|---|---|---|
| Peticions per segon | 3 105 | 11 840 |
| Latència p50 / p99 | 15 ms / 54 ms | 3 ms / 11 ms |
| Consultes a PostgreSQL en 10 s | 31 050 | 7 |
| Ús de CPU de PostgreSQL | 78 % | 4 % |
La xifra que de debò importa no són les peticions per segon: són les 7 consultes davant de 31 050. Hem alliberat la base de dades, el coll d'ampolla compartit que la lliçó 10-01 no podia tocar.
- Sessions i límit de peticions compartits
Saldem el deute de la lliçó 10-01 amb npm install connect-redis rate-limit-redis:
'use strict';
const session = require('express-session');
const { RedisStore } = require('connect-redis');
const rateLimit = require('express-rate-limit');
const { RedisStore: LimitStore } = require('rate-limit-redis');
const { obtenirClientRedis } = require('../db/redis.js');
const { configuracio } = require('../config/index.js');
// SESSIONS. Abans: magatzem en memoria (una copia per treballador). Ara:
// magatzem compartit; els 7 treballadors veuen la mateixa sessio.
const crearSessio = () => session({
store: new RedisStore({ client: obtenirClientRedis(), prefix: 'sessio:', ttl: 60 * 60 * 8 }),
name: 'escena_viva_sid',
secret: configuracio.sessio.secret,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: 'lax', maxAge: 1000 * 60 * 60 * 8 },
});
// LIMIT. El magatzem fa servir INCR + EXPIRE de forma atomica a Redis: el
// comptador es unic per als 7 treballadors i el limit torna a ser real.
const limitCompra = rateLimit({
windowMs: 60_000,
limit: 20,
standardHeaders: 'draft-7',
store: new LimitStore({ prefix: 'limit:compra:',
sendCommand: (...parametres) => obtenirClientRedis().call(...parametres) }),
message: { error: { codi: 'MASSA_PETICIONS', estat: 429, detalls: [],
missatge: "Has superat el limit de compres per minut." } },
});
module.exports = { crearSessio, limitCompra };Comprovat amb l'exercici 2 de la lliçó 10-01: amb limitGeneral a 20 per minut i 4 treballadors, ara la petició 21 rep 429, vingui del treballador que vingui. El mateix s'aplica a src/serveis/canvi-divises.js: substituir el seu Map en memòria per claus divises:EUR:USD amb TTL d'una hora deixa una sola crida a l'API externa per hora a tota la flota, en comptes de set.
- Cues de treball: el canvi de model i el 202
La lliçó 10-02 va treure la generació del PDF del fil principal, però l'organitzador de l'Auditorio Ribera continua esperant 4,4 segons amb la connexió HTTP oberta. I si a més cal enviar per correu les 500 entrades, parlem de minuts: s'exhaureixen els temps límit del proxy, l'usuari recarrega la pàgina i duplica la feina, i si el procés es reinicia a mitges, la feina es perd sense rastre.
sequenceDiagram
participant O as Organitzador
participant A as API
participant R as Redis (cua)
participant C as Consumidor
O->>A: POST /api/comandes/ped-77/entrades
A->>R: encuar generar-entrades
A-->>O: 202 Accepted + { treballId, estatUrl }
C->>R: prendre treball i processar (QR + PDF + correu)
O->>A: GET /api/treballs/tr-9f3 (sondeig)
A-->>O: 200 { estat: 'completat', descarregaUrl }
L'API passa de "fes això i espera" a "accepto el teu encàrrec, aquí tens el número de seguiment". Això és exactament el que significa el codi 202 Accepted, i el formalitzarem com a decisió de disseny a la lliçó 10-05.
- BullMQ: productor, consumidor i opcions que importen
BullMQ (npm install bullmq) implementa cues fiables sobre Redis fent servir llistes i conjunts ordenats, amb scripts Lua per garantir l'atomicitat. El productor viu a l'aplicació web:
'use strict';
const { Queue } = require('bullmq');
const { configuracio } = require('../config/index.js');
const cuaEntrades = new Queue('generar-entrades', {
connection: { url: configuracio.redis.url },
defaultJobOptions: {
attempts: 3, // intents maxims
backoff: { type: 'exponential', delay: 2000 }, // 2 s, 4 s, 8 s
removeOnComplete: { age: 3600, count: 1000 }, // neteja automatica
removeOnFail: { age: 24 * 3600 }, // les fallades duren un dia
},
});
// L'id del treball es el de la comanda: encuar dues vegades la mateixa
// comanda NO crea dos treballs. Idempotencia al productor.
const encuarGeneracioDEntrades = ({ comandaId, sessioId, correu }) =>
cuaEntrades.add('generar-pdf',
{ comandaId, sessioId, correu, sollicitatEl: new Date().toISOString() },
{ jobId: `entrades:${comandaId}` });
module.exports = { cuaEntrades, encuarGeneracioDEntrades };El consumidor viu en un procés a part (node src/processos/consumidor-entrades.js), no al servidor web: la feina pesada ja no comparteix màquina d'estats amb les peticions HTTP.
'use strict';
const { Worker } = require('bullmq');
const { configuracio } = require('../config/index.js');
const { PoolDeFils } = require('../treballadors/pool.js');
const { obtenirComandaPerId } = require('../repositoris/comandes.js');
const { enviarCorreuAmbEntrades } = require('../serveis/correu.js');
const { jaProcessat, marcarProcessat } = require('../serveis/idempotencia.js');
const pool = new PoolDeFils({ mida: 2 });
const consumidor = new Worker('generar-entrades', async (treball) => {
const { comandaId, correu } = treball.data;
// El mateix treball pot executar-se dues vegades (reintent despres d'una
// fallada de xarxa, o proces mort despres del treball i abans de confirmar-lo).
if (await jaProcessat(`entrades:${comandaId}`)) return { omes: true };
const comanda = await obtenirComandaPerId(comandaId);
await treball.updateProgress(20);
const { buffer, generades } = await pool.executar({
comandaId: comanda.id, esdevenimentTitol: comanda.esdevenimentTitol, entrades: comanda.entrades,
salaNom: comanda.salaNom, dataSessio: comanda.dataSessio,
});
await treball.updateProgress(70);
await enviarCorreuAmbEntrades({ destinatari: correu, adjunt: buffer });
await marcarProcessat(`entrades:${comandaId}`, 7 * 24 * 3600);
return { generades, enviatA: correu };
}, {
connection: { url: configuracio.redis.url },
concurrency: 2, // treballs simultanis aqui
limiter: { max: 30, duration: 60_000 }, // quota del proveidor de correu
});
consumidor.on('failed', (t, error) => console.error(`[cua] ${t?.id}: ${error.message}`));
// Aturada ordenada, com al M6: acabem el treball en curs.
for (const senyal of ['SIGTERM', 'SIGINT']) {
process.on(senyal, async () => { await consumidor.close(); await pool.tancar(); });
}
module.exports = { consumidor };| Opció | Què fa | Criteri |
|---|---|---|
attempts |
Reintents abans de donar-lo per fallit | 3-5 amb E/S externa; 1 si no és idempotent |
backoff |
Espera entre reintents | exponential: no matxucar un servei ja caigut |
removeOnComplete |
Neteja els treballs acabats | Sense això, Redis creix indefinidament |
removeOnFail |
Retenció dels fallits | Conserva'ls: són el teu diagnòstic |
concurrency |
Treballs alhora per consumidor | Limitat per la CPU i pel pool de fils |
limiter |
Sostre de ritme | Quotes d'APIs externes (correu, passarel·la de pagament) |
jobId |
Identitat del treball | Deduplicació al productor |
Quan un treball exhaureix els seus attempts passa al conjunt failed (la "cua de fallits"). No desapareix: queda amb la seva càrrega, el seu error i la seva pila, i es pot reintentar amb treball.retry() un cop arreglada la causa. Una alerta sobre la mida de failed és de les senyals de producció més útils que existeixen.
- Idempotència, treballs programats i observabilitat
Idempotència del consumidor. Tota cua seriosa garanteix almenys una vegada, no exactament una vegada. Si el treball envia correus, cobra o crea comandes, la duplicació és un incident real. La protecció de src/serveis/idempotencia.js és una marca a Redis: marcarProcessat fa SET processat:<clau> 1 EX <ttl> NX —atòmic, i per tant segur amb N consumidors— i jaProcessat ho comprova amb EXISTS abans d'actuar.
Millor encara quan es pot: fer l'operació naturalment idempotent. Si el PDF es desa com a comanda-ped-77.pdf i el correu es registra amb clau única per comanda, repetir el treball no fa cap mal encara que ningú no comprovi res.
Treballs programats i repetibles. Al mòdul 3 generàvem l'informe nocturn d'ocupació amb un cron del sistema i un script solt; amb BullMQ passa a formar part de l'aplicació:
const cuaInformes = new Queue('informes', { connection: { url: configuracio.redis.url } });
// Repetible: cada dia a les 03:00, hora de Madrid.
const programarInformeNocturn = () => cuaInformes.upsertJobScheduler(
'informe-ocupacio-diari',
{ pattern: '0 3 * * *', tz: 'Europe/Madrid' },
{ name: 'ocupacio', data: { abast: 'totes-les-sales' } }
);
// Retardat: recordatori 24 h abans de la sessio, amb l'opcio 'delay'.
const programarRecordatori = ({ comandaId, dataSessio }) =>
cuaInformes.add('recordatori', { comandaId },
{ delay: new Date(dataSessio).getTime() - Date.now() - 24 * 3600 * 1000 });L'avantatge sobre el cron del sistema és doble: funciona amb N treballadors sense executar-se N vegades (Redis coordina), i el treball hereta reintents, historial i observabilitat. Precisament sobre això últim, això és el mínim que cal vigilar en una cua:
| Senyal | Com s'obté | Què significa si es dispara |
|---|---|---|
| Treballs en espera | cua.getWaitingCount() |
Els consumidors no donen l'abast |
| Antiguitat del més vell | cua.getJobs(['waited'], 0, 0) |
Retard real percebut per l'usuari |
| Treballs fallits | cua.getFailedCount() |
Alguna cosa està trencada de debò |
| Treballs actius | cua.getActiveCount() |
Comparat amb concurrency, indica saturació |
Existeix bull-board per veure-ho en una interfície web; a Escena Viva n'hi ha prou d'exposar els comptadors a GET /api/salut/cues i deixar que el monitoratge del mòdul 11 els reculli.
- Persistència: Redis pot perdre dades
Redis viu en memòria i ofereix dos mecanismes de persistència, combinables:
| Mecanisme | Com funciona | Risc de pèrdua |
|---|---|---|
| RDB (instantània) | Bolca el conjunt a disc cada X canvis | Tot des de l'última instantània (minuts) |
| AOF (registre d'operacions) | Afegeix cada escriptura a un fitxer | Fins a 1 segon amb everysec |
| Tots dos | RDB per restaurar ràpid, AOF per a durabilitat | Recomanat en producció |
Fins i tot amb AOF pots perdre l'últim segon. Per tant: per a la memòria cau tant és (perdre-la només significa recalcular); per a les sessions és molest però tolerable; per a les cues importa, perquè un treball perdut és un correu que no s'envia mai. Per això la comanda s'escriu primer a PostgreSQL i el treball s'encua després: si el treball es perd, la comanda continua sent-hi i un procés de conciliació pot reencuar-lo. La regla que tanca la lliçó: la font de veritat continua sent la base de dades del mòdul 7. Redis és una capa de velocitat i de coordinació, mai el registre definitiu del negoci.
Errors Comuns i Consells
- Posar a la memòria cau sense TTL. Una clau sense caducitat és una fuita de memòria amb dades obsoletes a dins.
- Fer servir
KEYSen producció, que bloqueja l'únic fil de Redis i congela tota l'aplicació.SCAN, sempre. - Posar a la memòria cau respostes que depenen de l'usuari sense l'usuari a la clau. És una fuita de dades personals.
- Llegir l'aforament de la memòria cau per decidir una venda. Sobrevenda garantida la nit de l'estrena.
- Que una fallada de Redis tombi l'aplicació. Embolcalla les lectures en
try/catchi degrada cap a la base de dades. - Posar el consumidor de la cua dins del servidor web, o suposar que un treball s'executa una sola vegada: s'executa almenys una vegada, així que dissenya per a la repetició.
- Consell: prefixa les claus amb l'entorn (
produccio:,desenvolupament:). Evita el clàssic "he buidat la memòria cau de producció sense voler". - Consell: en desenvolupament, retorna una capçalera
X-Cache: HIT|MISS. Veure la ràtio d'encerts en viu ensenya més que qualsevol gràfica.
Exercicis
Exercici 1: mesurar la ràtio d'encerts
Instrumenta crearCatalegAmbCache per comptar encerts i fallades a Redis (INCR sobre metriques:cache:encerts i :fallades) i exposar-los a GET /api/salut/cache. Llança 2 000 peticions amb autocannon i calcula la ràtio amb TTL de 60 s i de 5 s. Interpreta la diferència.
Exercici 2: cua amb reintents i cua de fallits
Crea una cua enviar-correus el processador de la qual falli de manera determinista quan el destinatari acabi en @invalid.test. Configura 3 intents amb retrocés exponencial. Encua 5 treballs, 2 d'ells invàlids, i comprova l'estat final de cadascun i el contingut del conjunt failed.
Exercici 3: idempotència sota duplicació
Encua dues vegades el mateix treball de generació d'entrades per a ped-77, una vegada amb jobId fix i una altra sense. Comprova quantes vegades s'executa el processador en cada cas. Després, simula un reintent forçant una fallada després d'enviar el correu i verifica que la protecció d'idempotència impedeix el segon enviament.
Solucions
Exercici 1. Amb TTL de 60 s i una prova de 10 segons, la ràtio d'encerts ronda el 99,9 %: només la primera petició de cada variant falla. Amb TTL de 5 s hi ha una fallada cada 5 segons per variant, així que la ràtio baixa a ~99,4 % i les consultes a PostgreSQL es multipliquen per 12. L'aprenentatge és que el TTL és un comandament que regula l'equilibri entre frescor i càrrega, i que hi ha un punt de rendiments decreixents: passar de 60 a 600 segons a penes millora la ràtio, però multiplica per deu la finestra de dades obsoletes. Amb invalidació explícita ben posada, es pot pujar el TTL amb tranquil·litat.
Exercici 2. Els 3 treballs vàlids apareixen a completed al primer intent. Els 2 invàlids es reintenten als 2 s, 4 s i 8 s, acumulen attemptsMade: 3 i acaben a failed, conservant failedReason i la pila; cua.getFailedCount() retorna 2, i const [t] = await cua.getFailed(); await t.retry(); els torna a waiting. Detall important: amb retrocés exponencial, 3 intents consumeixen 14 segons, així que amb molts treballs fallant alhora la cua s'embussa. Per això convé combinar attempts moderats amb una alerta sobre failed.
Exercici 3. Amb jobId: 'entrades:ped-77', la segona crida a add no crea un treball nou: BullMQ retorna l'existent i el processador s'executa una sola vegada. Sense jobId es creen dos treballs, el processador s'executa dues vegades i, sense la comprovació d'idempotència, Lucía rebria dos correus amb 500 entrades cadascun. A la segona part, en fallar després de l'enviament, BullMQ reintenta i el processador arrenca de nou, però jaProcessat('entrades:ped-77') retorna true i surt amb { omes: true }. La lliçó clau: jobId protegeix contra duplicats al productor, i la marca a Redis protegeix contra reexecucions al consumidor. Calen totes dues.
Conclusió
Redis ha resolt tres problemes amb una sola peça. El catàleg se serveix des de la memòria cau amb el patró cache-aside, claus versionades, invalidació en vendre, TTL com a xarxa de seguretat i protecció contra estampides: de 3 105 a 11 840 peticions per segon, i de 31 050 consultes a PostgreSQL a 7. Les sessions i els comptadors del limitador han tornat a ser correctes amb set treballadors, saldant el deute de la lliçó 10-01. I la feina pesada ha sortit del procés web cap a una cua BullMQ amb reintents, retrocés exponencial, cua de fallits, idempotència al productor i al consumidor, treballs programats i observabilitat: l'organitzador de l'Auditorio Ribera rep ara un 202 Accepted immediat i un identificador de seguiment. També hem marcat els límits: no es posa a la memòria cau allò personal, no es decideix una venda contra la memòria cau, i Redis pot perdre dades, així que la font de veritat continua sent PostgreSQL.
Arribats aquí, Escena Viva fa servir tots els nuclis, no bloqueja el bucle d'esdeveniments, desa a la memòria cau i encua. Però totes les xifres que hem anat donant —641, 3 105, 11 840 req/s; p99 de 204, 54 i 11 ms— han aparegut amb un mètode que no hem formalitzat. A la lliçó següent, Optimització del Rendiment, convertim aquesta pràctica en disciplina: què es mesura i per què la mitjana menteix, com es dissenya una prova de càrrega honesta, com es llegeix un gràfic de flama, com es caça una fuita de memòria comparant instantànies del munt, i quin és l'ordre correcte de les palanques abans de comprar una màquina més gran.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
