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

  1. Per què no tot encaixa en una relacional
  2. Consistència i el teorema CAP sense dogmatisme
  3. Firestore: base de dades documental
  4. Firestore a AlpinaShop: cistella i sessions
  5. Bigtable: files ordenades i columnes amples
  6. Bigtable a AlpinaShop: telemetria de clics
  7. Spanner: relacional distribuïda
  8. Memorystore: memòria cau en Redis
  9. Taula comparativa transversal
  10. Arbre de decisió
  11. La decisió d'AlpinaShop

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

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

  1. 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() i average(), 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-prod

eur3 és una ubicació multiregió europea. Per a una sola regió seria europe-west1. La ubicació és immutable, igual que a Cloud Storage.

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

pip install google-cloud-firestore
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_TIMESTAMP utilitza 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 nombre i precio dins 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=ascending

Firestore 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:

gcloud firestore fields ttls update caduca_en \
  --collection-group=sesiones \
  --enable-ttl

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.

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

  1. 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,ctx
pip install google-cloud-bigtable
import 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=90d

On 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ó.

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

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

El 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).

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

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

  1. 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_TIMESTAMP a 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:

  1. L'històric de preus de cada producte, per auditar canvis (uns centenars de files al mes).
  2. Les valoracions i ressenyes de clients, amb text lliure, fotos associades i respostes niuades.
  3. La posició GPS dels repartidors durant el repartiment, una lectura cada 5 segons per repartidor.
  4. El nombre de vegades que s'ha vist la portada avui, mostrat al tauler intern.
  5. Les factures en PDF generades per a cada comanda.

Exercici 2: Firestore, cistella i consultes

  1. Crea una base de dades Firestore en mode Native.
  2. 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.
  3. Escriu una consulta que retorni les cistelles amb més de 100 € sense activitat en les últimes 48 hores.
  4. Explica quin índex compost necessita aquella consulta i com el crearies.
  5. 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.

  1. Proposa una row key per a la consulta (a) i justifica-la.
  2. Explica per què aquella clau no serveix bé per a la consulta (b) i què hi faries.
  3. Identifica un disseny de clau que produiria hotspots i explica el mecanisme.
  4. Defineix les famílies de columnes i quines columnes anirien a cadascuna.
  5. 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

gcloud firestore databases create --location=eur3 --type=firestore-native
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()]
  1. 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 error FAILED_PRECONDITION amb 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

  1. Row key per a la consulta (a):
<terme_normalitzat>#<timestamp>#<aleatori>
ex.: motxilla_40l#20260805094215#a7f3

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.

  1. 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.
  1. Un disseny que produiria hotspots:
<timestamp>#<terme>     ex.: 20260805094215#motxilla_40l

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.

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

  1. Política de caducitat: maxage=180d a 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=180d

Conclusió

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

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats