Saga Pattern: Distribuované transakce s choreografií a orchestrací
V mikroslužbách neexistuje nic takového jako BEGIN TRANSACTION ... COMMIT která překračuje hranice služeb.
Pokud operace zahrnuje objednávkovou službu, skladovou službu a platební službu,
Jak zajistíme, aby byly úspěšné buď všechny tři, nebo žádná?
The Vzor ságy je odpověď: každá služba provádí svou vlastní lokální transakci
a v případě neúspěchu, kompenzační transakce vrátit zpět to, co již bylo vykonáno.
Proč 2PC nefunguje v mikroslužbách
Il Dvoufázový závazek (2PC) je klasický protokol pro distribuované transakce: koordinátor žádá všechny účastníky, aby se připravili (přípravná fáze), pak objednejte commit. To funguje dobře pro homogenní databáze (např. dvě instance PostgreSQL), ale je to problematické v mikroslužbách:
- Spojka: Všechny služby musí být dostupné během transakce
- Výkon: prostředky zůstávají uzamčeny, dokud nejsou potvrzeny (distribuované zámky)
- Omezená podpora: Mnoho cloudových služeb, REST API a databází NoSQL nepodporuje 2PC
- Škálovatelnost: koordinátor se stává jediným bodem selhání
Vzor ságy nahrazuje jednu velkou distribuovanou transakci jednou sled místních transakcí, každý dokončen nezávisle, koordinovaný prostřednictvím akcí nebo orchestrátora.
Příklad: E-commerce Purchasing Saga
Kroky ságy k nákupu:
- Vytvořit objednávku (Služba objednávek) → OrdineCreato
- Rezervovat inventář (Služba zásob) → Rezervované zásoby
- Zpracovat platbu (Platební služba) → Platba potvrzena
- Potvrdit objednávku (Objednávka) → Objednávka potvrzena
Pokud se platba nezdaří, kompenzační transakce jsou:
- Uvolnit inventář (Služba zásob)
- Zrušit objednávku (Objednávka)
Choreografie Saga: Service Autonomy
Nel Choreografická sága, neexistuje žádný centrální orchestrátor. Každá služba naslouchá událostem vytvořeným předchozí službou a podle toho reaguje, na oplátku produkovat nové události. Koordinační logika je distribuována mezi službami: každá zná pouze svůj krok a události, na které reaguje.
Pro: maximální oddělení, žádný jediný bod selhání, jednoduchá implementace. Proti: obtížné ladit (logika ságy je distribuována), žádná viditelnost ústřední pro stav ságy, riziko cyklů špatně řízených událostí.
// CHOREOGRAPHY SAGA con Spring Kafka
// ===== STEP 1: Servizio Ordini - crea l'ordine =====
@Service
public class OrdiniService {
private final KafkaTemplate<String, Object> kafkaTemplate;
public String creaOrdine(CreaOrdineCommand cmd) {
String ordineId = UUID.randomUUID().toString();
// Salva l'ordine in stato PENDING
Ordine ordine = Ordine.builder()
.id(ordineId)
.clienteId(cmd.getClienteId())
.prodotti(cmd.getProdotti())
.stato(StatoOrdine.PENDING)
.build();
ordineRepository.save(ordine);
// Pubblica evento: il Servizio Inventario lo ascolterà
OrdineCreato event = new OrdineCreato(ordineId, cmd.getProdotti());
kafkaTemplate.send("ordini-events", ordineId, event);
return ordineId;
}
// Ascolta la conferma del pagamento per finalizzare l'ordine
@KafkaListener(topics = "pagamenti-events", groupId = "ordini-service")
public void onPagamento(ConsumerRecord<String, Object> record) {
if (record.value() instanceof PagamentoConfermato event) {
ordineRepository.updateStato(event.getOrdineId(), StatoOrdine.CONFERMATO);
kafkaTemplate.send("ordini-events", event.getOrdineId(),
new OrdineConfermato(event.getOrdineId()));
}
if (record.value() instanceof PagamentoFallito event) {
// Compensating: annulla l'ordine
ordineRepository.updateStato(event.getOrdineId(), StatoOrdine.ANNULLATO);
}
}
}
// ===== STEP 2: Servizio Inventario - riserva i prodotti =====
@Service
public class InventarioService {
@KafkaListener(topics = "ordini-events", groupId = "inventario-service")
public void onOrdinCreato(ConsumerRecord<String, Object> record) {
if (record.value() instanceof OrdineCreato event) {
try {
// Tenta la prenotazione dell'inventario
prenotaInventario(event.getOrdineId(), event.getProdotti());
// Successo: pubblica evento per il Servizio Pagamenti
kafkaTemplate.send("inventario-events", event.getOrdineId(),
new InventarioRiservato(event.getOrdineId(), event.getTotale()));
} catch (InventarioInsufficienterException e) {
// Fallimento: pubblica evento di fallimento
// Il Servizio Ordini annullerà l'ordine
kafkaTemplate.send("inventario-events", event.getOrdineId(),
new InventarioNonDisponibile(event.getOrdineId(), e.getMessage()));
}
}
// Compensating: rilascia l'inventario se il pagamento fallisce
if (record.value() instanceof PagamentoFallito event) {
rilasciaInventario(event.getOrdineId());
}
}
}
// ===== STEP 3: Servizio Pagamenti - processa il pagamento =====
@Service
public class PagamentiService {
@KafkaListener(topics = "inventario-events", groupId = "pagamenti-service")
public void onInventarioRiservato(ConsumerRecord<String, Object> record) {
if (record.value() instanceof InventarioRiservato event) {
try {
processaPagamento(event.getOrdineId(), event.getTotale());
kafkaTemplate.send("pagamenti-events", event.getOrdineId(),
new PagamentoConfermato(event.getOrdineId()));
} catch (PagamentoRifiutatoException e) {
kafkaTemplate.send("pagamenti-events", event.getOrdineId(),
new PagamentoFallito(event.getOrdineId(), e.getMessage()));
}
}
}
}
Orchestration Saga: Centralizované řízení
Nell'Orchestrační sága, centrální součást tzv Orchestr ságy (nebo Process Manager) koordinuje celou sekvenci. Orchestr zná všechny kroky, odeslat příkaz službám a čekat na jejich odezvu. V případě neúspěchu, orchestrátor explicitně posílá kompenzační příkazy.
Pro: centralizovaná a viditelná logika, snadné ladění, explicitní stav ságy, přesnější zpracování chyb. Proti: hlavní spojka, orchestrátor zná všechny zúčastněné služby.
// ORCHESTRATION SAGA con Axon Framework (Java)
// Axon gestisce automaticamente la persistenza dello stato della saga
import org.axonframework.saga.*;
import org.axonframework.eventhandling.*;
@Saga
public class AcquistoSaga {
@Autowired
private transient CommandGateway commandGateway;
// Lo stato della saga persiste automaticamente tra gli step
private String ordineId;
private List<ProdottoOrdinato> prodotti;
private BigDecimal totale;
// STEP 1: Avvio della saga quando viene creato un ordine
@StartSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineCreato event) {
this.ordineId = event.getOrdineId();
this.prodotti = event.getProdotti();
this.totale = event.getTotale();
// Invia comando al servizio inventario
commandGateway.send(new RiservaInventarioCommand(
ordineId,
prodotti
));
}
// STEP 2: Inventario riservato con successo - procedi con il pagamento
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioRiservato event) {
commandGateway.send(new ProcessaPagamentoCommand(
ordineId,
totale,
"CARTA_CREDITO"
));
}
// STEP 3: Pagamento confermato - finalizza l'ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(PagamentoConfermato event) {
commandGateway.send(new ConfermaOrdineCommand(ordineId));
}
// Fine saga (successo)
@EndSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineConfermato event) {
System.out.println("Saga completata con successo per ordine: " + ordineId);
}
// ===== COMPENSATING TRANSACTIONS =====
// Pagamento fallito: rilascia inventario, poi annulla ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(PagamentoFallito event) {
// Compensating step 1: rilascia inventario
commandGateway.send(new RilasciaInventarioCommand(ordineId, prodotti));
}
// Inventario rilasciato - annulla l'ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioRilasciato event) {
// Compensating step 2: annulla l'ordine
commandGateway.send(new AnnullaOrdineCommand(ordineId, "Pagamento fallito"));
}
// Inventario non disponibile - annulla direttamente
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioNonDisponibile event) {
commandGateway.send(new AnnullaOrdineCommand(ordineId, "Inventario insufficiente"));
}
// Fine saga per percorso di fallimento
@EndSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineAnnullato event) {
System.out.println("Saga fallita/annullata per ordine: " + ordineId + ". Motivo: " + event.getMotivo());
}
}
Saga s funkcemi AWS Step (bez serveru)
Pro bezserverové architektury na AWS, Krokové funkce je to řízená služba implementovat Orchestration Saga. Každý krok je funkce Lambda nebo akce AWS, a Step Functions automaticky řídí stav, opakování a kompenzace podle definice v jazyce Amazon States Language (ASL).
{
"Comment": "Saga acquisto e-commerce con compensating transactions",
"StartAt": "CreaOrdine",
"States": {
"CreaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:crea-ordine",
"Next": "RiservaInventario",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "FallimentoSaga"
}
]
},
"RiservaInventario": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:riserva-inventario",
"Next": "ProcessaPagamento",
"Catch": [
{
"ErrorEquals": ["InventarioInsufficienterException"],
"Next": "CompensaAnnullaOrdine"
}
]
},
"ProcessaPagamento": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:processa-pagamento",
"Retry": [
{
"ErrorEquals": ["TransientPaymentError"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Next": "ConfermaOrdine",
"Catch": [
{
"ErrorEquals": ["PagamentoRifiutatoException"],
"Next": "CompensaRilasciaInventario"
}
]
},
"ConfermaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:conferma-ordine",
"End": true
},
"CompensaRilasciaInventario": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:rilascia-inventario",
"Next": "CompensaAnnullaOrdine"
},
"CompensaAnnullaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:annulla-ordine",
"Next": "FallimentoSaga"
},
"FallimentoSaga": {
"Type": "Fail",
"Error": "SagaFailed",
"Cause": "La transazione distribuita e fallita, compensazioni eseguite"
}
}
}
Impotence při kompenzačních transakcích
Kompenzační transakce musí být idempotentní: pokud je vyvoláno několikrát (kvůli opakování sítě nebo orchestrátoru), výsledek musí být stejný. Nejběžnější technikou je použití a klíč idempotence: ID objednávky + typ operace tvoří jedinečný klíč, který zabraňuje duplicitním operacím.
// RilasciaInventarioHandler.java - Compensating transaction idempotente
@CommandHandler
public void handle(RilasciaInventarioCommand cmd) {
// Controllo idempotenza: questa compensazione e gia stata eseguita?
String idempotencyKey = cmd.getOrdineId() + ":RILASCIA_INVENTARIO";
if (compensazioniEseguite.contains(idempotencyKey)) {
System.out.println("Compensazione gia eseguita per " + idempotencyKey + ", skip");
return;
}
// Esegui la compensazione
for (ProdottoOrdinato prodotto : cmd.getProdotti()) {
inventarioRepository.incrementaDisponibilita(
prodotto.getProductId(),
prodotto.getQuantita()
);
}
// Segna la compensazione come eseguita
compensazioniEseguite.add(idempotencyKey);
// Pubblica evento per continuare la catena di compensazione
eventBus.publish(new InventarioRilasciato(cmd.getOrdineId()));
}
Saga State Tracking: Viditelnost do státu
Jednou z výtek Orchestration Saga je obtížnost sledování stavu ve velkoobjemových systémech. Explicitní tabulka sledování ságy řeší tento problém:
-- Tabella di tracking per le saga in corso
CREATE TABLE saga_state (
saga_id VARCHAR(36) PRIMARY KEY,
saga_type VARCHAR(100) NOT NULL,
ordine_id VARCHAR(36) NOT NULL,
current_step VARCHAR(100) NOT NULL,
stato VARCHAR(50) NOT NULL, -- IN_CORSO, COMPLETATA, FALLITA, COMPENSATA
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
error_message TEXT
);
-- Query utili per monitoring
-- Saga in corso da piu di 5 minuti (potenzialmente bloccate)
SELECT * FROM saga_state
WHERE stato = 'IN_CORSO'
AND started_at < NOW() - INTERVAL '5 minutes';
-- Distribuzione per step corrente
SELECT current_step, COUNT(*) as count
FROM saga_state WHERE stato = 'IN_CORSO'
GROUP BY current_step;
Srovnání: Choreografie vs orchestrace
| čekám | Choreografie | Orchestr |
|---|---|---|
| Spojka | Nízká (události) | Střední (orchestr zná služby) |
| Viditelnost stavu | Tvrdé (distribuované) | Vysoká (centralizovaná) |
| Ladění | Tvrdé (distribuovaná logika) | Snadné (explicitní stav) |
| Komplexnost služeb | Vysoká (každá služba zná kompenzace) | Nízká (jednoduché služby, logika v orchestrátoru) |
| Škálovatelnost | Vysoká (žádné jednotlivé body) | Střední (orchestrátor v měřítku) |
| Ideální případ použití | Málo kroků, autonomní služby, nezávislé týmy | Mnoho kroků, složitá logika, nezbytná viditelnost |
Další kroky v sérii
- Článek 6 – AWS EventBridge: Choreografie Saga používá sběrnici zpráv výměnné akce. EventBridge je ideální sběrnice spravovaná AWS pro inteligentní směrování událostí mezi Lambda a SQS.
- Článek 10 – Vzor pošty k odeslání: Zajistěte atomové odeslání událostí Saga spolu s aktualizací lokální databáze.
Propojení s ostatními sériemi
- Sourcing událostí + CQRS (článek 4): každý krok ságy vytváří události které lze použít jako zdroj pravdy k rekonstrukci agregovaného stavu. Axon Framework nativně podporuje kombinaci Saga + Event Sourcing.
- Apache Kafka (řada 38): Kafka je ideální komunikační sběrnice Choreografie Saga ve výrobě, se zárukami trvanlivosti a objednáním na klíčový oddíl.







