La lliçó anterior va acabar amb una pregunta sense resposta: la bicicleta 417 té la seva matrícula, el seu model i el seu estat a PostgreSQL, i també copiats dins de milers de documents de MongoDB. L'estació 12 té onze columnes en un motor i una fitxa de trenta camps en l'altre. Ningú no ha dit quina mana.
Aquesta lliçó respon aquella pregunta, i afegeix dues peces més al sistema: Redis per a la disponibilitat en temps real i les sessions de l'aplicació, i Elasticsearch per a la cerca d'estacions per nom i adreça. Quatre motors per a un sol servei municipal de bicicletes.
És important dir des del principi quina mena de lliçó és aquesta. No redissenya res: l'esquema relacional de 08-01 i les col·leccions de 08-02 es donen per fets i no es toquen. El que es tracta aquí és el que cap de les dues lliçons anteriors no podia tractar per separat — el repartiment i la coherència: quina dada viu en quin motor, qui és la font de la veritat, amb quin retard es propaguen els canvis, què continua funcionant quan una peça cau, i com s'opera un sistema amb quatre magatzems en lloc d'un.
I tracta també la pregunta que gairebé mai no apareix a les presentacions d'arquitectura: quan desmuntar tot això. Perquè la majoria de les arquitectures poliglotes que existeixen no haurien d'existir, i saber-ho reconèixer forma part de l'ofici.
Contingut
- La persistència poliglota, portada a la pràctica
- El preu real, i per què la resposta per defecte continua sent una sola base de dades
- L'arquitectura de VallBici
- La taula mestra de decisió: qui mana sobre cada dada
- Els quatre patrons de sincronització
- La coherència a la pràctica: 4 bicis a l'app, 3 en arribar
- Modes de fallada i degradació elegant
- Operació: còpies coordinades, monitoratge i cost d'equip
- Quan desmuntar l'arquitectura poliglota
- Errors Habituals i Consells
- Exercicis
- Conclusió
- La persistència poliglota, portada a la pràctica
El concepte es va introduir a 03-04 amb la definició de Martin Fowler:
Persistència poliglota: fer servir deliberadament diversos motors d'emmagatzematge dins d'un mateix sistema, triant per a cada tipus de dada el que millor la gestiona, en lloc de forçar-ho tot en un únic motor.
Allà era una idea. Aquí és un sistema en producció, i la diferència entre les dues coses és enorme. En la idea, cada dada "viu on millor encaixa" i tot sona raonable. En el sistema real apareixen quatre preguntes que la definició no esmenta i que són el 90 % de la feina:
- Qui és la font de la veritat de cada dada? Quan dos motors discrepen, un dels dos té raó per definició, i cal haver-ho decidit abans que passi.
- Amb quina latència es propaga un canvi? No és "instantani". És 50 ms, o 3 segons, o 15 minuts, i aquella xifra ha de ser una decisió de disseny amb un requisit al darrere.
- Què passa si una còpia es perd? Un magatzem derivat que es pot reconstruir en 20 minuts és una cosa; un que conté l'únic exemplar d'una dada és una altra de completament diferent.
- Què continua funcionant quan una peça cau? Quatre motors signifiquen quatre maneres independents que el sistema falli, i un sistema on qualsevol de les quatre el tomba sencer és pitjor que un amb una sola base de dades.
Aquesta lliçó és, en el fons, aquelles quatre preguntes respostes per a VallBici.
- El preu real, i per què la resposta per defecte continua sent una sola base de dades
Abans del diagrama bonic, la factura. A 03-04 la vam enumerar; ara la quantifiquem amb el que costa de debò.
| Cost | Amb un motor | Amb quatre motors |
|---|---|---|
| Sistemes per actualitzar i apedaçar | 1 | 4, amb calendaris diferents |
| Còpies de seguretat per dissenyar i provar | 1 | 4, i coordinar-les entre si |
| Sistemes de permisos i auditoria | 1 | 4, amb models incompatibles |
| Modes de fallada independents | 1 | 4, més les fallades de la sincronització |
| Competències a l'equip | 1 de profunda | 4, o 2 de profundes i 2 de superficials |
| Depurar «aquesta dada està malament» | Mirar una taula | Esbrinar en quin dels quatre es va torçar i quan |
| Restaurar a un instant coherent | Un PITR |
Problema obert (apartat 8) |
| Onboarding d'una persona nova | Dies | Setmanes |
Aquell penúltim punt mereix èmfasi. Amb un sol motor, "restaurar el sistema a les 11:39" és una operació resolta. Amb quatre, la còpia de PostgreSQL de les 11:39, la de MongoDB de les 11:41 i l'índex d'Elasticsearch de les 11:20 no descriuen el mateix instant del món, i cal decidir què significa "coherent" en aquell context.
La resposta correcta per defecte continua sent una sola base de dades. No per conservadorisme: perquè el cost de la segona peça és permanent i el benefici sol ser puntual.
Els senyals que sí que justifiquen afegir un motor
Un motor nou es justifica quan pots emplenar aquesta frase amb dades: "tenim [número] de [dada] que exigeix [propietat], el nostre motor actual dona [mesura] i necessitem [mesura]". Si no la pots emplenar, no hi ha cas. Els senyals concrets:
| Senyal | Exemple a VallBici | Motor que el resol |
|---|---|---|
| Volum d'escriptura que el motor principal no absorbeix sense degradar-se | 272 M punts GPS/any | MongoDB |
| Latència exigida un ordre de magnitud per sota del que és assolible | Disponibilitat en < 5 ms, 60.000 vegades/dia | Redis |
| Una capacitat funcional que el motor principal no té | Cerca amb errors tipogràfics i sinònims | Elasticsearch |
| Estructura genuïnament variable que produeix migracions constants | Detall d'incidències per tipus | MongoDB |
| Dades efímeres la pèrdua de les quals és irrellevant | Sessions de l'app | Redis |
I els que no ho justifiquen, encara que se sentin sovint: "és el que fa servir tothom", "així aprenem una tecnologia nova", "el relacional no escala" (dit sense una mesura), "volem ser moderns". Cadascuna d'aquelles frases costa, en un sistema com VallBici, entre 20.000 i 60.000 euros l'any en operació i en temps d'equip.
- L'arquitectura de VallBici
flowchart TB
subgraph clients["Clients"]
APP["App mobil<br/>24.000 persones"]
OPS["Tauler d'operacions<br/>i taller"]
AJT["Informes de<br/>l'ajuntament"]
end
API["API de VallBici<br/>(la que decideix a qui pregunta)"]
APP --> API
OPS --> API
AJT --> API
API --> RD["REDIS<br/>disponibilitat per estacio<br/>sessions (EXPIRE)<br/>reserves de 10 min"]
API --> PG["POSTGRESQL · font de la veritat<br/>abonaments · trajectes · cobraments<br/>bicicletes · ancoratges · taller"]
API --> MG["MONGODB<br/>telemetria · fitxes d'estacio<br/>incidencies"]
API --> ES["ELASTICSEARCH<br/>cerca d'estacions<br/>per nom i adreca"]
PG -->|outbox + publicador<br/>~2 s| RD
PG -->|outbox + publicador<br/>~2 s| MG
MG -->|reindexat incremental<br/>~30 s| ES
PG -->|reindexat incremental<br/>~30 s| ES
IDM["IDENTITAT MUNICIPAL<br/>(compartida amb BiblioRed)"]
IDM -.->|OIDC| API
BIB["BiblioRed<br/>(sistema veí)"]
IDM -.->|OIDC| BIB
style PG stroke-width:3px
Les fletxes gruixudes van totes en el mateix sentit, i això no és casualitat: PostgreSQL escriu cap als altres i ningú no escriu cap a PostgreSQL. Un sistema on les fletxes de sincronització formen un cicle és un sistema on les dades oscil·len, i depurar-lo és un malson. Regla: el graf de sincronització ha de ser acíclic.
Què aporta cada peça
PostgreSQL — el nucli transaccional. El de 08-01, sense canvis. És l'únic motor on es cobren diners, l'únic amb integritat referencial i l'únic amb restriccions declaratives. És la font de la veritat de tot el que té conseqüències legals o econòmiques.
MongoDB — telemetria, fitxes i incidències. El de 08-02, sense canvis.
Redis — disponibilitat, sessions i reserves. La peça nova, i la que més il·lustra el criteri. L'app pregunta "quantes bicis hi ha a cadascuna de les 60 estacions?" 60.000 vegades al dia. A PostgreSQL això és un SELECT sobre estacions que retorna 60 files: costaria uns 2 ms i funcionaria perfectament. Llavors, per què Redis?
Per dos motius que sí que són números. Primer, aquella consulta es dispara cada vegada que algú obre el mapa i cada 10 segons mentre el té obert: en hora punta són 400 consultes per segon contra la mateixa base que està processant desbloquejos, i competir per connexions amb la transacció de cobrament és exactament el que no es vol. Segon, i més important: la reserva de 10 minuts de l'exercici 1 de 08-01 i la sessió de l'app són dades que caduquen soles, i EXPIRE fa en una línia el que a PostgreSQL requereix un procés de neteja.
# Disponibilitat: un hash per estació
redis> HSET est:12 lliures 4 ancoratges_lliures 20 total 24 ts 1749888000
(integer) 4
redis> HGETALL est:12
1) "lliures" 2) "4"
3) "ancoratges_lliures" 4) "20"
5) "total" 6) "24"
7) "ts" 8) "1749888000"
# Totes les estacions de cop, en una sola anada i tornada
redis> MGET est:1:lliures est:2:lliures est:3:lliures
1) "7" 2) "0" 3) "12"
# Sessió de l'app: caduca sola als 30 dies
redis> SET sess:9f3a2b... '{"persona_id":8801,"abonament_id":10233}' EX 2592000
OK
redis> TTL sess:9f3a2b...
(integer) 2591994
# Reserva de bicicleta: caduca sola als 10 minuts
redis> SET resv:bici:417 10233 EX 600 NX
OK
redis> SET resv:bici:417 10999 EX 600 NX # una altra persona intenta reservar la mateixa
(nil) # ← NX la rebutja: ja està reservadaAquell últim bloc és la joia operativa. SET ... NX és una operació atòmica de comprovar-i-fixar: la segona persona rep nil i sap que va arribar tard, sense transaccions i sense bloquejos. I la reserva caduca sola: no hi ha procés de neteja que pugui fallar. Compara-ho amb la solució de l'exercici 1 de 08-01 —índex únic parcial més caducitat implícita en la consulta— i veuràs la mateixa regla resolta amb dues eines diferents, cadascuna idiomàtica en el seu motor.
Elasticsearch — la cerca. El requisit real: algú escriu "moll nord", "passeig mol" (amb l'errada) o "estacio 12" i espera trobar l'estació 12. A PostgreSQL això és ILIKE '%...%' —que no fa servir índex, com vam veure a 06-03— o pg_trgm, que va força bé. Amb 60 estacions, pg_trgm seria suficient i cal dir-ho. Elasticsearch hi entra per una raó concreta i a termini: el pla municipal preveu 200 estacions i unificar la cerca amb la de BiblioRed i altres serveis de l'ajuntament en un sol cercador ciutadà. És el senyal "capacitat funcional que el motor principal no té" de l'apartat 2 — tolerància a errades, sinònims, ressaltat, rellevància ajustable.
La identitat municipal, compartida amb BiblioRed. Vallmar té un proveïdor d'identitat únic: la mateixa persona entra a BiblioRed per reservar un llibre i a VallBici per desbloquejar una bici amb les mateixes credencials. És una dada compartida entre dos sistemes de la plataforma municipal, i aquí cal ser molt estricte amb una cosa: VallBici no guarda contrasenyes ni credencials. Guarda un subject_id del proveïdor d'identitat a persones_abonades i res més. Si l'ajuntament canvia de proveïdor, VallBici canvia una columna; si VallBici guardés credencials, una fallada seva comprometria també BiblioRed.
- La taula mestra de decisió: qui mana sobre cada dada
Aquest és el document més important del sistema. Si un equip amb arquitectura poliglota no el té escrit, no té arquitectura: té quatre bases de dades.
| Conjunt de dades | Font de la veritat | Còpies a | Latència de sincronia | Si es perd la còpia |
|---|---|---|---|---|
| Persones abonades i abonaments | PostgreSQL | — | — | No aplica |
| Trajectes (obertura i tancament) | PostgreSQL | Mongo (només l'_id) |
~2 s | Irrellevant |
| Cobraments i imports | PostgreSQL | — | — | No aplica |
| Inventari de bicicletes | PostgreSQL | Mongo (matrícula, model, tipus) | ~2 s | Reconstruïble des de PG |
| Ocupació d'ancoratges | PostgreSQL | Redis (comptador agregat) | < 1 s | Reconstruïble en 200 ms |
| Sessió de l'app | Redis | — | — | Es perd: cal tornar a entrar |
| Reserva de 10 minuts | Redis | — | — | Es perd: la reserva s'anul·la |
| Telemetria GPS | MongoDB | — | — | Es perd de debò |
| Fitxa enriquida d'estació | MongoDB | ES (nom, adreça, districte) | ~30 s | Reconstruïble des de Mongo |
| Dades bàsiques d'estació (codi, ancoratges, districte) | PostgreSQL | Mongo, Redis, ES | ~2 s / ~30 s | Reconstruïble des de PG |
| Detall d'incidències | MongoDB | — | — | Es perd de debò |
| Ordre de taller (fet comptable) | PostgreSQL | Mongo (l'ordre_taller_id) |
~2 s | Irrellevant |
| Índex de cerca | Elasticsearch (derivat) | — | ~30 s | Reconstruïble en ~2 min |
Tres lectures que cal fer d'aquesta taula:
Primera: la columna de l'esquerra té un sol valor per fila, sempre. No existeix "les dues". Si dos motors poguessin modificar la mateixa dada, caldrien regles de resolució de conflictes, i això és un problema molt més difícil del que ningú no vol tenir en un sistema municipal.
Segona: hi ha tres files amb "es perd de debò". Telemetria, detall d'incidències, i —parcialment— sessions i reserves. Aquelles són les que necessiten còpia de seguretat pròpia. Tota la resta és derivat i es reconstrueix. Una dada derivada no necessita còpia de seguretat; necessita un procediment de reconstrucció provat. Confondre les dues coses fa que es facin còpies cares del que no importa i cap del que sí.
Tercera: l'ocupació d'ancoratges té la seva veritat a PostgreSQL, no a Redis. És contraintuïtiu —Redis és qui la serveix— i és la decisió que sosté tot l'apartat 6. Redis té una còpia ràpida i aproximada; la veritat és a la taula ancoratges, protegida per la seva clau primària i el seu UNIQUE. Quan les dues discrepen, guanya PostgreSQL, sense excepcions.
Un cas especial mereix nota: l'estació apareix en quatre motors. Les seves dades bàsiques neixen a PostgreSQL (codi, ancoratges, districte), la seva fitxa enriquida neix a MongoDB (fotos, accessibilitat, horaris), la seva disponibilitat viu a Redis i el seu text cercable a Elasticsearch. Quatre motors, un sol objecte del món. I funciona perquè cada camp té un únic amo: ningú no edita el nombre d'ancoratges des de MongoDB, i ningú no edita les fotos des de PostgreSQL. La regla no és "cada entitat en un motor"; és "cada camp, un amo".
- Els quatre patrons de sincronització
Com arriba un canvi de PostgreSQL als altres. Hi ha quatre maneres de fer-ho i només una és dolenta.
Patró 1 — Escriptura dual (i per què és fràgil)
L'aplicació escriu als dos llocs, un darrere l'altre.
await pg.query('UPDATE ancoratges SET bicicleta_id = NULL WHERE ...');
await redis.hincrby('est:12', 'lliures', -1); // ← i si falla aquí?sequenceDiagram
participant API
participant PG as PostgreSQL
participant RD as Redis
API->>PG: UPDATE ... COMMIT
PG-->>API: OK
API->>RD: HINCRBY est:12 lliures -1
Note over API,RD: 💥 l'API cau aqui
Note over RD: Redis diu 4 bicis · PostgreSQL diu 3 · per sempre
El problema no és que falli: és que falla en silenci i no es recupera. No hi ha cap transacció que abasti els dos motors, així que la incoherència queda. I no val invertir l'ordre: si s'escriu primer a Redis i falla PostgreSQL, Redis anuncia una bici que no s'ha desbloquejat.
Es fa servir igualment, en un cas molt concret: quan la còpia és reconstruïble i el desfasament és tolerable. La disponibilitat a Redis compleix les dues condicions, així que VallBici sí que fa escriptura dual cap a Redis… acompanyada d'un reconstructor periòdic que corregeix les derives. Escriptura dual sola, sense xarxa de seguretat, és el que no es fa mai.
Patró 2 — Outbox
La idea: si no es pot fer una transacció entre dos motors, se'n fa una dins d'un que inclogui la intenció d'escriure a l'altre.
CREATE TABLE outbox (
esdeveniment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
agregat VARCHAR(20) NOT NULL, -- 'trajecte', 'bicicleta', 'estacio'
agregat_id BIGINT NOT NULL,
tipus VARCHAR(30) NOT NULL, -- 'trajecte_iniciat', 'bici_a_taller'
payload JSONB NOT NULL,
ts_creat TIMESTAMPTZ NOT NULL DEFAULT now(),
ts_publicat TIMESTAMPTZ
);
CREATE INDEX idx_outbox_pendents ON outbox (esdeveniment_id) WHERE ts_publicat IS NULL;La transacció de desbloqueig de 08-01 guanya un INSERT més, dins del mateix BEGIN:
BEGIN;
UPDATE ancoratges SET bicicleta_id = NULL WHERE estacio_id = 12 AND numero = 3;
UPDATE bicicletes SET estat = 'en_us' WHERE bicicleta_id = 417;
INSERT INTO trajectes (...) VALUES (...) RETURNING trajecte_id; -- 884213
INSERT INTO outbox (agregat, agregat_id, tipus, payload) VALUES
('trajecte', 884213, 'trajecte_iniciat',
'{"trajecte_id":884213,"bicicleta_id":417,"matricula":"VB-0417",
"model":"Ciclmar E-Vall","tipus":"electrica","estacio_origen":12}'),
('estacio', 12, 'disponibilitat_canviada', '{"estacio_id":12,"delta":-1}');
COMMIT;I un publicador independent:
-- S'executa cada 500 ms. SKIP LOCKED permet diversos publicadors en paral·lel (06-02).
BEGIN;
SELECT esdeveniment_id, tipus, payload FROM outbox
WHERE ts_publicat IS NULL ORDER BY esdeveniment_id
FOR UPDATE SKIP LOCKED LIMIT 100;
-- ... escriu a MongoDB i Redis ...
UPDATE outbox SET ts_publicat = now() WHERE esdeveniment_id = ANY($1);
COMMIT;La garantia exacta que dona l'outbox, dita amb precisió: si la transacció de negoci va confirmar, l'esdeveniment és a la taula outbox, perquè es va escriure en la mateixa transacció. Per tant es publicarà: si el publicador cau, en reiniciar-se el troba pendent. El que no garanteix és que es publiqui una sola vegada: si escriu a MongoDB i mor abans de l'UPDATE, en reintentar-ho el publicarà de nou. És un lliurament almenys una vegada, i per això totes les escriptures destí han de ser idempotents — que és exactament per això que a 08-02 vam fer servir el trajecte_id com a _id de MongoDB. Les peces encaixen.
Patró 3 — Captura de canvis (CDC)
En lloc que l'aplicació declari què ha canviat, es llegeix el registre d'escriptura anticipada del mateix motor. És el mateix WAL de 06-01: el fitxer on PostgreSQL anota cada canvi abans d'aplicar-lo, i que allà servia per a la durabilitat i el PITR. Aquí se li dona un segon ús.
-- Publicació lògica: PostgreSQL emet els canvis d'aquestes taules
CREATE PUBLICATION vallbici_cdc FOR TABLE ancoratges, bicicletes, estacions;
SELECT pg_create_logical_replication_slot('vallbici_slot', 'pgoutput');Un connector (Debezium i similars) se subscriu al slot i produeix, per a cada canvi, un esdeveniment amb l'abans i el després:
{ "op": "u", "source": { "lsn": 47118293, "ts_ms": 1749888000123 },
"before": { "estacio_id": 12, "numero": 3, "bicicleta_id": 417 },
"after": { "estacio_id": 12, "numero": 3, "bicicleta_id": null } }L'avantatge sobre l'outbox és que l'aplicació no hi participa. Ningú no es pot oblidar d'emetre un esdeveniment, ni tan sols un UPDATE fet a mà per una persona administradora a les 3 de la matinada. L'inconvenient és que els esdeveniments són canvis de files, no fets de negoci: l'esdeveniment de dalt diu "l'ancoratge 3 va passar a null", no "la persona 8801 va desbloquejar una bici elèctrica". Reconstruir el significat a partir de les files és feina, i aquella feina s'acobla a l'esquema: canviar una columna trenca el consumidor.
Avís operatiu que ha tombat més d'un sistema: un slot de replicació lògica amb un consumidor caigut fa que PostgreSQL conservi WAL indefinidament per poder-l'hi servir quan torni. Si el consumidor porta tres dies aturat, el disc s'omple i el motor s'atura. Un slot oblidat és una bomba de rellotgeria, i el seu monitoratge (pg_replication_slots) és obligatori.
Patró 4 — Reconstrucció periòdica
El més simple i el més infravalorat: cada cert temps, es recalcula el magatzem derivat des de zero o es compara amb la veritat.
-- Reconstruir la disponibilitat de les 60 estacions. S'executa cada 5 minuts.
SELECT e.estacio_id,
COUNT(*) FILTER (WHERE a.bicicleta_id IS NOT NULL AND a.estat = 'operatiu') AS lliures,
COUNT(*) FILTER (WHERE a.bicicleta_id IS NULL AND a.estat = 'operatiu') AS buits
FROM estacions e JOIN ancoratges a USING (estacio_id)
GROUP BY e.estacio_id; estacio_id | lliures | buits
------------+---------+-------
12 | 4 | 20
41 | 0 | 20
...
(60 rows · 8 ms)Vuit mil·lisegons per reconstruir l'estat complet de Redis. Amb aquella xifra sobre la taula, la conclusió és forta: qualsevol deriva del comptador es corregeix sola en menys de cinc minuts i ningú no arriba a notar-ho. Quan la reconstrucció completa és barata, la sincronització incremental es pot permetre ser imperfecta — i això simplifica tota la resta.
Per a Elasticsearch, la reconstrucció completa (60 documents, o 200 en el futur) triga uns dos minuts i es fa amb índex paral·lel i canvi d'àlies, de manera que la cerca no s'interromp:
$ curl -XPOST localhost:9200/_aliases -d '{"actions":[
{"remove":{"index":"estacions_v3","alias":"estacions"}},
{"add": {"index":"estacions_v4","alias":"estacions"}}]}'
{"acknowledged":true}La taula que cal recordar
| Patró | Consistència | Complexitat | Latència típica | Quan fer-lo servir |
|---|---|---|---|---|
| Escriptura dual | Cap garantia; deriva silenciosa | Molt baixa | Immediata | Només si la còpia és reconstruïble i hi ha reconstructor |
| Outbox | Almenys una vegada, sense pèrdues | Mitjana (taula + publicador) | 0,5-3 s | El cas general. L'opció per defecte |
| CDC | Almenys una vegada, sense pèrdues ni oblits | Alta (connector, slot, operació) | 0,1-1 s | Molts consumidors o escriptures fora de l'aplicació |
| Reconstrucció | Convergència garantida, amb retard | Baixa | Minuts o hores | Xarxa de seguretat dels altres tres, sempre |
VallBici fa servir outbox com a mecanisme principal cap a MongoDB i Redis, escriptura dual com a drecera ràpida cap a Redis per a la disponibilitat, i reconstrucció periòdica de Redis (5 min) i d'Elasticsearch (nocturna) com a xarxa de seguretat. CDC es descarta avui: dos consumidors no justifiquen operar un connector i vigilar un slot. Si demà l'ajuntament vol un magatzem analític i un sistema d'alertes llegint el mateix, la decisió canvia — i això també és correcte.
- La coherència a la pràctica: 4 bicis a l'app, 3 en arribar
Ara el problema concret, que és el que la gent pateix.
L'app diu que a l'estació 12 hi ha 4 bicicletes. La persona camina set minuts, hi arriba i n'hi ha 3. És un error del sistema?
No. I entendre-ho bé és la diferència entre dissenyar un sistema distribuït i patir-lo. Aquella dada mai no va poder ser exacta, ni tan sols amb un sol motor: entre l'instant en què PostgreSQL la va llegir i l'instant en què la persona va mirar la pantalla han passat 200 ms, i en aquells set minuts de caminada altres persones han desbloquejat bicis. El desfasament de Redis (< 1 s) és menyspreable davant del desfasament intrínsec de set minuts.
Aquesta és la formulació útil:
La pregunta no és si la dada està desfasada. Ho està sempre. La pregunta és si el desfasament que introdueix l'arquitectura és petit comparat amb el desfasament que ja hi havia.
Amb aquell regle de mesurar, la taula queda clara:
| Dada | Desfasament intrínsec | Desfasament que afegeix l'arquitectura | Acceptable? |
|---|---|---|---|
| Bicis lliures al mapa | Minuts (el temps d'arribar-hi) | < 1 s | Sí, folgadament |
| Fitxa de l'estació | Dies | ~2 s | Sí |
| Resultats de cerca | Dies | ~30 s | Sí |
| Bicis lliures en el moment de desbloquejar | Zero | — | No. Aquí no hi cap desfasament |
| Import cobrat | Zero | — | No |
Les dues últimes files són les que governen el disseny.
Com es dissenya perquè el desfasament sigui acceptable
1. La reserva de curta durada. Converteix una promesa incerta en un compromís ferm. La persona veu 4 bicis, prem "reservar", i a partir d'aquell moment té una bici garantida durant 10 minuts. La reserva es pren a Redis amb SET ... NX EX 600, que és atòmica — però només després de confirmar-la contra PostgreSQL:
-- Es confirma contra la font de la veritat, no contra el comptador de Redis
BEGIN;
SELECT a.estacio_id, a.numero, a.bicicleta_id
FROM ancoratges a JOIN bicicletes b USING (bicicleta_id)
WHERE a.estacio_id = 12 AND a.estat = 'operatiu' AND b.estat = 'ancorada'
FOR UPDATE OF a SKIP LOCKED LIMIT 1;
COMMIT;Si PostgreSQL retorna 0 files, l'app diu "ho sentim, algú s'acaba d'endur l'última" — i actualitza Redis de passada. El comptador ràpid serveix per pintar el mapa; la veritat es consulta en comprometre's.
2. La confirmació en el desbloqueig. El mateix principi en el moment crític. L'app mai no desbloqueja basant-se en el comptador de Redis. El desbloqueig és la transacció de 08-01, íntegra, contra PostgreSQL, amb el seu FOR UPDATE SKIP LOCKED. Si dues persones demanen l'última bici, en guanya una i l'altra rep un error correcte. Redis no participa en aquella decisió.
3. Marca de temps visible. L'app mostra "actualitzat fa 3 s". És una decisió de producte que canvia la percepció: una dada amb antiguitat declarada es llegeix com una estimació, no com una promesa.
sequenceDiagram
participant P as Persona
participant API
participant RD as Redis
participant PG as PostgreSQL
P->>API: obrir mapa
API->>RD: MGET est:*:lliures
RD-->>API: rapid, aproximat (< 5 ms)
API-->>P: "estacio 12 · 4 bicis · fa 3 s"
P->>API: reservar
API->>PG: SELECT ... FOR UPDATE SKIP LOCKED
PG-->>API: bici 417 - veritat confirmada
API->>RD: SET resv:bici:417 ... NX EX 600
API-->>P: "bici VB-0417 reservada · 10 min"
Note over API,PG: El cobrament MAI no surt de PostgreSQL
Per què la transacció de cobrament mai no surt de PostgreSQL
Mereix un apartat propi perquè és la regla que no admet excepcions.
Tancar un trajecte toca quatre coses —el trajecte, l'ancoratge, la bicicleta i el cobrament— i les quatre han de canviar totes o cap. Una fallada a mitges que deixés el trajecte tancat sense cobrament és diners perduts; una que deixés el cobrament sense tancar el trajecte és un cobrament duplicat en el tancament següent. L'atomicitat de 06-01 no és un luxe aquí: és el requisit.
I no existeix cap transacció que abasti quatre motors. Els protocols de confirmació en dues fases existeixen, i a la pràctica s'eviten: bloquegen recursos en tots els participants fins que el coordinador decideix, i si el coordinador cau, els participants es queden bloquejats. Ningú no vol això a la ruta de cobrament d'un servei municipal.
La conseqüència de disseny és directa i cal respectar-la sense excepcions:
Tot el que participa en una mateixa transacció de negoci viu en el mateix motor. Si dues dades han de canviar atòmicament, no es reparteixen. I si algú proposa repartir-les, la resposta correcta és redissenyar el repartiment, no inventar una transacció distribuïda.
Per això l'outbox és a PostgreSQL i no en una cua externa: és l'única manera que "el trajecte es va tancar" i "cal avisar els altres motors" siguin atòmics entre si.
- Modes de fallada i degradació elegant
Quatre motors són quatre maneres de fallar. La pregunta de disseny no és "com evitem que caiguin?" —cauran— sinó "què continua funcionant quan cadascun cau?".
Un sistema poliglot ben dissenyat es degrada per capes. Un de mal dissenyat cau sencer quan falla la peça menys important, i llavors és objectivament pitjor que un monòlit amb una sola base de dades.
| Peça que cau | Què deixa de funcionar | Què continua funcionant | Pla de contingència | Gravetat |
|---|---|---|---|---|
| Redis | Mapa ràpid, sessions, reserves | Tota la resta: desbloqueig, ancoratge, cobrament, taller, informes | L'API passa a consultar disponibilitat a PostgreSQL (8 ms, 60 files); les sessions caduquen i cal tornar a entrar; les reserves es desactiven | Baixa |
| Elasticsearch | Cerca per text lliure | Tota la resta; la llista d'estacions per districte i el mapa continuen | L'app passa a filtrar per districte i a ILIKE sobre les 60 estacions de PostgreSQL |
Molt baixa |
| MongoDB | Fitxa enriquida, telemetria, detall d'incidències | Desbloqueig, ancoratge i cobrament: intactes | La fitxa mostra les dades bàsiques de PostgreSQL; la telemetria s'acumula a la passarel·la i es bolca en tornar; les incidències s'obren només com a ordre de taller | Mitjana |
| PostgreSQL | Desbloquejar, ancorar, cobrar, donar d'alta abonaments — el servei | Consulta del mapa (des de Redis, congelat), cerca, fitxes | Mode només lectura declarat; l'app avisa; no s'obren trajectes | Crítica |
La fila de Redis és la que valida l'arquitectura. Redis és la peça que més peticions atén i la que menys importa: si cau, el sistema es posa una mica més lent i continua cobrant. Aquella asimetria —la peça més sol·licitada és la més prescindible— és senyal d'un bon repartiment. El senyal contrari, un motor auxiliar que tomba el cobrament, indica que hi ha dades al lloc equivocat.
El que no s'ha de fer quan cau MongoDB: escriure la telemetria a PostgreSQL "provisionalment". Sona solidari i és un desastre: crea un segon camí d'escriptura sense provar, fica 272 milions de files l'any a la base que sosté el cobrament i deixa dades en dos formats que algú haurà de reconciliar. La resposta correcta és acumular a la passarel·la amb un límit i descartar el més antic si s'omple. La telemetria és valuosa; no val una caiguda del cobrament.
I el que sí que s'ha de fer quan cau PostgreSQL: declarar-ho. Un sistema que continua acceptant desbloquejos "a cegues" amb la promesa de registrar-los després regalarà bicicletes i cobrarà malament. En mode només lectura, l'app mostra el mapa i diu clarament que no es poden iniciar trajectes. Fallar de manera visible i honesta és una decisió de disseny, no una rendició.
- Operació: còpies coordinades, monitoratge i cost d'equip
El problema de restaurar a un instant coherent
Cada motor té la seva estratègia de còpia:
| Motor | Estratègia | Freqüència | Granularitat de restauració |
|---|---|---|---|
| PostgreSQL | Còpia base + WAL arxivat (PITR) | Contínua | Qualsevol instant |
| MongoDB | Snapshot del sistema de fitxers + oplog | Cada 6 h + continu | Qualsevol instant dins de la finestra de l'oplog |
| Redis | Cap còpia de la disponibilitat; RDB diari de les sessions | Diària | Aproximada, i tant se val |
| Elasticsearch | Cap: es reconstrueix | — | No aplica |
I aquí hi ha el problema que no té solució perfecta: restaurar a les 11:39 no significa el mateix en els quatre motors. PostgreSQL pot tornar exactament a les 11:39; MongoDB pot tornar a les 11:39 si l'oplog hi arriba; Redis tornarà a un estat d'ahir que ja no val.
L'estratègia de VallBici, i el raonament:
- PostgreSQL es restaura a l'instant exacte. És la veritat. Tota la resta es defineix respecte a ell.
- MongoDB es restaura a un instant igual o posterior. Si MongoDB té telemetria de trajectes que, després de la restauració, PostgreSQL ja no coneix, són documents orfes — molestos però inofensius. A l'inrevés (Mongo anterior a PG) faltaria telemetria de trajectes existents, que es detecta però no es recupera. Davant del dubte, que sobri informació als derivats, no que en falti.
- Redis es buida i es reconstrueix. Els 8 ms de l'apartat 5 fan que aquesta decisió sigui trivial. Les sessions es perden: 24.000 persones tornen a entrar. És molest i és acceptable.
- Elasticsearch es reindexa des de zero. Dos minuts.
- S'executa una reconciliació completa abans de tornar a obrir el servei, i se'n registra el resultat.
# Guió de restauració coordinada, resumit
$ pg_ctl stop && pg_restore_pitr --target-time "2026-06-14 11:39:00+02"
$ mongorestore --oplogReplay --oplogLimit 1749893999
$ redis-cli FLUSHALL
$ ./reconstruir_disponibilitat.sh # 60 estacions des de PostgreSQL
$ ./reindexar_elasticsearch.sh --alias-swap
$ ./reconciliar.sh --informe /var/log/vallbici/reconciliacio-20260614.txt[reconciliar] bicicletes orfes a MongoDB .............. 3 [reconciliar] trajectes sense telemetria .............. 128 [reconciliar] comptadors Redis divergents ............. 0 [reconciliar] documents ES absents .................... 0 [reconciliar] RESULTAT: divergencies tolerables · servei a punt
Aquelles 128 traces de telemetria que falten són el cost real de la restauració, i està bé que apareguin en un informe en lloc de descobrir-se per casualitat sis mesos després.
Monitoratge mínim
Amb quatre motors, el monitoratge de cadascun per separat és necessari però no suficient. L'específic de l'arquitectura poliglota és vigilar el que hi ha entre ells:
| Mètrica | Llindar d'alerta | Per què |
|---|---|---|
Antiguitat de l'esdeveniment més vell sense publicar a outbox |
> 60 s | El publicador està caigut o encallat |
Files pendents a outbox |
> 5.000 | S'acumula més ràpid del que es publica |
| Divergències en la reconstrucció de Redis | > 2 estacions | L'escriptura dual està fallant |
| Retard de reindexat d'Elasticsearch | > 5 min | La cerca retorna dades velles |
| Retenció de WAL per slots de replicació | > 5 GB | La bomba de l'apartat 5.3 |
| Orfes detectats a la reconciliació nocturna | > 10 | Alguna cosa s'està trencant a poc a poc |
Les tres primeres no les dona cap motor: són pròpies de l'arquitectura i cal instrumentar-les a mà. Un equip que monitora quatre motors impecablement i no vigila la cua de l'outbox té un punt cec just on passen les fallades característiques d'aquest disseny.
El cost d'equip
La part que no apareix als diagrames. Per a VallBici, amb quatre motors, l'equip necessita competència real en:
- PostgreSQL: modelatge, plans d'execució, transaccions, PITR. Profunda, no negociable.
- MongoDB: modelatge documental, pipelines d'agregació, índexs, conjunts de rèplica. Profunda.
- Redis: estructures de dades, caducitat, persistència i els seus límits. Superficial n'hi ha prou.
- Elasticsearch: analitzadors, mappings, reindexat sense tall. Mitjana.
- La sincronització: outbox, idempotència, reconciliació. És la competència que ningú no té al currículum i la que més falta fa.
En un equip petit això significa, a la pràctica, que dues persones esdevenen imprescindibles i que les vacances d'agost són un risc operatiu. És un cost real i cal posar-lo sobre la taula quan es decideix l'arquitectura, no descobrir-lo després.
- Quan desmuntar l'arquitectura poliglota
Aquesta part gairebé mai no s'escriu, i és la que més diners estalvia.
Els senyals que sobra un motor
| Senyal | Què indica |
|---|---|
| El motor auxiliar guarda menys dades de les que va costar desplegar-lo | S'hi va afegir per entusiasme, no per necessitat |
| La meitat de les consultes al motor auxiliar acaben consultant també el principal | El repartiment està malament: aquelles dades van juntes |
| Ningú no ha mirat el tauler d'aquell motor en tres mesos | No està resolent cap problema visible |
| Cada incidència comença amb "en quin dels quatre és el problema?" | El cost de depuració supera el benefici |
| El procés de sincronització té més codi que la funcionalitat que sosté | Clàssic. És el moment de parar |
| El volum que va justificar el motor s'ha estancat molt per sota del previst | La premissa era falsa |
| Només una persona de l'equip el sap operar | Risc operatiu més gran que el benefici |
Aplicat a VallBici, amb honestedat: Elasticsearch és el candidat. Es va justificar per un pla de 200 estacions i per un cercador municipal unificat. Si d'aquí a dos anys continuen sent 60 estacions i el cercador unificat no s'ha fet, Elasticsearch estarà indexant 60 documents que pg_trgm serviria igual de bé, i haurà costat dos anys d'operació, actualitzacions i monitoratge. La decisió correcta llavors és treure'l, no defensar-lo perquè ja hi és.
Com es torna enrere, sense drama
L'ordre importa, i és més fàcil del que sembla si el motor era derivat:
- Comprovar que és derivat. Si la seva font de la veritat és un altre motor, es pot apagar sense perdre res. Si conté dades originals, primer cal migrar-les i això és un altre projecte.
- Implementar el camí alternatiu al motor principal. Per a la cerca: índex GIN amb
pg_trgmsobrenomiadreca. - Executar en paral·lel i comparar resultats durant dues o tres setmanes. Registrar les consultes on discrepen i decidir si la diferència importa.
- Canviar el trànsit, deixant el motor vell encès i sincronitzat.
- Esperar. Dues setmanes sense incidències.
- Apagar, i esborrar el codi de sincronització. Aquest pas és el que s'oblida: deixar el publicador escrivint en un motor apagat produeix errors al registre que algú perseguirà durant mesos.
-- El camí alternatiu per a la cerca, a PostgreSQL
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_estacions_cerca ON estacions
USING gin ((nom || ' ' || adreca) gin_trgm_ops);
SELECT codi, nom, adreca,
similarity(nom || ' ' || adreca, 'passeig mol') AS score
FROM estacions
WHERE (nom || ' ' || adreca) % 'passeig mol'
ORDER BY score DESC LIMIT 5;codi | nom | adreca | score --------+-------------------------+-------------------+--------- VB-012 | Estació 12 · Moll Nord | Passeig del Moll 14| 0.41 VB-019 | Estació 19 · Moll Sud | Passeig del Moll 88| 0.39
Amb l'errada inclosa. Per a 60 estacions, això és suficient — i aquell és exactament l'argument. Treure una peça que sobra no és admetre un error: és la mateixa capacitat d'anàlisi que la va posar, aplicada amb dades noves.
Errors Habituals i Consells
Error 1: no tenir escrita la taula de l'apartat 4. Sense ella, cada persona de l'equip té la seva pròpia idea de quin motor mana, i el dia que discrepen s'"arregla" el que es miri primer. La taula de fonts de la veritat és el document fundacional d'una arquitectura poliglota.
Error 2: sincronització bidireccional. Dos motors que s'escriuen mútuament produeixen cicles, oscil·lacions i conflictes que cal resoldre amb regles ad hoc. Graf acíclic, sempre.
Error 3: repartir una transacció entre dos motors. Si dues dades han de canviar atòmicament, viuen al mateix motor. No hi ha excepció que valgui en un sistema que cobra diners.
Error 4: escriptura dual sense reconstructor. Funciona el 99,9 % de les vegades, i el 0,1 % restant deixa una incoherència permanent que ningú no detecta. Tota escriptura dual necessita la seva xarxa de seguretat.
Error 5: creure que "eventual" significa "aviat". La consistència eventual (03-04) garanteix convergència si no hi ha més escriptures i si el mecanisme funciona. Un publicador caigut significa "mai". Per això l'antiguitat de l'outbox és una alerta.
Error 6: fer còpia de seguretat de les dades derivades i no de les originals. Vam veure que Redis i Elasticsearch no necessiten còpia; MongoDB, per a la telemetria i les incidències, sí. Confondre-ho costa car justament el dia que importa.
Error 7: deixar un slot de replicació lògica sense consumidor. El disc s'omple i el motor s'atura. És la fallada més ximple i més freqüent de les arquitectures amb CDC.
Consell 1: comença amb un motor i afegeix el segon quan tinguis el número. El número: quantes dades, quina latència cal, què dona el teu motor actual. Sense número, no hi ha cas.
Consell 2: escriu el procediment de reconstrucció abans que el de sincronització. Si saps reconstruir el derivat en minuts, la sincronització incremental pot fallar sense conseqüències greus. És el que fa tolerable tota l'arquitectura.
Consell 3: fes que les escriptures derivades siguin idempotents des del primer dia. _id amb significat, upsert, $max per a les marques de temps. El lliurament "almenys una vegada" és l'únic que pots aconseguir; la idempotència és el que el fa inofensiu.
Consell 4: prova les caigudes. Apaga Redis en un entorn de proves i comprova que es pot desbloquejar una bicicleta. La taula de l'apartat 7 és una hipòtesi fins que s'executa.
Consell 5: revisa l'arquitectura una vegada l'any amb la taula de l'apartat 9 al davant. Les arquitectures no se simplifiquen soles.
Exercicis
Exercici 1 — L'històric de preus i el tauler públic
L'ajuntament vol un tauler públic en temps real —un web obert a la ciutadania— amb: bicis lliures per estació (actualitzat cada 10 s), trajectes del dia en curs, i el mapa de calor del mes anterior. El consultaran unes 3.000 persones al dia, amb pics en publicar-se a la premsa.
- Decideix quin motor serveix cadascuna de les tres dades i justifica-ho amb la taula de l'apartat 4.
- Cal afegir alguna peça nova? Argumenta amb el criteri de l'apartat 2.
- Escriu què passa amb el tauler quan cau cadascun dels quatre motors.
Exercici 2 — Un esdeveniment nou a l'outbox
El taller vol que, quan una bicicleta es retira al taller a PostgreSQL, la fitxa d'aquella bici a MongoDB i el comptador de Redis s'actualitzin automàticament, i que l'app deixi d'oferir-la.
- Escriu la transacció de PostgreSQL que retira la bici i emet l'esdeveniment d'outbox.
- Escriu el que fa el publicador a MongoDB i a Redis, garantint idempotència.
- Explica què passa si el publicador processa el mateix esdeveniment dues vegades, i què passa si el processa amb 40 minuts de retard.
Exercici 3 — La identitat compartida amb BiblioRed
L'ajuntament vol que, en donar-se d'alta a VallBici, es detecti si la persona ja és sòcia de BiblioRed per oferir-li una tarifa combinada. BiblioRed té el seu propi PostgreSQL, separat del de VallBici.
- Enumera tres maneres de resoldre-ho i tria'n una, justificant amb els criteris de la lliçó.
- Explica per què no s'ha de fer que VallBici consulti directament la base de dades de BiblioRed.
- Indica qui és la font de la veritat de "aquesta persona és sòcia de BiblioRed" i què passa si la còpia es desfasa.
Solucions
Solució 1
1. El repartiment:
| Dada del tauler | Motor | Justificació |
|---|---|---|
| Bicis lliures cada 10 s | Redis | És exactament el cas per al qual hi és: lectura massiva, dada aproximada, desfasament < 1 s davant d'un desfasament intrínsec de minuts |
| Trajectes del dia en curs | PostgreSQL, amb memòria cau de 60 s | És un agregat sobre la font de la veritat. 3.000 consultes al dia amb memòria cau d'un minut són ~1.440 consultes reals: irrellevant |
| Mapa de calor del mes anterior | MongoDB, precalculat | El pipeline amb $unwind triga segons i no es pot executar per petició. Es calcula una vegada al mes amb $merge i el tauler llegeix el resultat |
2. Cap peça nova. Aplicant el criteri: hi ha un volum que els motors actuals no absorbeixin? 3.000 visites diàries són ~0,03 peticions/segon de mitjana i potser 20/s en pic. Redis n'atén desenes de milers per segon. Una latència inassolible? No. Una capacitat funcional que falti? No. Sense número que ho justifiqui, no s'afegeix res.
El que sí que cal afegir és una memòria cau de resposta HTTP de 10 segons davant del tauler: converteix el pic de premsa en una consulta cada 10 segons, sigui quin sigui el nombre de visites. És la intervenció amb millor relació benefici/cost i no és una base de dades.
3. Degradació del tauler:
| Cau | Tauler |
|---|---|
| Redis | Les bicis lliures se serveixen des de PostgreSQL (8 ms); el tauler continua sencer |
| Elasticsearch | Sense efecte: el tauler no cerca text |
| MongoDB | Desapareix el mapa de calor; els altres dos blocs continuen. Si el resultat precalculat es copia a PostgreSQL en generar-lo, ni això |
| PostgreSQL | Es congelen els trajectes del dia; el mapa de bicis lliures continua des de Redis, amb avís de "dades no actualitzades" |
Fixa't que l'última fila descriu un tauler públic que continua funcionant visualment durant una caiguda del nucli. Per a un tauler d'imatge municipal, això val molt.
Solució 2
1. La transacció:
BEGIN;
UPDATE bicicletes SET estat = 'taller'
WHERE bicicleta_id = 417 AND estat = 'ancorada'; -- si està en ús, 0 files: avortar
UPDATE ancoratges SET bicicleta_id = NULL
WHERE bicicleta_id = 417
RETURNING estacio_id; -- 12
INSERT INTO ordres_taller (bicicleta_id, tipus, motiu)
VALUES (417, 'avaria', 'Fre posterior sense recorregut')
RETURNING ordre_id; -- 30412
INSERT INTO outbox (agregat, agregat_id, tipus, payload) VALUES
('bicicleta', 417, 'bici_a_taller',
'{"bicicleta_id":417,"ordre_id":30412,"estacio_id":12,
"motiu":"Fre posterior sense recorregut","ts":"2026-06-14T07:02:11Z"}');
COMMIT;L'AND estat = 'ancorada' és la guarda: si la bici és en un trajecte obert, l'UPDATE afecta 0 files i l'aplicació ha d'avortar en lloc de retirar una bici que algú està fent servir. L'activador de 08-01 s'encarrega de bicis_disponibles; l'esdeveniment d'outbox no ho repeteix.
2. El publicador:
// MongoDB — idempotent pel filtre sobre ordre_taller_id, que és únic
db.incidencies.updateOne(
{ ordre_taller_id: NumberLong(30412) },
{ $setOnInsert: {
bicicleta: { id: 417, matricula: "VB-0417", tipus: "mecanica", model: "Norvent Urbana2" },
tipus: "frens", estat: "en_taller", gravetat: 3,
reportada_per: { canal: "operari" },
ts_obertura: ISODate("2026-06-14T07:02:11Z"),
estacio: 12, detall: {}, esquema_v: 1 } },
{ upsert: true }
);# Redis — idempotent perquè fixa un valor, no l'incrementa
redis> SREM est:12:bicis 417
(integer) 1
redis> HSET est:12 lliures 3 ts 1749884531
(integer) 0La clau és en el tipus d'operació. updateOne amb upsert i $setOnInsert produeix el mateix resultat s'executi una vegada o cent. SREM sobre un conjunt és idempotent per naturalesa. I el comptador es fixa amb HSET a un valor absolut, no amb HINCRBY -1: un HINCRBY aplicat dues vegades restaria dos. Aquesta és la regla general: en un sistema amb lliurament "almenys una vegada", fixa valors absoluts, no increments.
3. Els dos escenaris:
- Processat dues vegades: no canvia res. L'
upserttroba la incidència existent i$setOnInsertno toca res; elSREMretorna 0 i l'HSETfixa el mateix valor. És exactament el comportament que la idempotència ha de donar. - Processat amb 40 minuts de retard: durant 40 minuts l'app va continuar oferint la bici 417 a l'estació 12. I no passa res greu, perquè el desbloqueig confirma contra PostgreSQL: qui l'intenti agafar rep un error correcte ("aquesta bicicleta no està disponible"), perquè
bicicletes.estat = 'taller'ja està posat des del minut zero. El desfasament produeix una mala experiència, no una incoherència. Això és el disseny funcionant: la veritat es va consultar en el moment del compromís. El que sí que ha de passar és que l'alerta d'antiguitat de l'outbox (> 60 s) hagi saltat als 40 minuts.
Solució 3
1. Tres maneres i una de triada:
| Opció | Com | Valoració |
|---|---|---|
| A — Consulta directa a la BD de BiblioRed | VallBici obre una connexió al PostgreSQL de BiblioRed | Descartada (veure punt 2) |
| B — API de BiblioRed consultada en el moment de l'alta | VallBici crida GET /socis/{subject_id} durant l'alta |
Correcta, però acobla l'alta a la disponibilitat de BiblioRed |
| C — Atribut al proveïdor d'identitat municipal | BiblioRed publica l'atribut soci_bibliored: true al perfil OIDC; VallBici el rep al token |
Triada |
La C guanya per tres raons alineades amb la lliçó: la dada arriba en el moment de l'inici de sessió sense trucada addicional; el proveïdor d'identitat ja és la peça compartida entre els dos sistemes, així que no s'hi afegeix cap dependència nova; i si BiblioRed cau, VallBici continua funcionant amb l'últim valor del token. La B continua sent necessària com a suport per al cas de persones que es donen d'alta presencialment sense passar pel proveïdor d'identitat.
2. Per què no la consulta directa. Quatre raons, en ordre de pes:
- Acobla els esquemes. El dia que BiblioRed reanomeni una columna, VallBici deixa de funcionar, i ningú de l'equip de BiblioRed sabrà que això podia passar. És la pitjor forma de dependència que hi ha: invisible per a qui la trenca.
- Trenca el model de permisos. VallBici necessitaria credencials de lectura sobre la base de dades de socis de la biblioteca. Un incident de seguretat a VallBici passaria a ser un incident a BiblioRed.
- Trenca la taula de fonts de la veritat. Un sistema que llegeix la base de dades d'un altre sistema no té un contracte, té un costum.
- Dades personals. Que VallBici pugui llegir quins llibres llegeix una persona és un tractament de dades que ningú no ha autoritzat. Un contracte d'API exposa
soci_bibliored: truei res més.
3. La font de la veritat és BiblioRed, i el proveïdor d'identitat és un intermediari que en distribueix una còpia. VallBici guarda aquella còpia a persones_abonades.soci_bibliored amb la data en què la va rebre.
Si la còpia es desfasa —la persona es dona de baixa de BiblioRed i VallBici no se n'assabenta— l'efecte és que continua gaudint de la tarifa combinada fins a la renovació següent. És un cost econòmic petit i acotat, i la mitigació adequada no és sincronitzar més ràpid, sinó caducar la còpia: l'atribut té validesa de 30 dies i es refresca a cada inici de sessió. Si porta més de 30 dies sense refrescar-se, la tarifa combinada no s'aplica a la renovació següent. És la mateixa idea que l'EXPIRE de Redis, aplicada a una dada de negoci: quan depens d'una còpia aliena, posa-li data de caducitat.
Conclusió
Has vist una arquitectura poliglota completa i, sobretot, has vist el que costa. Quatre motors per a VallBici: PostgreSQL amb el nucli transaccional de 08-01, MongoDB amb la telemetria, les fitxes i les incidències de 08-02, Redis amb la disponibilitat, les sessions i les reserves, i Elasticsearch amb la cerca. I compartint identitat amb BiblioRed, perquè en una plataforma municipal els sistemes són veïns, no illes.
El que cal endur-se no és el diagrama. Són cinc idees, i funcionen igual amb quatre motors que amb dos.
La primera: cada camp té un amo. No cada entitat, cada camp. L'estació 12 viu en quatre motors i no hi ha conflicte perquè ningú no edita el nombre d'ancoratges des de MongoDB ni les fotos des de PostgreSQL. La taula de l'apartat 4 és el document fundacional del sistema, i un equip que no la té escrita no té arquitectura: té quatre bases de dades.
La segona: el que canvia junt, viu junt. El trajecte, l'ancoratge, la bicicleta i el cobrament canvien en la mateixa transacció, així que són al mateix motor. No hi ha transacció que abasti quatre sistemes, i les que ho intenten costen més del que resolen. Per això la taula outbox és dins de PostgreSQL: és l'únic que fa atòmics "es va tancar el trajecte" i "cal avisar els altres".
La tercera: el desfasament no és l'enemic; el desfasament desconegut sí. Que l'app digui 4 bicis i n'hi hagi 3 no és una fallada: és una dada amb una antiguitat declarada, i el desfasament d'un segon que afegeix Redis és menyspreable davant dels set minuts que triga una persona a arribar-hi. El que sí que és innegociable és que el moment del compromís —reservar, desbloquejar, cobrar— es resolgui contra la font de la veritat. Els comptadors ràpids pinten pantalles; les decisions es prenen contra PostgreSQL.
La quarta: es dissenya per a les caigudes, no contra elles. Que Redis pugui caure sense impedir ni un sol cobrament és la prova que el repartiment està ben fet. I aquella taula de contingències és una hipòtesi fins que apagues Redis en proves i comproves que es pot desbloquejar una bicicleta.
I la cinquena, que és la més incòmoda: la resposta correcta per defecte continua sent una sola base de dades. Cada motor afegit costa operació, competència a l'equip, modes de fallada i setmanes d'aprenentatge per a qui arribi nou. VallBici en té quatre perquè hi ha números al darrere: 272 milions de punts GPS anuals, 60.000 consultes diàries de disponibilitat, una cerca amb errades que ILIKE no fa. Si aquells números no existissin, l'arquitectura correcta seria PostgreSQL amb jsonb, pg_trgm i una memòria cau — i dir-ho en veu alta a la reunió de disseny és més valuós que qualsevol diagrama de quatre caixes. Per això l'apartat 9 existeix: saber desmuntar una peça quan els seus números deixen de sostenir-la és la mateixa competència que la va posar, exercida un any després amb dades noves.
Amb aquesta lliçó es tanca el mòdul 8 i es tanca el recorregut pràctic del curs. Vas començar a 01-01 preguntant-te què és una base de dades i per què no n'hi ha prou amb un full de càlcul; acabes repartint les dades d'un servei municipal entre quatre motors i sabent justificar cada repartiment, cada duplicació i cada sincronització. Entremig has normalitzat fins a la FNBC i has desnormalitzat a propòsit, has vist una actualització perduda amb els teus propis ulls en dos terminals, has llegit plans d'execució i has dissenyat documents pensant primer en les consultes. Això és, amb força precisió, la feina. El que queda al davant no és més temari: és criteri, i el criteri es fa amb projectes i amb errors propis. El mòdul 9 reuneix el material per continuar pel teu compte —09-01 els llibres que convé tenir a mà, 09-02 els cursos i tutorials per aprofundir en cada motor, i 09-03 les eines amb què es treballa de debò—. Tria un dels tres casos de VallBici, munta'l a la teva màquina i trenca'l: és l'única part del curs que no et podem donar feta.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
