Wzór Saga: transakcje rozproszone z choreografią i orkiestracją
W mikroserwisach nie ma czegoś takiego jak BEGIN TRANSACTION ... COMMIT który przekracza granice usług.
Jeżeli operacja obejmuje Usługę Zamówienia, Usługę Inwentaryzacji i Usługę Płatniczą,
Jak możemy zapewnić, że albo wszystkie trzy okażą się skuteczne, albo żadne nie będzie skuteczne?
The Wzór Sagi jest odpowiedź: każda usługa wykonuje własną transakcję lokalną
oraz w przypadku niepowodzenia transakcje kompensacyjne cofnąć to, co już zostało zrobione.
Dlaczego 2PC nie działa w mikrousługach
Il Zatwierdzenie dwufazowe (2 szt.) to klasyczny protokół dla transakcji rozproszonych: Koordynator prosi wszystkich uczestników o przygotowanie (faza przygotowań), a następnie zamów zatwierdzenie. Działa to dobrze w przypadku jednorodnych baz danych (np. dwóch instancji PostgreSQL), ale jest problematyczne w mikroserwisach:
- Sprzęganie: Wszystkie usługi muszą być dostępne podczas transakcji
- Wydajność: zasoby pozostają zablokowane do momentu zatwierdzenia (blokady rozproszone)
- Ograniczone wsparcie: Wiele usług w chmurze, interfejsów API REST i baz danych NoSQL nie obsługuje 2 komputerów
- Skalowalność: koordynator staje się pojedynczym punktem awarii
Wzorzec Saga zastępuje jedną dużą rozproszoną transakcję jedną sekwencja transakcji lokalnych, każdy realizowany niezależnie, koordynowany za pośrednictwem wydarzeń lub orkiestratora.
Przykład: Saga o zakupach w handlu elektronicznym
Kroki Saga dotyczące zakupu:
- Utwórz zamówienie (Serwis zamówień) → OrdineCreato
- Zapasy rezerwowe (Usługa inwentarza) → Zapasy zarezerwowane
- Przetwarzanie płatności (Usługa płatnicza) → Płatność potwierdzona
- Potwierdź zamówienie (Zamówienie usługi) → Zamówienie potwierdzone
Jeżeli płatność nie powiedzie się, transakcjami kompensującymi są:
- Zwolnij inwentarz (Usługa inwentaryzacji)
- Anuluj zamówienie (Zamówienie usługi)
Saga choreograficzna: autonomia usług
Nel Saga choreograficzna, nie ma centralnego koordynatora. Każdy serwis nasłuchuje zdarzeń wywołanych przez poprzedni serwis i odpowiednio reaguje, z kolei tworząc nowe wydarzenia. Logika koordynacji jest rozproszona między służbami: każda zna tylko swój krok i zdarzenia, na które reaguje.
Zawodowiec: maksymalne oddzielenie, brak pojedynczego punktu awarii, proste w zastosowaniu. Przeciwko: trudne do debugowania (logika sagi jest dystrybuowana), brak widoczności kluczowe dla stanu sagi jest ryzyko cykli źle zarządzanych wydarzeń.
// 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()));
}
}
}
}
Saga o orkiestracji: scentralizowana kontrola
w'Saga orkiestracyjna, centralny element zwany Orkiestrator Sagi (lub Kierownik Procesu) koordynuje całą sekwencję. Orkiestrator zna wszystkie kroki, wyślij polecenie do usług i poczekaj na ich reakcję. W przypadku niepowodzenia, koordynator jawnie wysyła polecenia kompensacji.
Zawodowiec: scentralizowana i widoczna logika, łatwa do debugowania, wyraźny stan sagi, bardziej precyzyjna obsługa błędów. Przeciwko: sprzęgło główne, orkiestrator zna wszystkie zaangażowane usługi.
// 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 z funkcjami krokowymi AWS (bezserwerowo)
W przypadku architektur bezserwerowych w AWS, Funkcje kroku jest to usługa zarządzana wdrożyć Sagę Orchestration. Każdy krok jest funkcją Lambda lub akcją AWS, i Step Functions automatycznie zarządzają stanem, ponownymi próbami i kompensacjami z definicji w języku stanów Amazonii (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"
}
}
}
Idempotencja w transakcjach kompensacyjnych
Transakcje kompensacyjne muszą być idempotentny: jeśli zostanie wywołany kilka razy (z powodu ponownej próby sieci lub programu Orchestrator), wynik musi być taki sam. Najpopularniejszą techniką jest użycie a klucz idempotencji: identyfikator zamówienia + typ operacji tworzą unikalny klucz, który zapobiega powielaniu operacji.
// 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()));
}
Śledzenie stanu Saga: wgląd w stan
Jedną z krytyki Orchestration Saga jest trudność w śledzeniu stanu w systemach o dużej objętości. Wyraźna tabela śledzenia sagi rozwiązuje ten problem:
-- 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;
Porównanie: choreografia kontra orkiestracja
| Czekam | Choreografia | Orkiestracja |
|---|---|---|
| Sprzęganie | Niski (zdarzenia) | Średni (orkiestrator zna usługi) |
| Widoczność stanu | Twarde (rozproszone) | Wysoka (scentralizowana) |
| Debugowanie | Twardy (logika rozproszona) | Łatwy (stan jawny) |
| Kompleksowość usług | Wysoka (każdy serwis zna rekompensaty) | Niski (proste usługi, logika w programie Orchestrator) |
| Skalowalność | Wysoka (brak pojedynczych punktów) | Średni (Orkiestrator w skali) |
| Idealny przypadek użycia | Kilka kroków, autonomiczne usługi, niezależne zespoły | Wiele kroków, złożona logika, niezbędna widoczność |
Kolejne kroki w serii
- Artykuł 6 – AWS EventBridge: Choreografia Saga korzysta z magistrali komunikatów wydarzenia wymiany. EventBridge to idealna magistrala zarządzana przez AWS do inteligentnego routingu zdarzeń pomiędzy Lambdą i SQS.
- Artykuł 10 – Wzór skrzynki nadawczej: Zapewnij atomową wysyłkę wydarzeń Saga wraz z aktualizacją lokalnej bazy danych.
Połącz z innymi seriami
- Pozyskiwanie źródeł zdarzeń + CQRS (art. 4): każdy etap Sagi wytwarza wydarzenia które można wykorzystać jako źródło prawdy do rekonstrukcji stanu zagregowanego. Axon Framework natywnie obsługuje kombinację Saga + Event Sourcing.
- Apache Kafka (seria 38): Kafka to idealny autobus do przesyłania wiadomości Choreografia Saga w produkcji, z gwarancją trwałości i porządku na partycję kluczową.







