A la lliçó 02-03 vam migrar la base de dades tienda a Cloud SQL i tot va encaixar bé: productes, comandes, clients i estoc són dades relacionals de manual. Però en construir l'aplicació han anat apareixent coses que encaixen malament en aquest model. La cistella de la compra canvia a cada clic i no necessita transaccions sobre múltiples taules. Les sessions d'usuari es llegeixen a cada petició i es descarten al cap de pocs dies. I la Lucía vol registrar cada clic sobre cada producte per saber què es mira i no es compra: això són milions d'esdeveniments al mes que, ficats a PostgreSQL, farien créixer la base de dades fins a ofegar la part que sí que importa.
Ficar tot això en una base de dades relacional no és impossible, però és forçar l'eina. En aquesta lliçó veuràs per què existeixen altres models de dades, entendràs la consistència i el teorema CAP sense dogmes, coneixeràs Firestore, Bigtable, Spanner i Memorystore amb exemples reals d'AlpinaShop, i acabaràs amb un arbre de decisió i el repartiment documentat de quina dada viu a cada lloc.
Contingut
- Per què no tot encaixa en una relacional
- Consistència i el teorema CAP sense dogmatisme
- Firestore: base de dades documental
- Firestore a AlpinaShop: cistella i sessions
- Bigtable: files ordenades i columnes amples
- Bigtable a AlpinaShop: telemetria de clics
- Spanner: relacional distribuïda
- Memorystore: memòria cau en Redis
- Taula comparativa transversal
- Arbre de decisió
- La decisió d'AlpinaShop
- Per què no tot encaixa en una relacional
El model relacional fa cinquanta anys que funciona i continua sent la resposta correcta la majoria de vegades. Les seves virtuts són enormes: un llenguatge declaratiu universal, integritat referencial, transaccions ACID, normalització que evita duplicar dades i un optimitzador que resol consultes que no havies previst.
Els seus límits apareixen en tres fronts concrets:
- Escala d'escriptura. PostgreSQL escala verticalment. Quan una sola màquina no dóna més de si, s'ha acabat. Repartir escriptures entre diversos nodes mantenint ACID és un problema molt difícil.
- Dades sense esquema fix o molt niuades. Una cistella amb productes, quantitats, opcions i promocions es pot modelar amb cinc taules i cinc
JOIN, o desar-se com un únic document que es llegeix d'una sola vegada. - Volum extrem amb accés molt simple. Milers de milions d'esdeveniments dels quals només es consulta "tots els del producte X entre aquestes dues dates" no necessiten un motor SQL complet; necessiten un magatzem ordenat per clau i molt ràpid.
D'aquí surten els grans models de dades:
| Model | Com organitza les dades | Exemple a GCP | Fort en | Feble en |
|---|---|---|---|---|
| Relacional | Taules, files, columnes, relacions | Cloud SQL, AlloyDB | Integritat, consultes complexes, transaccions | Escala horitzontal d'escriptura |
| Documental | Documents JSON en col·leccions | Firestore | Flexibilitat d'esquema, lectura d'una entitat completa, temps real | Agregacions i consultes analítiques |
| Columnar ample | Files ordenades per clau, amb famílies de columnes | Bigtable | Escriptura i lectura massives per clau, latència baixa i estable | Consultes per qualsevol camp, transaccions multifila |
| Relacional distribuïda | Taules repartides entre nodes, transaccions globals | Spanner | Escala horitzontal amb ACID i SQL | Cost base elevat |
| Clau-valor en memòria | Parells clau-valor a la RAM | Memorystore (Redis) | Latència de microsegons | Volatilitat, capacitat limitada per la RAM |
| Analític columnar | Columnes comprimides, processament massiu | BigQuery | Agregacions sobre milers de milions de files | Lectures i escriptures puntuals d'una fila |
El terme "NoSQL" és enganyós: no significa "sense SQL" —Spanner és SQL pur i Firestore té el seu propi llenguatge de consulta— sinó "no exclusivament relacional". La lectura útil és "Not Only SQL": fes servir l'eina que correspongui a cada dada.
- Consistència i el teorema CAP sense dogmatisme
El teorema CAP diu que un sistema distribuït no pot garantir simultàniament les tres propietats següents:
- C (Consistency): tota lectura retorna l'escriptura més recent.
- A (Availability): tota petició rep resposta, encara que no sigui la més actual.
- P (Partition tolerance): el sistema continua funcionant encara que la xarxa es parteixi entre nodes.
La formulació popular "tria'n dues de tres" és enganyosa. En un sistema distribuït real la partició de xarxa no és opcional: els cables es tallen, els centres de dades s'aïllen. Per tant P sempre hi és, i l'elecció real és què fer durant una partició: respondre amb dades possiblement obsoletes (AP) o rebutjar la petició per no mentir (CP).
I el matís que se sol ometre: gairebé tots els sistemes moderns són configurables, i l'elecció es fa per operació, no d'una vegada per sempre.
Tipus de consistència que trobaràs:
| Tipus | Què garanteix | On apareix |
|---|---|---|
| Forta | Tota lectura veu l'última escriptura confirmada | Cloud SQL, Spanner, Firestore (lectures de document) |
| Eventual | Les rèpliques convergeixen amb el temps; una lectura pot veure dades antigues | Rèpliques de lectura de Cloud SQL, algunes consultes distribuïdes |
| De sessió / "llegeix les teves pròpies escriptures" | Veus els teus propis canvis, potser no els dels altres a l'instant | Habitual en aplicacions web amb memòria cau |
Les conseqüències pràctiques, que és el que importa:
- En el pagament d'una comanda, vols consistència forta. Ningú no ha de comprar l'última tenda de campanya dues vegades. Cloud SQL o Spanner.
- En un comptador de "127 persones estan veient això", la consistència eventual ja fa el fet. Que digui 125 durant dos segons no fa mal a ningú.
- A la cistella, vols que l'usuari vegi els seus propis canvis a l'instant, però tant se val que un altre dispositiu trigui un segon a assabentar-se'n.
La pregunta correcta mai no és "aquesta base de dades és consistent?", sinó "què passa si aquesta dada concreta està desactualitzada un segon?". Si la resposta és "res", tens llibertat de disseny i moltes més opcions de rendiment i cost.
- Firestore: base de dades documental
Firestore desa documents —estructures tipus JSON— dins de col·leccions. Un document pot contenir subcol·leccions, formant una jerarquia.
sesiones/ (col.leccio)
ses_9f3a1c/ (document)
usuario: "cliente_4821"
creada: 2026-08-05T10:14:00Z
ultimo_acceso: 2026-08-05T10:41:00Z
carritos/ (col.leccio)
cli_4821/ (document)
actualizado: 2026-08-05T10:40:12Z
total: 238.90
lineas: [ (array d'objectes, niuat)
{ sku: "MOC-40", nombre: "Motxilla Trekking 40L", cantidad: 1, precio: 89.90 },
{ sku: "BOT-GTX", nombre: "Botes Gore-Tex", cantidad: 1, precio: 149.00 }
]
eventos/ (subcol.leccio del document)
evt_001/ { tipo: "add", sku: "MOC-40", ts: ... }Característiques clau:
- Sense esquema fix. Dos documents de la mateixa col·lecció poden tenir camps diferents. Flexible i perillós a parts iguals: la disciplina la poses tu, al codi.
- Consistència forta en les lectures de document, fins i tot en la configuració multiregió. És una diferència notable respecte d'altres bases documentals.
- Transaccions ACID sobre múltiples documents.
- Temps real. Un client es pot subscriure a un document o consulta i rebre canvis automàticament, sense sondejar.
- Mode offline. Els SDK de mòbil i web mantenen una memòria cau local i sincronitzen en recuperar connexió.
- Regles de seguretat declaratives: els clients poden accedir directament a la base de dades sense backend intermedi, amb les regles controlant què pot llegir i escriure cada usuari.
- Escala automàtica sense aprovisionar res.
Índexs i consultes. Firestore indexa automàticament cada camp, cosa que fa que les consultes simples funcionin sense configuració. Però les consultes amb diversos filtres o filtre més ordenació requereixen un índex compost explícit. La primera vegada que executis una consulta així obtindràs un error... amb un enllaç directe per crear l'índex que falta, un detall d'usabilitat molt encertat.
Limitacions que cal conèixer abans de dissenyar:
- Un document no pot superar 1 MiB. Les cistelles i sessions hi caben de sobres; un historial creixent dins d'un document, no.
- Límit d'escriptura sostinguda sobre un mateix document (de l'ordre d'una per segon). Un comptador global molt actiu requereix la tècnica de sharded counters.
- No hi ha
JOIN. Les dades es desnormalitzen: es duplica el nom i el preu del producte dins de la línia de la cistella, i s'assumeix aquesta duplicació. - Les agregacions són limitades. Hi ha
count(),sum()iaverage(), però no és una base analítica. Per a això, BigQuery.
Firestore té dos modes: Native (el modern, amb temps real i offline) i Datastore (compatibilitat amb l'antic Cloud Datastore). Per a un projecte nou, sempre Native.
gcloud firestore databases create \
--location=eur3 \
--type=firestore-native \
--project=alpinashop-prodeur3 és una ubicació multiregió europea. Per a una sola regió seria europe-west1. La ubicació és immutable, igual que a Cloud Storage.
- Firestore a AlpinaShop: cistella i sessions
La cistella és un cas de llibre per a Firestore: una entitat per usuari que es llegeix sencera, s'escriu amb freqüència, té estructura niuada i no necessita relacionar-se amb res més en el moment de llegir-la.
from datetime import datetime, timedelta, timezone
from google.cloud import firestore
db = firestore.Client(database="(default)")
def obtenir_cistella(cliente_id: str) -> dict:
"""Llegeix la cistella completa d'un client en UNA sola lectura."""
doc = db.collection("carritos").document(cliente_id).get()
if not doc.exists:
return {"lineas": [], "total": 0.0}
return doc.to_dict()
def afegir_linia(cliente_id: str, sku: str, nom: str, preu: float, quantitat: int = 1):
"""Afegeix una linia a la cistella dins d'una transaccio."""
ref = db.collection("carritos").document(cliente_id)
@firestore.transactional
def _actualitzar(transaccio, ref):
snapshot = ref.get(transaction=transaccio)
dades = snapshot.to_dict() if snapshot.exists else {"lineas": []}
# Si el SKU ja hi es, incrementem la quantitat en lloc de duplicar la linia
for linia in dades["lineas"]:
if linia["sku"] == sku:
linia["cantidad"] += quantitat
break
else:
dades["lineas"].append({
"sku": sku,
"nombre": nom, # desnormalitzat a proposit: no hi ha JOIN
"precio": preu, # preu en el moment d'afegir
"cantidad": quantitat,
})
dades["total"] = round(
sum(l["precio"] * l["cantidad"] for l in dades["lineas"]), 2
)
dades["actualizado"] = firestore.SERVER_TIMESTAMP
transaccio.set(ref, dades)
_actualitzar(db.transaction(), ref)
def buidar_cistella(cliente_id: str):
db.collection("carritos").document(cliente_id).delete()Punts importants del codi:
- La transacció és necessària perquè llegim la cistella, la modifiquem i la tornem a escriure. Sense ella, dues pestanyes del navegador afegint productes alhora es podrien trepitjar. Firestore reintenta la transacció automàticament si detecta conflicte.
SERVER_TIMESTAMPutilitza el rellotge del servidor, no el del client. No confiïs mai en l'hora del dispositiu de l'usuari.- La desnormalització és deliberada. Desem
nombreipreciodins de la línia. Si el preu canvia demà, la cistella conserva el preu al qual es va afegir, que a més és el comportament correcte de negoci.
Sessions amb caducitat automàtica:
def crear_sessio(sesion_id: str, cliente_id: str, dies: int = 30):
"""Crea una sessio amb marca de caducitat per al TTL de Firestore."""
caduca = datetime.now(timezone.utc) + timedelta(days=dies)
db.collection("sesiones").document(sesion_id).set({
"cliente_id": cliente_id,
"creada": firestore.SERVER_TIMESTAMP,
"caduca_en": caduca, # camp configurat com a TTL
"carrito_items": 0,
})
def cistelles_abandonades(hores: int = 24):
"""Cistelles amb contingut que fa mes de N hores que no es toquen."""
limit = datetime.now(timezone.utc) - timedelta(hours=hores)
consulta = (
db.collection("carritos")
.where(filter=firestore.FieldFilter("actualizado", "<", limit))
.where(filter=firestore.FieldFilter("total", ">", 0))
.order_by("actualizado")
.limit(100)
)
return [(d.id, d.to_dict()) for d in consulta.stream()]Aquella consulta amb dos filtres de desigualtat i ordenació requereix un índex compost. Es declara així:
gcloud firestore indexes composite create \
--collection-group=carritos \
--field-config=field-path=actualizado,order=ascending \
--field-config=field-path=total,order=ascendingFirestore pot a més esborrar documents automàticament segons un camp de data, cosa que resol la neteja de sessions sense escriure cap procés:
Regles de seguretat. Si el navegador o l'app mòbil accedeixen directament a Firestore, les regles són l'única barrera:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Cada client nomes pot llegir i escriure LA SEVA cistella
match /carritos/{clienteId} {
allow read, write: if request.auth != null
&& request.auth.uid == clienteId;
}
// Les sessions no son accessibles des del client: nomes des del backend
match /sesiones/{sesionId} {
allow read, write: if false;
}
// El cataleg public es de nomes lectura per a tothom
match /catalogo/{producto} {
allow read: if true;
allow write: if false;
}
}
}La regla if false no bloqueja el backend: els comptes de servei amb permisos IAM ometen les regles de seguretat, que només s'apliquen als SDK de client.
- Bigtable: files ordenades i columnes amples
Bigtable és el sistema que Google va crear per als seus propis índexs de cerca, i sobre el qual es va construir HBase. El seu model és enganyosament simple:
- Una taula és un conjunt de files ordenades lexicogràficament per la seva clau.
- Cada fila té famílies de columnes, i dins de cada família, columnes arbitràries.
- Cada cel·la pot desar diverses versions amb marca temporal.
- L'única forma eficient d'accés és per clau de fila o per rang de claus.
Fila (row key) | familia "ev" | familia "ctx" --------------------------------|-------------------------------------|------------------ MOC-40#20260805#0942#a7f3 | ev:tipo=vista ev:duracion=8300 | ctx:pais=ES ctx:disp=movil MOC-40#20260805#0943#b1c9 | ev:tipo=add_cart | ctx:pais=ES ctx:disp=movil BOT-GTX#20260805#0944#c2d1 | ev:tipo=vista ev:duracion=2100 | ctx:pais=PT ctx:disp=escritorio
Fortaleses: latència d'un sol dígit de mil·lisegons de manera consistent, milions d'operacions per segon, escala a petabytes, i creixement horitzontal afegint nodes sense aturar el servei.
Limitacions, igual d'importants: no hi ha índexs secundaris (només pots buscar per clau de fila), no hi ha JOIN, no hi ha transaccions entre files (només atomicitat dins d'una fila) i no hi ha SQL en el sentit tradicional.
El disseny de la row key ho és tot. Com que les files s'emmagatzemen ordenades i es reparteixen en blocs contigus entre nodes, la clau decideix simultàniament quines consultes seran eficients i si la càrrega es repartirà bé.
El problema que cal evitar es diu hotspot: si les claus són seqüencials —una marca temporal al principi, per exemple— totes les escriptures noves cauen al mateix bloc, aquest bloc va a un sol node, i aquest node se satura mentre la resta del clúster està ociosa.
| Disseny de row key | Exemple | Resultat |
|---|---|---|
| Timestamp al principi | 20260805094215#MOC-40 |
Hotspot greu: totes les escriptures al mateix node |
| ID seqüencial | 0000001, 0000002 |
Hotspot greu: mateix problema |
| Camp d'alta cardinalitat primer | MOC-40#20260805094215 |
Ben repartit, i permet escanejar per producte |
| Hash pur | a7f3c1b9... |
Repartiment perfecte, però impossible escanejar per rangs útils |
| Prefix de camp + timestamp + sufix aleatori | MOC-40#20260805094215#a7f3 |
Repartiment correcte i escanejos per producte i data |
La regla es resumeix així: comença la clau per un camp d'alta cardinalitat i que aparegui a les teves consultes, i afegeix el temps després. I dissenya la clau a partir de les consultes que faràs, no de l'estructura de les dades: a Bigtable, el patró d'accés és l'esquema.
Un altre truc freqüent és utilitzar el timestamp invertit (9999999999 - epoch) quan vols que els esdeveniments més recents surtin primer en un escaneig, ja que l'ordre sempre és ascendent.
- Bigtable a AlpinaShop: telemetria de clics
La Lucía vol saber quins productes es miren molt i es compren poc. Això significa registrar cada vista de fitxa, cada afegit a la cistella i cada eliminació: en campanya, diversos milions d'esdeveniments al mes. A PostgreSQL aquella taula creixeria sense control i competiria amb les comandes per recursos.
gcloud services enable bigtable.googleapis.com bigtableadmin.googleapis.com
# Instancia de desenvolupament (1 node). En produccio, tipus PRODUCTION amb >=3 nodes.
gcloud bigtable instances create alpinashop-telemetria \
--display-name="Telemetria del cataleg" \
--cluster-config=id=telemetria-c1,zone=europe-west1-b,nodes=1 \
--instance-type=PRODUCTION
gcloud bigtable instances tables create eventos-catalogo \
--instance=alpinashop-telemetria \
--column-families=ev,ctximport uuid
from datetime import datetime, timezone
from google.cloud import bigtable
from google.cloud.bigtable import row_filters
client = bigtable.Client(project="alpinashop-prod", admin=False)
instancia = client.instance("alpinashop-telemetria")
taula = instancia.table("eventos-catalogo")
def registrar_esdeveniment(sku: str, tipus: str, pais: str, dispositiu: str,
duracio_ms: int | None = None):
"""
Row key: <sku>#<timestamp>#<aleatori>
- <sku> primer: alta cardinalitat, reparteix la carrega i permet
escanejar tots els esdeveniments d'un producte amb un prefix.
- <timestamp> despres: ordena cronologicament dins de cada producte
i permet acotar rangs de dates.
- <aleatori> al final: evita col.lisions entre esdeveniments simultanis
del mateix producte.
"""
ara = datetime.now(timezone.utc)
clau = f"{sku}#{ara.strftime('%Y%m%d%H%M%S%f')}#{uuid.uuid4().hex[:6]}"
fila = taula.direct_row(clau)
fila.set_cell("ev", "tipo", tipus)
fila.set_cell("ctx", "pais", pais)
fila.set_cell("ctx", "dispositivo", dispositiu)
if duracio_ms is not None:
fila.set_cell("ev", "duracion_ms", str(duracio_ms))
fila.commit()
def esdeveniments_de_producte(sku: str, limit: int = 1000):
"""Escaneig per prefix: tots els esdeveniments d'un producte."""
files = taula.read_rows(
start_key=f"{sku}#".encode(),
end_key=f"{sku}$".encode(), # '$' es el caracter seguent a '#' en ASCII
limit=limit,
)
resultat = []
for fila in files:
cel_les = {
f"{fam}:{col.decode()}": cel_les_col[0].value.decode()
for fam, cols in fila.cells.items()
for col, cel_les_col in cols.items()
}
resultat.append((fila.row_key.decode(), cel_les))
return resultat
def esdeveniments_de_producte_en_dia(sku: str, dia: str):
"""Escaneig per rang: esdeveniments d'un producte en un dia concret (YYYYMMDD)."""
return taula.read_rows(
start_key=f"{sku}#{dia}".encode(),
end_key=f"{sku}#{dia}999999999999".encode(),
)Fixa't en el truc d'end_key: com que l'ordre és lexicogràfic i $ (0x24) va just després de # (0x23) en ASCII, utilitzar {sku}$ com a límit superior captura exactament totes les claus que comencen per {sku}#. És el patró idiomàtic d'escaneig per prefix a Bigtable.
Cicle de vida de les dades. Bigtable pot fer caducar cel·les automàticament per edat o per nombre de versions, cosa que evita que la taula creixi indefinidament:
# Conservar els esdeveniments 90 dies
cbt -instance=alpinashop-telemetria setgcpolicy eventos-catalogo ev maxage=90d
cbt -instance=alpinashop-telemetria setgcpolicy eventos-catalogo ctx maxage=90dOn encaixa Bigtable a l'arquitectura de dades. Bigtable desa i serveix els esdeveniments amb latència mínima, però no respon a "quantes vistes per categoria hi va haver a l'octubre": això és una agregació analítica i el seu lloc és BigQuery. El patró habitual és que l'aplicació escrigui a Bigtable —o publiqui al topic pedidos-nuevos de Pub/Sub— i que un procés periòdic exporti a BigQuery per a l'anàlisi. Tot aquest circuit és el mòdul 4.
Sobre el cost, amb l'honestedat que mereix: Bigtable factura per node i hora, estigui o no en ús, més l'emmagatzematge. Un node costa de l'ordre de diversos centenars de dòlars al mes, i producció en recomana tres. No té nivell gratuït ni escala a zero. Per als volums actuals d'AlpinaShop, Bigtable és una decisió prematura: els mateixos esdeveniments es poden publicar a Pub/Sub i inserir directament a BigQuery a un cost molt inferior. L'estudiem aquí perquè és el servei correcte quan el volum i l'exigència de latència ho justifiquen, i perquè convé saber reconèixer aquell moment. A l'apartat 11 documentarem aquesta decisió.
- Spanner: relacional distribuïda
Spanner és la resposta de Google a un problema que es considerava irresoluble: una base de dades que escali horitzontalment sense renunciar a SQL, a les transaccions ACID ni a la consistència forta, fins i tot entre continents.
Com ho aconsegueix:
- Repartiment automàtic de les taules en fragments distribuïts entre nodes, que es divideixen i reequilibren sols.
- TrueTime: una API de temps recolzada per rellotges atòmics i GPS als centres de dades de Google, que acota la incertesa del rellotge a uns pocs mil·lisegons i permet ordenar transaccions globalment de manera correcta.
- Replicació síncrona entre zones i regions mitjançant consens Paxos.
Resultat: transaccions fortament consistents a escala global, amb SLA de disponibilitat de fins al 99,999 % en configuració multiregió, i SQL estàndard (amb dialecte GoogleSQL o compatibilitat PostgreSQL).
-- L'"interleaving" emmagatzema fisicament les linies al costat de la seva comanda,
-- de manera que llegir una comanda amb les seves linies no creua la xarxa entre nodes.
CREATE TABLE Pedidos (
PedidoId INT64 NOT NULL,
ClienteId INT64 NOT NULL,
Fecha TIMESTAMP NOT NULL,
Total NUMERIC,
) PRIMARY KEY (PedidoId);
CREATE TABLE LineasPedido (
PedidoId INT64 NOT NULL,
LineaId INT64 NOT NULL,
Sku STRING(32) NOT NULL,
Cantidad INT64 NOT NULL,
Precio NUMERIC,
) PRIMARY KEY (PedidoId, LineaId),
INTERLEAVE IN PARENT Pedidos ON DELETE CASCADE;Quan justifica el seu cost. Spanner es factura per processing units (des de 100 PU, aproximadament la desena part d'un node) més emmagatzematge, i una configuració de producció multiregió és substancialment més cara que una instància de Cloud SQL equivalent. Es justifica quan:
- Les escriptures superen el que pot absorbir la instància més gran de Cloud SQL o AlloyDB.
- Necessites escriptura activa en diverses regions amb consistència forta.
- La disponibilitat exigida és de cinc nous i una finestra de failover d'un minut és inacceptable.
- Manegues dades financeres o d'inventari global on una inconsistència té cost real i directe.
Casos típics: banca, plataformes de joc globals, inventari multinacional, sistemes de reserves.
Per a AlpinaShop, Spanner és sobredimensionat, i dir-ho amb claredat forma part d'aprendre a triar. Una botiga espanyola amb pics estacionals és a diversos ordres de magnitud de necessitar-lo. Si l'empresa creixés fins a operar en diversos continents amb inventari compartit, seria la conversa adequada; i el pas intermedi natural seria abans AlloyDB, que multiplica el rendiment mantenint compatibilitat amb PostgreSQL.
- Memorystore: memòria cau en Redis
Memorystore és Redis (i Valkey i Memcached) gestionat. No és realment una base de dades: és una memòria cau en memòria amb latència de microsegons.
gcloud services enable redis.googleapis.com
gcloud redis instances create alpinashop-cache \
--size=1 \
--region=europe-west1 \
--redis-version=redis_7_2 \
--tier=basicEl nivell basic és un node sense rèplica —adequat per a una memòria cau, on perdre les dades només implica recalcular-les—; standard afegeix rèplica i failover automàtic.
Usos naturals a AlpinaShop:
- Memòria cau del catàleg. La portada consulta els mateixos 50 productes milers de vegades al dia. Cachejar-los 5 minuts elimina gairebé tota aquesta càrrega de Cloud SQL.
- Sessions. Alternativa a Firestore quan l'únic que importa és la velocitat i es pot tolerar perdre-les.
- Limitació de peticions (rate limiting). Comptadors per IP amb caducitat automàtica.
- Cues lleugeres i bloquejos distribuïts.
import json
import redis
cache = redis.Redis(host="10.0.0.3", port=6379, decode_responses=True)
def productes_destacats():
"""Patro cache-aside: mirar la memoria cau, i si no hi es, anar a la base de dades."""
clau = "catalogo:destacados"
en_cache = cache.get(clau)
if en_cache:
return json.loads(en_cache)
with engine.connect() as conn:
files = conn.execute(sqlalchemy.text(
"SELECT sku, nombre, precio FROM tienda.productos "
"WHERE destacado = true ORDER BY nombre LIMIT 50"
)).mappings().all()
productes = [dict(f) for f in files]
# ex=300: caduca en 5 minuts. La caducitat NO es opcional:
# sense ella, les dades obsoletes es queden per sempre.
cache.set(clau, json.dumps(productes, default=str), ex=300)
return productes
def invalidar_producte(sku: str):
"""En canviar un producte, invalidar les claus afectades."""
cache.delete("catalogo:destacados", f"producto:{sku}")Tres advertiments sobre memòries cau, que causen més incidents dels que sembla:
- Tota entrada ha de tenir caducitat. Sense TTL, una dada obsoleta pot quedar-se indefinidament.
- La invalidació és la part difícil. Quan la Marta canvia un preu, algú ha d'esborrar la clau corresponent. Si no, la botiga mostra el preu antic durant minuts.
- Memorystore només és accessible per IP privada dins de la teva VPC. Requereix que el client sigui a la mateixa xarxa; per a App Engine o Cloud Run cal un connector d'accés a VPC serverless (03-01).
- Taula comparativa transversal
| Servei | Model | Consistència | Latència típica | Escala | Consultes | Cost relatiu | Quan utilitzar-la |
|---|---|---|---|---|---|---|---|
| Cloud SQL | Relacional | Forta | ~1–10 ms | Vertical, fins a TB | SQL complet | Baix-mitjà | Dades transaccionals amb relacions. El cas per defecte |
| AlloyDB | Relacional (PostgreSQL) | Forta | ~1–5 ms | Vertical ampliada + rèpliques | SQL complet + motor columnar | Mitjà-alt | Cloud SQL es queda curt sense sortir de PostgreSQL |
| Spanner | Relacional distribuïda | Forta, global | ~5–15 ms | Horitzontal, il·limitada | SQL complet | Alt | Escala global amb ACID |
| Firestore | Documental | Forta per document | ~10–50 ms | Automàtica | Consultes per camps indexats; sense JOIN |
Baix (per operació) | Entitats autocontingudes, temps real, offline |
| Bigtable | Columnar ample | Forta per fila | ~2–10 ms estable | Horitzontal, petabytes | Només per clau o rang | Alt (per node/hora) | Sèries temporals i volum enorme amb accés per clau |
| Memorystore | Clau-valor en memòria | Forta (node únic) | <1 ms | Limitada per RAM | Comandes Redis | Mitjà | Memòria cau, sessions, comptadors |
| BigQuery | Analític columnar | Forta | segons | Horitzontal, petabytes | SQL analític | Baix per consulta, alt si se n'abusa | Agregacions sobre volums massius (04-01) |
| Cloud Storage | Objectes | Forta | ~50–200 ms | Il·limitada | Només per clau | Molt baix | Fitxers, imatges, còpies (02-02) |
Dues lectures d'aquesta taula que convé subratllar. Primera: BigQuery no és una base de dades operacional. Les seves consultes triguen segons i la seva facturació penalitza les lectures freqüents de poc volum; no el posis mai al camí d'una petició web. Segona: la latència no ho és tot. Bigtable és més ràpid que Cloud SQL, però si les teves dades són relacionals, triar Bigtable significa reimplementar a mà el que SQL et donava de franc.
- Arbre de decisió
graph TD
A[Tinc una dada per desar] --> B{Es un fitxer binari:<br/>imatge, video, PDF?}
B -->|Si| C[Cloud Storage]
B -->|No| D{Es analitica sobre<br/>grans volums?}
D -->|Si| E[BigQuery]
D -->|No| F{El puc perdre<br/>sense consequencies?}
F -->|Si| G[Memorystore Redis]
F -->|No| H{Te relacions i<br/>necessita transaccions<br/>entre entitats?}
H -->|Si| I{Hi cap en una<br/>sola maquina?}
I -->|Si| J[Cloud SQL]
I -->|Quasi, necessito<br/>mes rendiment| K[AlloyDB]
I -->|No: escala global| L[Spanner]
H -->|No| M{Volum enorme amb<br/>acces nomes per clau<br/>o rang de temps?}
M -->|Si| N[Bigtable]
M -->|No| O{Entitat autocontinguda,<br/>temps real o offline?}
O -->|Si| P[Firestore]
O -->|No| J
L'arbre és una guia, no un dogma. Dos consells que l'acompanyen:
- En cas de dubte, comença per Cloud SQL. És el més versàtil, el més conegut i el més fàcil d'abandonar si t'equivoques. Canviar d'una relacional a una altra cosa sempre és més fàcil que a l'inrevés.
- No utilitzis cinc bases de dades perquè pots. Cadascuna afegeix una tecnologia per operar, monitorar, salvaguardar i aprendre. La complexitat es paga cada dia; el benefici només apareix si el problema ho justifica de debò.
- La decisió d'AlpinaShop
Amb tot l'anterior, aquest és el repartiment documentat de les dades d'AlpinaShop:
| Dada | On viu | Per què |
|---|---|---|
| Productes, preus, estoc | Cloud SQL (alpinashop-pedidos, BD tienda) |
Relacional, transaccional, consultat per molts camps, volum modest |
| Comandes i línies de comanda | Cloud SQL | Transaccions ACID imprescindibles: cobrament, estoc i comanda han de ser atòmics |
| Clients i adreces | Cloud SQL | Relacional, amb integritat referencial cap a comandes |
| Cistella de la compra | Firestore (col·lecció carritos) |
Entitat autocontinguda per client, escriptura molt freqüent, estructura niuada, sense necessitat de JOIN. A més permet sincronització en temps real entre dispositius |
| Sessions d'usuari | Firestore (col·lecció sesiones, amb TTL) |
Alta rotació, caducitat automàtica sense procés de neteja, i no embruten la base transaccional |
| Catàleg cachejat de la portada | Memorystore (alpinashop-cache) |
Els mateixos 50 productes milers de vegades al dia; TTL de 5 minuts elimina aquesta càrrega de Cloud SQL |
| Imatges de producte | Cloud Storage (alpinashop-catalogo) |
60 GB de binaris; no són dades de base de dades (02-02) |
| Telemetria de clics | Pub/Sub → BigQuery avui; Bigtable quan el volum ho exigeixi | Volum mitjà i consultes analítiques, no operacionals. Bigtable factura per node 24×7 i avui no està justificat |
| Informes i analítica | BigQuery (alpinashop_analitica) |
Agregacions sobre històrics; allibera Cloud SQL i la rèplica de la Lucía (mòdul 4) |
I el que no utilitzem, amb el seu motiu, perquè descartar amb criteri és tan valuós com triar:
- Spanner: sobredimensionat en diversos ordres de magnitud. Una botiga espanyola no necessita transaccions globals entre continents. Si l'escala transaccional arribés a estrènyer, el pas previ seria AlloyDB.
- Bigtable, per ara: el servei correcte per al problema, però prematur per al volum actual. La seva facturació per node i hora, sense escalat a zero, no es compensa amb uns milions d'esdeveniments al mes que BigQuery absorbeix sense despentinar-se. La decisió es revisarà quan la telemetria creixi en un ordre de magnitud o quan calgui llegir esdeveniments individuals amb latència de mil·lisegons.
Aquest és exactament el tipus de decisió que cal documentar i datar: no perquè no hagi de canviar, sinó perquè d'aquí a un any s'entengui per què es va prendre i amb quines dades.
Errors habituals i consells
- Triar NoSQL "perquè escala" sense necessitar-ho. La majoria d'aplicacions no arriben mai al límit d'una instància de Cloud SQL ben dimensionada.
- Modelar Firestore com si fos relacional. Sense
JOIN, fer deu lectures encadenades per compondre una pantalla és lent i car. Desnormalitza. - Dissenyar la row key de Bigtable amb el timestamp al davant. Hotspot garantit: totes les escriptures al mateix node.
- Utilitzar hash pur com a row key. Reparteix bé, però perds la capacitat d'escanejar rangs, que és la raó d'utilitzar Bigtable.
- Posar BigQuery al camí d'una petició web. Latència de segons i facturació per dades escanejades.
- Desar a la memòria cau sense TTL. Dades obsoletes eternes.
- Oblidar invalidar la memòria cau en canviar un preu. La botiga mostra preus antics.
- Confiar en el rellotge del client. Utilitza
SERVER_TIMESTAMPa Firestore i marques del servidor en general. - Superar 1 MiB en un document de Firestore acumulant-hi historial. Utilitza subcol·leccions.
- Deixar una instància de Bigtable encesa "per provar". Factura per node i hora, no escala a zero.
- Consell: comença sempre per la relacional i mou-te només quan tinguis una raó mesurada.
- Consell: dissenya l'esquema de Bigtable a partir de les consultes, mai a l'inrevés.
- Consell: crea els índexs compostos de Firestore des de l'enllaç que dóna l'error. És la via més ràpida i fiable.
- Consell: activa TTL a Firestore per a tot allò que caduca; t'estalvia escriure i mantenir un procés de neteja.
- Consell: documenta i data la decisió de quina dada va a quina base. El teu jo futur t'ho agrairà.
Exercicis
Exercici 1: triar la base de dades adequada
Per a cada dada d'AlpinaShop, indica el servei, una alternativa raonable i una justificació de dues línies:
- L'històric de preus de cada producte, per auditar canvis (uns centenars de files al mes).
- Les valoracions i ressenyes de clients, amb text lliure, fotos associades i respostes niuades.
- La posició GPS dels repartidors durant el repartiment, una lectura cada 5 segons per repartidor.
- El nombre de vegades que s'ha vist la portada avui, mostrat al tauler intern.
- Les factures en PDF generades per a cada comanda.
Exercici 2: Firestore, cistella i consultes
- Crea una base de dades Firestore en mode Native.
- Escriu funcions en Python per afegir una línia a la cistella, canviar la quantitat d'una línia i buidar la cistella, utilitzant una transacció on correspongui.
- Escriu una consulta que retorni les cistelles amb més de 100 € sense activitat en les últimes 48 hores.
- Explica quin índex compost necessita aquella consulta i com el crearies.
- Escriu la regla de seguretat que permeti a cada client accedir només a la seva cistella i a ningú llegir les sessions.
Exercici 3: dissenyar una row key de Bigtable
AlpinaShop vol registrar a Bigtable, més endavant, les cerques del cercador intern: terme cercat, nombre de resultats, si hi va haver clic, país i dispositiu. Les consultes previstes són: (a) totes les cerques d'un terme en un rang de dates; (b) les cerques d'un dia concret per exportar-les.
- Proposa una row key per a la consulta (a) i justifica-la.
- Explica per què aquella clau no serveix bé per a la consulta (b) i què hi faries.
- Identifica un disseny de clau que produiria hotspots i explica el mecanisme.
- Defineix les famílies de columnes i quines columnes anirien a cadascuna.
- Indica quina política de caducitat aplicaries i per què.
Solucions
Solució 1
| Dada | Servei | Alternativa | Justificació |
|---|---|---|---|
| 1. Històric de preus | Cloud SQL | BigQuery si creixés molt | Volum mínim, relacionat amb productos, es consulta amb JOIN i per rang de dates. No hi ha cap raó per treure'l de la relacional |
| 2. Valoracions i ressenyes | Firestore (fotos a Cloud Storage) | Cloud SQL amb columnes JSONB | Estructura niuada i variable (respostes dins de ressenyes), es llegeix la ressenya completa d'una vegada, i el temps real permet mostrar respostes a l'instant |
| 3. Posició GPS de repartidors | Bigtable (o Firestore si en són pocs) | Pub/Sub + BigQuery per a l'històric | Sèrie temporal d'escriptura contínua amb accés per repartidor i rang de temps: el cas canònic de Bigtable. Amb 5 repartidors, Firestore és més barat i suficient |
| 4. Comptador de vistes d'avui | Memorystore | Firestore amb comptador fragmentat | Un comptador que s'incrementa constantment i la pèrdua del qual és irrellevant: INCR de Redis és l'operació exacta. A Cloud SQL seria contenció d'escriptura sobre una fila |
| 5. Factures en PDF | Cloud Storage | — | Són fitxers binaris. A la base de dades només hi va la ruta de l'objecte. A més admeten retenció bloquejada per obligació legal (02-02) |
Solució 2
from datetime import datetime, timedelta, timezone
from google.cloud import firestore
db = firestore.Client()
def _recalcular(dades: dict) -> dict:
dades["total"] = round(sum(l["precio"] * l["cantidad"] for l in dades["lineas"]), 2)
dades["actualizado"] = firestore.SERVER_TIMESTAMP
return dades
def afegir_linia(cliente_id, sku, nom, preu, quantitat=1):
ref = db.collection("carritos").document(cliente_id)
@firestore.transactional
def _op(tx, ref):
snap = ref.get(transaction=tx)
dades = snap.to_dict() if snap.exists else {"lineas": []}
for l in dades["lineas"]:
if l["sku"] == sku:
l["cantidad"] += quantitat
break
else:
dades["lineas"].append(
{"sku": sku, "nombre": nom, "precio": preu, "cantidad": quantitat}
)
tx.set(ref, _recalcular(dades))
_op(db.transaction(), ref)
def canviar_quantitat(cliente_id, sku, quantitat):
ref = db.collection("carritos").document(cliente_id)
@firestore.transactional
def _op(tx, ref):
snap = ref.get(transaction=tx)
if not snap.exists:
return
dades = snap.to_dict()
if quantitat <= 0:
dades["lineas"] = [l for l in dades["lineas"] if l["sku"] != sku]
else:
for l in dades["lineas"]:
if l["sku"] == sku:
l["cantidad"] = quantitat
tx.set(ref, _recalcular(dades))
_op(db.transaction(), ref)
def buidar_cistella(cliente_id):
# Esborrat directe: no cal transaccio, es una sola operacio atomica
db.collection("carritos").document(cliente_id).delete()
def cistelles_valuoses_abandonades():
limit = datetime.now(timezone.utc) - timedelta(hours=48)
consulta = (
db.collection("carritos")
.where(filter=firestore.FieldFilter("total", ">", 100))
.where(filter=firestore.FieldFilter("actualizado", "<", limit))
.order_by("total")
.order_by("actualizado")
.limit(50)
)
return [(d.id, d.to_dict()) for d in consulta.stream()]- La consulta filtra per dos camps diferents i ordena per tots dos, així que Firestore necessita un índex compost sobre
(total, actualizado)a la col·lecciócarritos. Els índexs automàtics de camp únic no basten quan hi ha més d'un filtre de desigualtat. En executar la consulta sense l'índex, Firestore retorna un errorFAILED_PRECONDITIONamb un enllaç que el crea amb la definició exacta; també es pot declarar així:
gcloud firestore indexes composite create \
--collection-group=carritos \
--field-config=field-path=total,order=ascending \
--field-config=field-path=actualizado,order=ascending// 5. Regles de seguretat
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /carritos/{clienteId} {
allow read, write: if request.auth != null
&& request.auth.uid == clienteId;
}
match /sesiones/{sesionId} {
allow read, write: if false; // nomes backend, via compte de servei
}
}
}Solució 3
- Row key per a la consulta (a):
El terme va primer perquè és d'alta cardinalitat (milers de termes diferents, cosa que reparteix bé la càrrega entre nodes) i perquè és el camp que apareix a la consulta: escanejar des de motxilla_40l#20260805 fins a motxilla_40l#20260812 retorna exactament les cerques d'aquell terme en aquella setmana, llegint només files contigües. El sufix aleatori evita col·lisions entre cerques simultànies del mateix terme.
- Aquella clau no serveix per a la consulta (b) —"totes les cerques d'un dia"— perquè les files d'un mateix dia estan disperses per tota la taula, repartides entre tots els termes: caldria escanejar la taula completa filtrant. Les opcions són:
- La recomanada: exportar a BigQuery i fer-hi les consultes per data. Bigtable resol l'accés operacional per clau; l'analítica per altres dimensions és feina de BigQuery.
- Mantenir una segona taula amb clau
<data>#<salt>#<terme>#<ts>, on<salt>és un número de 0 a N que reparteix artificialment la càrrega i evita el hotspot del prefix per data. Duplica l'emmagatzematge i exigeix escriure dues vegades.
- Un disseny que produiria hotspots:
Com que les files s'emmagatzemen ordenades per clau i es reparteixen en blocs contigus entre nodes, totes les escriptures d'un mateix instant comparteixen prefix i cauen al mateix bloc, gestionat per un únic node. Aquell node se satura mentre la resta del clúster està ociosa: l'escala horitzontal deixa de funcionar. És exactament el mateix problema amb claus seqüencials tipus 0000001, 0000002.
- Famílies de columnes:
| Família | Columnes | Motiu |
|---|---|---|
busq |
termino_original, num_resultados, hubo_clic, sku_clic |
Dades del fet en si; es llegeixen gairebé sempre juntes |
ctx |
pais, dispositivo, idioma, sesion_id |
Context; de vegades es consulta sense necessitar la resta |
Agrupar en famílies les columnes que es llegeixen juntes millora el rendiment, perquè Bigtable emmagatzema i recupera cada família de manera independent. Convé mantenir poques famílies (idealment menys de deu) i amb noms curts, ja que el nom es repeteix a cada cel·la emmagatzemada.
- Política de caducitat:
maxage=180da totes dues famílies. Les cerques tenen valor operacional durant uns mesos —detectar termes sense resultats, ajustar el cercador— i després el seu valor és exclusivament històric, que ja està cobert per l'exportació a BigQuery, on l'emmagatzematge és molt més barat. La caducitat automàtica evita que la taula creixi sense límit i que el cost d'emmagatzematge pugi de manera indefinida.
cbt -instance=alpinashop-telemetria setgcpolicy busquedas busq maxage=180d
cbt -instance=alpinashop-telemetria setgcpolicy busquedas ctx maxage=180dConclusió
Has deixat de tenir una sola eina per a totes les dades. Saps que el model relacional continua sent la resposta per defecte i per què —integritat, transaccions, SQL, un optimitzador que resol consultes que no havies previst— i també on són els seus tres límits reals: l'escala d'escriptura, les dades niuades sense esquema fix i el volum extrem amb accés trivial. Has entès el teorema CAP sense el dogmatisme habitual: la partició de xarxa no és opcional, l'elecció real és què fer durant una partició, i a la pràctica es decideix per operació. La pregunta útil no és si una base de dades és consistent, sinó què passa si aquella dada concreta està un segon desactualitzada.
Has conegut Firestore —documents i col·leccions, consistència forta per document, transaccions, temps real, mode offline, TTL automàtic, regles de seguretat i la necessitat d'índexs compostos— i hi has desat la cistella i les sessions d'AlpinaShop, desnormalitzant a propòsit i utilitzant transaccions on calia. Has conegut Bigtable —files ordenades per clau, famílies de columnes, sense índexs secundaris ni JOIN— i, sobretot, has après que el disseny de la row key és el disseny sencer: començar per un camp d'alta cardinalitat, posar el temps després, evitar els hotspots que provoca qualsevol clau seqüencial, i modelar a partir de les consultes i no de les dades. Has situat Spanner com la resposta a un problema molt concret —escala global amb ACID— i has vist per què a AlpinaShop no li correspon. Has afegit Memorystore com a memòria cau amb les seves tres regles: sempre TTL, la invalidació és el difícil, i només accessible des de la VPC.
La taula comparativa transversal i l'arbre de decisió et donen un mètode reutilitzable, i el repartiment de dades d'AlpinaShop és una decisió documentada i datada: Cloud SQL per a productes, comandes i clients; Firestore per a cistella i sessions; Memorystore per a la memòria cau de la portada; Cloud Storage per a les imatges; Pub/Sub i BigQuery per a la telemetria, amb Bigtable descartat per ara de manera explícita i raonada.
Amb això, AlpinaShop té resolts el còmput en quatre nivells i les dades en cinc serveis. Queda la pregunta que ho uneix tot i que tanca el mòdul: davant d'una càrrega concreta, quin servei de còmput cal triar? A 02-07, Com triar el servei de còmput adequat, recorrerem el continu d'abstracció de VM a funció, compararem Compute Engine, GKE Standard i Autopilot, App Engine, Cloud Run i Cloud Functions amb criteris mesurables, calcularem el cost d'un mateix escenari de trànsit en cadascun, veurem els patrons de migració lift-and-shift, replatform i refactor, i documentarem l'arquitectura definitiva d'AlpinaShop que servirà de base per a la resta del curs.
Curs de Google Cloud Platform (GCP)
Mòdul 1: Introducció a Google Cloud Platform
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
