En la lliçó anterior vam veure què és un esdeveniment i qui el produeix o consumeix. Ara baixem un nivell: com viatja físicament aquest missatge d'un servei a un altre de forma fiable quan tots dos no es coneixen i poden estar arrencant, caient o saturats en moments diferents? La resposta és la missatgeria asíncrona a través de brokers de missatges. Un broker és una peça d'infraestructura intermèdia que rep missatges dels productors, els emmagatzema de forma duradora i els entrega als consumidors quan aquests estan preparats.
Dominar la missatgeria asíncrona és imprescindible perquè d'ella depenen la fiabilitat i la coherència de qualsevol sistema distribuït. En aquesta lliçó estudiarem les dues primitives bàsiques (cues i topics), les garanties d'entrega, el problema dels duplicats i com resoldre'l amb idempotència, i compararem les tres tecnologies més usades del mercat: RabbitMQ, Apache Kafka i Amazon SQS.
Contingut
- Per què missatgeria asíncrona?
- Cues (point-to-point) vs Topics (publicació/subscripció)
- Garanties d'entrega: at-most-once, at-least-once, exactly-once
- El problema dels duplicats i la idempotència
- Comparativa: RabbitMQ vs Kafka vs Amazon SQS
- Exemple pràctic: productor i consumidor idempotent
- Errors comuns i consells
- Exercicis i solucions
- Conclusió
- Per què missatgeria asíncrona?
En una crida síncrona, si el servei B està caigut, la crida d'A falla. Amb missatgeria asíncrona, A diposita el missatge al broker i continua treballant; quan B es recuperi, el consumirà. Això aporta:
- Desacoblament temporal: productor i consumidor no necessiten estar vius al mateix temps.
- Amortiment de pics (buffering): si arriben 10.000 missatges de cop, el broker els reté i el consumidor els processa al seu ritme.
- Resiliència: un consumidor lent o caigut no tomba el productor.
- Cues (point-to-point) vs Topics (publicació/subscripció)
Existeixen dos models de distribució de missatges.
Cua (point-to-point)
Un missatge en una cua el processa un únic consumidor. Si hi ha diversos consumidors connectats a la mateixa cua, el broker reparteix (balanceja) els missatges entre ells, però cada missatge va a un de sol. Es fa servir per distribuir treball (work queue).
Topic (publicació/subscripció)
Un missatge publicat en un topic s'entrega a tots els subscriptors. Es fa servir per difondre esdeveniments a múltiples interessats.
flowchart TB
subgraph Cola["CUA (point-to-point)"]
P1[Productor] --> Q[(Cua)]
Q --> CA[Consumidor A]
Q --> CB[Consumidor B]
Q -. cada missatge a UN de sol .-> CA
end
subgraph Topic["TOPIC (pub/sub)"]
P2[Productor] --> T{{Topic}}
T --> SA[Subscriptor A]
T --> SB[Subscriptor B]
T -. cada missatge a TOTS .-> SA
end| Aspecte | Cua (point-to-point) | Topic (pub/sub) |
|---|---|---|
| Receptors per missatge | Un | Tots els subscriptors |
| Objectiu | Repartir càrrega de treball | Difondre esdeveniments |
| Patró típic | Comandes / tasques | Esdeveniments de domini |
| Exemple | Cua d'enviament d'emails | "ComandaCreada" a inventari, facturació, enviaments |
- Garanties d'entrega: at-most-once, at-least-once, exactly-once
Quan un missatge viatja per la xarxa, es poden perdre acusaments de rebut (acks), caure processos, etc. Per això existeixen diferents nivells de garantia:
| Garantia | Significat | Risc | Cost |
|---|---|---|---|
| At-most-once | S'entrega 0 o 1 vegada | Es pot perdre un missatge | Mínim (sense reintents) |
| At-least-once | S'entrega 1 o més vegades | Es pot duplicar | Mitjà (requereix reintents i acks) |
| Exactly-once | S'entrega exactament 1 vegada | Cap (ideal) | Alt (complex, no sempre real) |
Punts clau:
- At-most-once: el consumidor confirma abans de processar. Si falla, el missatge es perd. Només vàlid si perdre alguna dada és acceptable (p. ex. mètriques no crítiques).
- At-least-once: el consumidor confirma després de processar amb èxit. Si falla just abans de l'ack, el missatge es torna a entregar → pot haver-hi duplicats. És el nivell més habitual i recomanat.
- Exactly-once: semànticament perfecte, però costós. En sistemes distribuïts purs és molt difícil de garantir d'extrem a extrem; sol aconseguir-se combinant at-least-once amb idempotència en el consumidor (ho veurem a continuació). Kafka ofereix "exactly-once" dins dels seus propis límits (transaccions), però tan bon punt l'efecte surt fora de Kafka (escriure en una altra BD, cridar una API), torna a dependre de la idempotència.
Regla d'or del món real: dissenya per a at-least-once i fes els teus consumidors idempotents. Això et dona, a la pràctica, l'efecte d'exactly-once.
- El problema dels duplicats i la idempotència
Una operació és idempotent si executar-la diverses vegades produeix el mateix resultat que executar-la una sola vegada. Si els nostres consumidors són idempotents, els duplicats d'at-least-once deixen de ser un problema.
Tècniques habituals per aconseguir idempotència:
- Clau d'idempotència / ID de missatge: registrar els IDs ja processats i descartar els repetits.
- Operacions naturalment idempotents:
UPDATE saldo SET valor = 100(assignació absoluta) és idempotent;UPDATE saldo SET valor = valor + 100(increment) no ho és. UPSERTamb clau única: inserir o ignorar si ja existeix.
-- Taula que registra quins missatges ja hem processat.
CREATE TABLE mensajes_procesados (
mensaje_id VARCHAR(64) PRIMARY KEY, -- clau d'idempotència
procesado_en TIMESTAMP NOT NULL
);
-- En rebre un missatge, intentem inserir el seu ID.
-- Si ja existeix (clau primària duplicada), sabem que és un duplicat.
INSERT INTO mensajes_procesados (mensaje_id, procesado_en)
VALUES ('msg-abc-123', CURRENT_TIMESTAMP)
ON CONFLICT (mensaje_id) DO NOTHING; -- PostgreSQL: ignora si ja existeixExplicació:
mensaje_idés la clau primària: la base de dades garanteix que no n'hi hagi dos d'iguals.ON CONFLICT ... DO NOTHINGfa que el segon intent d'inserir el mateix ID no produeixi error ni efecte. Així detectem el duplicat sense lògica addicional complexa.- Si l'
INSERTva afectar 0 files, era un duplicat i podem saltar-nos el processament.
- Comparativa: RabbitMQ vs Kafka vs Amazon SQS
| Característica | RabbitMQ | Apache Kafka | Amazon SQS |
|---|---|---|---|
| Model principal | Broker de cues (AMQP) | Log distribuït d'esdeveniments | Cua gestionada (cloud) |
| Paradigma | Cues + exchanges (pub/sub) | Topics particionats + offsets | Cues (Standard i FIFO) |
| Retenció de missatges | Fins a consumir (s'esborren) | Configurable (dies/setmanes), reproduïble | Fins a 14 dies |
| Reproducció (replay) | No nativa | Sí (rellegir des d'un offset) | No |
| Ordre | Per cua | Per partició | FIFO només en cues FIFO |
| Throughput | Alt | Molt alt (milions/s) | Alt (escala automàtica) |
| Garantia típica | At-least-once | At-least-once / exactly-once* | At-least-once (Std) / exactly-once (FIFO) |
| Gestió | Autogestionat / cloud | Autogestionat / gestionat | Totalment gestionat (AWS) |
| Cas ideal | Enrutament complex, RPC, tasques | Streaming, event sourcing, big data | Desacoblament simple a AWS sense operar infra |
Resum pràctic:
- RabbitMQ: excel·lent quan necessites enrutament flexible (exchanges amb regles) i patrons tradicionals de cues de treball.
- Kafka: l'elecció per a alt volum, retenció i reproducció d'esdeveniments; base d'event sourcing i streaming (lliçó 05-05).
- SQS: el més simple si ja ets a AWS i només vols desacoblar sense gestionar servidors.
- Exemple pràctic: productor i consumidor idempotent
Vegem un consumidor de Kafka en Spring que aplica at-least-once + idempotència.
@Component
public class ConsumidorPagos {
private final RepositorioIdempotencia idempotencia;
private final ServicioContabilidad contabilidad;
public ConsumidorPagos(RepositorioIdempotencia idempotencia,
ServicioContabilidad contabilidad) {
this.idempotencia = idempotencia;
this.contabilidad = contabilidad;
}
@KafkaListener(topics = "pagos.confirmados", groupId = "contabilidad")
public void consumir(PagoConfirmadoEvent evento, Acknowledgment ack) {
// 1. Ja hem processat aquest missatge? -> idempotència
if (!idempotencia.registrarSiEsNuevo(evento.pagoId())) {
ack.acknowledge(); // duplicat: confirmem i sortim
return;
}
// 2. Lògica de negoci real
contabilidad.asentarApunte(evento);
// 3. Confirmem NOMÉS després de processar amb èxit (at-least-once)
ack.acknowledge();
}
}Explicació detallada:
@KafkaListenersubscriu el mètode al topicpagos.confirmados. ElgroupId"contabilidad" identifica aquest grup de consumidors; Kafka reparteix les particions entre els membres del grup.registrarSiEsNuevo(...)intenta inserir l'ID (com en el SQL anterior). Retornafalsesi ja existia → és un duplicat, el confirmem i sortim sense reprocessar.- La confirmació (
ack.acknowledge()) es fa després d'asentarApunte. Si el procés mor abans de l'ack, Kafka tornarà a entregar el missatge (at-least-once), però la idempotència evitarà el doble assentament.
# Configuració Spring Kafka per a confirmació manual (clau de l'at-least-once)
spring:
kafka:
consumer:
group-id: contabilidad
enable-auto-commit: false # NO confirmar automàticament
auto-offset-reset: earliest # llegir des del principi si no hi ha offset
listener:
ack-mode: manual # confirmem nosaltres amb ack.acknowledge()enable-auto-commit: falseés essencial: si Kafka confirmés sol, podria confirmar abans que acabéssim de processar i perdríem missatges davant d'una fallada.ack-mode: manualdelega en el nostre codi el moment exacte de la confirmació.
Errors Comuns i Consells
- Confirmar abans de processar. Converteix el teu at-least-once en at-most-once accidental i perds missatges davant de fallades. Confirma sempre al final.
- Creure que "exactly-once" elimina la necessitat d'idempotència. Tan bon punt l'efecte creua la frontera del broker (una altra BD, una API externa), necessites idempotència igualment.
- No dimensionar la dead-letter queue (DLQ). Els missatges que fallen repetidament han d'anar a una cua de descart per no bloquejar la cua principal en un bucle infinit de reintents.
- Fer servir topic quan volies cua (o a la inversa). Difondre una comanda a "tots" pot executar la mateixa acció N vegades.
- Consell: defineix sempre un camp
mensaje_idúnic en els teus esdeveniments des del primer dia; afegir-lo després és dolorós.
Exercicis
- Un equip processa pagaments amb un consumidor que confirma el missatge just en rebre'l i després contacta amb la passarel·la bancària. Si el procés mor entre l'ack i la crida bancària, quina garantia real tenen i quin problema passa? Com ho corregiries?
- Indica per a cada cas si faries servir cua o topic: (a) enviar el correu de benvinguda una sola vegada; (b) notificar a inventari, facturació i CRM d'una nova comanda; (c) repartir 1.000 tasques de generació de PDF entre 5 workers.
- Escriu una sentència SQL idempotent per "marcar una comanda com a pagada" que pugui executar-se diverses vegades sense efectes secundaris.
Solucions
- Tenen at-most-once: si mor després de l'ack, el missatge no es torna a entregar i el pagament mai no arriba a la passarel·la (es perd). Correcció: confirmar després de la crida bancària (at-least-once) i fer l'operació idempotent amb la clau d'idempotència del pagament per no cobrar dues vegades davant d'un reintent.
- (a) Cua (un sol receptor, una sola vegada). (b) Topic (tres subscriptors reben l'esdeveniment). (c) Cua (repartiment de treball entre workers).
- Per exemple:
-- Assignació absoluta de l'estat: idempotent. UPDATE pedidos SET estado = 'PAGADO', pagado_en = COALESCE(pagado_en, CURRENT_TIMESTAMP) WHERE pedido_id = :pedidoId AND estado <> 'PAGADO';
Executar-la de nou no canvia res perquè la condició estado <> 'PAGADO' ja no es compleix i pagado_en es conserva amb COALESCE.
Conclusió
La missatgeria asíncrona és el sistema circulatori de les arquitectures dirigides per esdeveniments. Vam diferenciar cues (un receptor, repartiment de càrrega) de topics (tots els subscriptors, difusió). Vam entendre les tres garanties d'entrega i per què at-least-once + idempotència és la combinació pragmàtica que fa servir la indústria. Finalment, vam comparar RabbitMQ, Kafka i SQS per saber quan triar cadascuna.
En la lliçó següent, "Patrons d'Esdeveniments: Event Sourcing i CQRS", veurem com, en lloc de desar només l'estat actual, podem emmagatzemar la seqüència completa d'esdeveniments com a font de veritat, i com separar els models de lectura i escriptura per escalar.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
