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

  1. Què és Redis i quins tipus de dada farem servir
  2. Aixecar-lo i connectar-s'hi amb ioredis
  3. Memòria cau del catàleg: el patró de memòria cau a part (cache-aside)
  4. Claus ben dissenyades i el problema difícil: la invalidació
  5. Estampida de memòria cau i els seus remeis
  6. Què no s'ha de posar mai a la memòria cau, i mesura
  7. Sessions i límit de peticions compartits
  8. Cues de treball: el canvi de model i el 202
  9. BullMQ: productor, consumidor i opcions que importen
  10. Idempotència, treballs programats i observabilitat
  11. Persistència: Redis pot perdre dades

  1. 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.

  1. Aixecar-lo i connectar-s'hi amb ioredis

Per 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 PONG

En 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 };

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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 KEYS en 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/catch i 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

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats