Il modello Saga: transazioni distribuite nell'architettura dei microservizi

Il modello Saga: transazioni distribuite nell'architettura dei microservizi

Nelle tradizionali applicazioni monolitiche, mantenere la coerenza dei dati tra più entità è semplice. I motori di database relazionali forniscono garanzie ACID (Atomicità, Coerenza, Isolamento, Durabilità) racchiuse nelle transazioni SQL locali. Se l’inserimento di un ordine, una detrazione di pagamento o una riserva di inventario falliscono a metà, la chiamata a ROLLBACK annulla istantaneamente ogni modifica del database.

Tuttavia, quando si passa a una moderna architettura di microservizi, la gestione dei dati cambia radicalmente. Per garantire l’autonomia del dominio e la scalabilità indipendente, ogni microservizio possiede il proprio database privato. Una singola operazione aziendale, come l’elaborazione di un checkout e-commerce, ora abbraccia più confini di servizi e motori di database (ad esempio, PostgreSQL per gli ordini, DynamoDB per i pagamenti, Redis per l’inventario).

Poiché i microservizi distribuiti non possono fare affidamento su una singola transazione di database, il mantenimento della coerenza dei dati oltre i confini della rete diventa uno dei problemi più impegnativi nell’ingegneria dei sistemi distribuiti.

Per risolvere questo problema senza sacrificare la disponibilità o le prestazioni del sistema, gli architetti software si affidano al Saga Pattern.

In questo approfondimento, esploreremo il motivo per cui le transazioni distribuite tradizionali falliscono, analizzeremo i meccanismi principali del modello Saga, confronteremo coreografia e orchestrazione, analizzeremo le contromisure di isolamento, esamineremo le implementazioni del codice di produzione in Go e Java e impareremo come gestire in modo sicuro i rollback degli errori nel mondo reale.


Il problema principale: perché il commit a due fasi (2PC) fallisce nei microservizi

Prima di adottare il modello Saga, gli ingegneri spesso si chiedono: Perché non possiamo utilizzare il tradizionale Two-Phase Commit (2PC/XA) nei nostri microservizi?

I meccanismi di 2PC

Two-Phase Commit coordina una transazione distribuita su più nodi di database utilizzando un Transaction Manager centrale in due fasi:

  1. Fase di preparazione: il gestore delle transazioni chiede a tutti i nodi del database partecipanti di preparare e bloccare le righe richieste. I partecipanti votano YES o NO.
  2. Fase di impegno: se tutti i partecipanti hanno votato YES, il Manager invia un comando COMMIT a tutti i nodi. Se un nodo ha votato NO o è scaduto, emette un ROLLBACK.
Client ----> Transaction Manager
                   |
     +-------------+-------------+
     | (Prepare)   | (Prepare)   | (Prepare)
     v             v             v
 Order DB     Payment DB    Inventory DB

Perché 2PC è un anti-pattern per i microservizi

Sebbene 2PC garantisca una forte coerenza, si guasta negli ambienti di microservizi nativi del cloud a causa di diversi difetti architetturali:

  1. Blocco e conflitto di risorse: le righe del database rimangono bloccate durante l’handshake di rete in più fasi. Se la latenza della rete aumenta o un servizio rallenta, i blocchi vengono mantenuti aperti, consumando rapidamente i pool di connessioni e causando errori di sistema a cascata.
  2. Collo di bottiglia della disponibilità: In 2PC, la disponibilità del sistema è limitata dal prodotto di tutte le disponibilità dei partecipanti ($A_{totale} = A_1 \times A_2 \times \dots \times A_n$). Se un singolo servizio o nodo del database va offline durante la fase di preparazione, l’intera transazione globale si blocca a tempo indeterminato.
  3. Perdita di autonomia del servizio: 2PC obbliga i servizi a esporre i protocolli XA a livello di database attraverso le API di rete, accoppiando direttamente i motori di database.
  4. Vincoli del broker: i broker di messaggi ad alto rendimento come Apache Kafka o RabbitMQ non partecipano in modo nativo alle tradizionali transazioni XA 2PC tra database relazionali.

Secondo il Teorema CAP, i sistemi distribuiti devono scegliere tra Strong Consistency (C) e High Availability (A) in Network Partitions (P). I moderni sistemi di microservizi danno priorità alla disponibilità e alla tolleranza delle partizioni, scambiando la coerenza istantanea con la coerenza eventuale (BASE: disponibilità fondamentale, stato soft, coerenza eventuale).


Cos’è il modello Saga?

Il modello Saga è stato originariamente proposto da Hector Garcia-Molina e Kenneth Salem nel 1987 come meccanismo per la gestione di transazioni di lunga durata nei sistemi di gestione di database. Nei microservizi moderni, una Saga rappresenta una sequenza di transazioni locali discrete.

Invece di racchiudere l’intero flusso multiservizio in un unico blocco globale, Saga esegue un processo aziendale come una serie di transazioni di database locali indipendenti ($T_1, T_2, \dots, T_n$).

Ogni transazione locale aggiorna i dati nel database di un singolo microservizio e pubblica un evento o un messaggio di dominio. Questo evento attiva la successiva transazione locale ($T_{i+1}$) nel servizio downstream.

[Order Service]         [Payment Service]       [Inventory Service]
  Local Tx T1 -------------> Local Tx T2 -------------> Local Tx T3
(Create Order)              (Process Payment)            (Reserve Stock)

Esecuzione a termine vs. compensazione a ritroso

Se tutte le transazioni locali hanno esito positivo, la Saga viene completata con successo (esecuzione in avanti).

Tuttavia, se una transazione locale fallisce a metà (ad esempio, $T_3$ fallisce perché un articolo è esaurito), Saga non può semplicemente chiamare un database ROLLBACK per i passaggi precedenti ($T_1, T_2$) perché quelle transazioni locali hanno già effettuato il commit nei rispettivi database.

Per annullare le transazioni locali già impegnate, la Saga deve eseguire le Transazioni di compensazione ($C_{n-1}, \dots, C_1$) in ordine inverso.

Forward Flow:    T1 (Create Order) ---> T2 (Charge Card) ---> T3 (Reserve Inventory - FAILS)
                                                                       |
Backward Rollback:  C1 (Cancel Order) <--- C2 (Refund Card) <----------+

Classificazione delle transazioni Saga

Per progettare un robusto flusso di lavoro Saga, ogni passaggio nella sequenza della transazione deve essere classificato in uno dei tre tipi strutturali:

Tipo di transazione Descrizione Requisito di idempotenza e rollback
Operazioni Compensabili Passi eseguiti prima del punto di non ritorno. Possono essere annullati o invertiti se un passaggio a valle fallisce. Deve avere una transazione di compensazione corrispondente ($C_i$).
Transazione pivot Il passo definitivo della Saga. Se il Pivot ha successo, la Saga finirà sicuramente. Se fallisce, la Saga torna indietro. Né risarcibile né risarcibile; segna il confine tra rollback e completamento in avanti.
Transazioni recuperabili Passaggi eseguiti dopo la transazione Pivot. Alla fine il loro successo è garantito e non richiedono alcun compenso. Devono essere rigorosamente idempotenti, poiché verranno ritentati automaticamente fino all’esito positivo.

Esempio di analisi del pagamento e-commerce

Considera un checkout dell’ordine e-commerce composto da quattro passaggi:

  1. $T_1$: Crea ordine in sospeso (Compensabile) $\to$ Annulla tramite $C_1$: Annulla ordine.
  2. $T_2$: Autorizza pagamento (Compensabile) $\to$ Annulla tramite $C_2$: Rimborso pagamento.
  3. $T_3$: Riserva inventario (Transazione pivot) $\to$ Se l’allocazione delle azioni ha esito positivo, l’ordine è finalizzato. Se fallisce, attiva $C_2$ e $C_1$.
  4. $T_4$: Richiesta di spedizione di spedizione (Riprovabile) $\to$ Eseguito dopo il pivot; riprovato fino alla consegna.

Stili architettonici della Saga: coreografia vs. orchestrazione

Esistono due stili architettonici principali per implementare il modello Saga nei sistemi distribuiti: Coreografia (decentralizzata) e Orchestration (centralizzata).

Stili di implementazione del modello Saga: coreografia e diagramma dell'architettura dell'orchestrazione

Stile 1: Coreografia (decentralizzazione guidata dagli eventi)

In una Saga basata sulla coreografia, non esiste un controllore o un coordinatore centrale. Invece, i microservizi comunicano in modo asincrono ascoltando gli eventi del dominio pubblicati su un bus di eventi centrale (come Apache Kafka, NATS o RabbitMQ).

Meccanica del flusso di lavoro

  1. Order Service esegue $T_1$ (crea un ordine in sospeso) ed emette un evento OrderCreated a Kafka.
  2. Il Servizio di pagamento ascolta OrderCreated, esegue $T_2$ (addebita la carta di credito) ed emette un evento PaymentCompleted (o PaymentFailed).
  3. Inventory Service ascolta PaymentCompleted, esegue $T_3$ (prenota articoli). Se gli articoli sono esauriti, emette InventoryReservationFailed.
  4. Il Servizio di pagamento ascolta InventoryReservationFailed ed esegue $C_2$ (emette un rimborso).
  5. Servizio ordini ascolta PaymentRefunded ed esegue $C_1$ (contrassegna l’ordine come annullato).

Vantaggi della coreografia

  • Loose Coupling: i servizi si iscrivono solo agli argomenti dell’evento; non conoscono altre implementazioni di servizi.
  • High throughput e decentralizzazione: il pub/sub dell’evento diretto elimina i colli di bottiglia dell’orchestratore centrale.
  • Semplicità per flussi di lavoro brevi: facile da configurare per flussi di lavoro semplici in 2-3 passaggi.

Svantaggi della coreografia

  • Rischio di dipendenza ciclica: i servizi possono finire per ascoltare gli eventi degli altri, creando dipendenze cicliche complesse.
  • Difficile tracciabilità del codice: comprendere il processo aziendale end-to-end richiede la tracciabilità della logica su più repository di codebase.
  • Event Storming e complessità: all’aumentare del numero di passaggi (ad esempio, 10+ servizi), la gestione dei casi limite di errore porta all’esplosione dello stato dell’evento.

Stile 2: orchestrazione (gestione centralizzata del flusso di lavoro)

In una Saga basata sull’orchestrazione, un microservizio dedicato noto come Saga Orchestrator controlla l’intero ciclo di vita della transazione distribuita. L’orchestratore funge da coordinatore centrale, inviando comandi espliciti ai microservizi partecipanti e ascoltando i loro eventi di risposta.

Meccanica del flusso di lavoro

  1. Il cliente invia una richiesta d’ordine al Saga Orchestrator.
  2. L’orchestrator invia un comando CreateOrder a Servizio ordini. Il servizio ordini restituisce OrderCreated.
  3. L’orchestrator aggiorna la propria macchina a stati e invia un comando ProcessPayment al Servizio di pagamento. Il servizio di pagamento restituisce PaymentSuccessful.
  4. L’orchestrator invia un comando ReserveInventory a Inventory Service. Il servizio di inventario restituisce InventoryFailed (Out of Stock).
  5. L’orchestrator rileva il guasto e avvia il flusso di compensazione:
    • Invia il comando RefundPayment al Servizio di pagamento.
    • Invia il comando CancelOrder al Servizio ordini.
  6. L’orchestrator contrassegna l’esecuzione della Saga come FAILED.

Vantaggi dell’orchestrazione

  • Logica aziendale centralizzata: lo stato del flusso di lavoro e la logica aziendale sono localizzati in un singolo servizio di orchestrazione o macchina a stati.
  • Nessuna dipendenza ciclica: i microservizi rispondono ai comandi dell’orchestratore; non dipendono né conoscono altri servizi a valle.
  • Cancella monitoraggio e debug: lo stato della transazione end-to-end viene archiviato esplicitamente nell’archivio stati dell’orchestratore (ad esempio, PostgreSQL o motori di flusso di lavoro come Temporal/Camunda).
  • Gestione degli errori più semplice: l’aggiunta di nuovi passaggi o la modifica delle regole di rollback viene gestita interamente all’interno dell’orchestratore.

Svantaggi dell’orchestrazione

  • Complessità dell’orchestratore: rischio di inserire troppa logica di dominio nell’orchestratore, trasformandolo in un anti-modello “orchestratore intelligente, servizio stupido”. -Potenziale singolo punto di errore: l’agente di orchestrazione deve essere reso altamente disponibile e con stato.

Matrice comparativa: coreografia vs. orchestrazione

Caratteristica Coreografia Orchestrazione
Struttura di controllo Decentralizzato (Event Pub/Sub) Centralizzato (Coordinatore Saga/Macchina a Stati)
Accoppiamento Estremamente basso (i servizi consumano eventi) Medio (I servizi accettano comandi dal Coordinatore)
Visibilità del processo Basso (distribuito nei file di registro) Alto (l’archivio a stato singolo visualizza il flusso di lavoro)
Il più adatto per Flussi di lavoro semplici (da 2 a 4 fasi di servizio) Flussi di lavoro aziendali complessi (oltre 5 passaggi, logica di ramificazione)
Strumenti/Framework Kafka, RabbitMQ, NATS, AWS EventBridge Temporal.io, Funzioni passo AWS, Camunda, Axon

Sfide e contromisure di isolamento (gestione di “ACID meno I”)

Poiché le transazioni locali in una Saga si impegnano immediatamente nei loro database locali, il modello Saga manca di Isolamento (I) dalle tradizionali garanzie ACID.

Se un client legge una riga del database modificata da $T_1$ mentre Saga è ancora in esecuzione, sta leggendo stato intermedio non impegnato. Se un passaggio a valle fallisce e attiva la compensazione ($C_1$), il client ha eseguito una Dirty Read.

Anomalie comuni causate dalla mancanza di isolamento

  1. Aggiornamenti persi: Saga A aggiorna un record. Prima che la Saga A venga completata, la Saga B sovrascrive lo stesso record. Se Saga A fallisce ed esegue la compensazione, sovrascrive l’aggiornamento di Saga B.
  2. Letture sporche: un cliente legge lo stock disponibile aggiornato da Saga A ($T_1$). La Saga A fallisce nel downstream ($T_3$) e ripristina lo stock ($C_1$), ma il cliente ha già effettuato un ordine sulla base di dati non aggiornati.
  3. Letture non ripetibili: un servizio legge i dati al passaggio $T_1$ e li legge di nuovo al passaggio $T_3$, ma un’altra Saga simultanea ha modificato i dati nel mezzo.

Contromisure e strategie di mitigazione

Per mantenere l’integrità dei dati nonostante la mancanza di isolamento, gli architetti software implementano modelli di progettazione di isolamento specifici:

1. Blocco semantico (stato in sospeso/contrassegnato)

Quando la transazione locale $T_1$ aggiorna un record del database, imposta un campo di stato su PENDING o APPROVAL_REQUIRED (ad esempio, ORDER_PENDING_PAYMENT).

Altre Saghe simultanee che leggono questo record devono controllare il flag di blocco semantico e bloccare o modificare il loro comportamento finché lo stato non cambia in COMMITTED o CANCELLED.

2. Impegno dell’ordine

Progettare la sequenza delle transazioni locali in modo che le operazioni ad alto rischio o irreversibili avvengano in una fase avanzata dell’esecuzione di Saga, riducendo al minimo la finestra di vulnerabilità.

3. Rilettura della convalida (controllo ottimistico della concorrenza)

Prima di eseguire un passaggio critico o una compensazione, rileggere il record del database di destinazione e verificare i timestamp della versione (version_id) per garantire che non si sia verificata alcuna modifica simultanea.

4. Visione pessimistica

Riordinare i passaggi di una Saga per ridurre al minimo l’esposizione economica (ad esempio, posizionare l’autorizzazione al pagamento il più vicino possibile alla transazione pivot).


Flusso della sequenza di produzione: transazioni di compensazione

Di seguito è riportato il diagramma di flusso della sequenza completa che illustra un errore di esecuzione in avanti e la conseguente esecuzione di compensazione all’indietro:

Diagramma di flusso della sequenza del modello Saga che mostra il fallimento dell'esecuzione in avanti e la compensazione dei rollback

Implementazioni pratiche del codice

Esploriamo esempi di implementazione pronti per la produzione sia per la coreografia (in Go) che per l’orchestrazione (in Java Spring Boot).

Implementazione 1: Saga basata sulla coreografia in Go

In questo esempio Go, mostriamo un Servizio ordini che gestisce la creazione degli ordini e ascolta gli eventi di mancato pagamento su un broker di eventi per eseguire la logica di rollback di compensazione.

package saga

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"
)

// Event definitions
type OrderCreatedEvent struct {
	OrderID   string  `json:"order_id"`
	CustomerID string `json:"customer_id"`
	Amount    float64 `json:"amount"`
}

type PaymentFailedEvent struct {
	OrderID string `json:"order_id"`
	Reason  string `json:"reason"`
}

// OrderRepository handles local DB operations
type OrderRepository interface {
	CreateOrder(ctx context.Context, orderID string, amount float64) error
	UpdateOrderStatus(ctx context.Context, orderID string, status string) error
}

// EventBus abstraction for message broker (e.g., Kafka / NATS)
type EventBus interface {
	Publish(topic string, payload []byte) error
	Subscribe(topic string, handler func(payload []byte)) error
}

type OrderSagaChoreographer struct {
	repo     OrderRepository
	eventBus EventBus
}

func NewOrderSagaChoreographer(repo OrderRepository, bus EventBus) *OrderSagaChoreographer {
	c := &OrderSagaChoreographer{repo: repo, eventBus: bus}
	c.registerSubscriptions()
	return c
}

// Step 1: Forward Transaction (T1)
func (s *OrderSagaChoreographer) StartOrderSaga(ctx context.Context, orderID, customerID string, amount float64) error {
	// Execute local database transaction
	err := s.repo.CreateOrder(ctx, orderID, amount)
	if err != nil {
		return fmt.Errorf("failed local DB transaction T1: %w", err)
	}

	// Emit domain event for downstream Payment Service
	event := OrderCreatedEvent{OrderID: orderID, CustomerID: customerID, Amount: amount}
	bytes, _ := json.Marshal(event)
	
	log.Printf("[SAGA][T1] Order %s created. Publishing OrderCreatedEvent...", orderID)
	return s.eventBus.Publish("orders.created", bytes)
}

// Register subscription for compensating events
func (s *OrderSagaChoreographer) registerSubscriptions() {
	_ = s.eventBus.Subscribe("payments.failed", func(payload []byte) {
		var event PaymentFailedEvent
		if err := json.Unmarshal(payload, &event); err != nil {
			log.Printf("[ERROR] Corrupt payment event: %v", err)
			return
		}

		// Execute Compensating Transaction (C1)
		s.handlePaymentFailed(context.Background(), event)
	})
}

// Step C1: Compensating Transaction
func (s *OrderSagaChoreographer) handlePaymentFailed(ctx context.Context, event PaymentFailedEvent) {
	log.Printf("[SAGA][C1] Payment failed for Order %s (Reason: %s). Rolling back local order...", event.OrderID, event.Reason)
	
	// Revert order status to CANCELLED in local DB
	err := s.repo.UpdateOrderStatus(ctx, event.OrderID, "CANCELLED_PAYMENT_FAILED")
	if err != nil {
		log.Printf("[CRITICAL] Failed to execute compensating transaction C1 for Order %s: %v", event.OrderID, err)
		// Trigger alert or write to Dead Letter Queue (DLQ)
		return
	}
	
	log.Printf("[SAGA][SUCCESS] Order %s successfully compensated and cancelled.", event.OrderID)
}

Implementazione 2: Saga basata sull’orchestrazione in Java (Spring Boot)

In questo esempio Java, creiamo un Saga Orchestrator utilizzando un modello di macchina a stati per coordinare i comandi in avanti ed eseguire rollback compensativi quando un passaggio downstream fallisce.

package com.ghaznix.saga.orchestrator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.UUID;

public enum SagaState {
    STARTED,
    ORDER_CREATED,
    PAYMENT_PROCESSED,
    INVENTORY_RESERVED,
    COMPLETED,
    COMPENSATING_PAYMENT,
    COMPENSATING_ORDER,
    FAILED
}

@Service
public class OrderSagaOrchestrator {

    private static final Logger log = LoggerFactory.getLogger(OrderSagaOrchestrator.class);

    private final OrderServiceClient orderClient;
    private final PaymentServiceClient paymentClient;
    private final InventoryServiceClient inventoryClient;

    public OrderSagaOrchestrator(OrderServiceClient orderClient,
                                PaymentServiceClient paymentClient,
                                InventoryServiceClient inventoryClient) {
        this.orderClient = orderClient;
        this.paymentClient = paymentClient;
        this.inventoryClient = inventoryClient;
    }

    public boolean executeOrderSaga(String customerId, String productId, double amount, int quantity) {
        String sagaId = UUID.randomUUID().toString();
        log.info("[SAGA {}] Starting Order Saga Workflow...", sagaId);
        
        SagaState currentState = SagaState.STARTED;
        String orderId = null;
        String paymentId = null;

        try {
            // Step 1: Forward Local Tx T1 - Create Order
            log.info("[SAGA {}][T1] Sending CreateOrder command...", sagaId);
            orderId = orderClient.createOrder(customerId, productId, amount);
            currentState = SagaState.ORDER_CREATED;

            // Step 2: Forward Local Tx T2 - Process Payment
            log.info("[SAGA {}][T2] Sending ProcessPayment command for Order {}...", sagaId, orderId);
            paymentId = paymentClient.chargePayment(orderId, customerId, amount);
            currentState = SagaState.PAYMENT_PROCESSED;

            // Step 3: Forward Local Tx T3 (Pivot Step) - Reserve Inventory
            log.info("[SAGA {}][T3] Sending ReserveInventory command for Product {}...", sagaId, productId);
            boolean stockReserved = inventoryClient.reserveStock(productId, quantity);

            if (!stockReserved) {
                throw new InventoryAllocationException("Stock allocation failed: Item out of stock.");
            }

            currentState = SagaState.INVENTORY_RESERVED;
            log.info("[SAGA {}][SUCCESS] Saga completed successfully!", sagaId);
            return true;

        } catch (Exception ex) {
            log.error("[SAGA {}][FAILURE] Step failed during state {}. Triggering Compensation...", sagaId, currentState, ex);
            rollbackSaga(sagaId, currentState, orderId, paymentId);
            return false;
        }
    }

    private void rollbackSaga(String sagaId, SagaState failedState, String orderId, String paymentId) {
        log.info("[SAGA {}] Initiating backward compensating transactions from state: {}", sagaId, failedState);

        // Compensate Step 2 if payment was processed
        if (failedState == SagaState.PAYMENT_PROCESSED || failedState == SagaState.INVENTORY_RESERVED) {
            try {
                log.info("[SAGA {}][C2] Executing Payment Refund compensation for Payment {}...", sagaId, paymentId);
                paymentClient.refundPayment(paymentId);
            } catch (Exception e) {
                log.error("[CRITICAL][SAGA {}] Payment refund C2 failed! Manual intervention or DLQ required.", sagaId, e);
            }
        }

        // Compensate Step 1 if order was created
        if (failedState != SagaState.STARTED && orderId != null) {
            try {
                log.info("[SAGA {}][C1] Executing Order Cancel compensation for Order {}...", sagaId, orderId);
                orderClient.cancelOrder(orderId);
            } catch (Exception e) {
                log.error("[CRITICAL][SAGA {}] Order cancellation C1 failed!", sagaId, e);
            }
        }

        log.info("[SAGA {}] Saga compensation workflow finished. Final State: FAILED.", sagaId);
    }
}

Elementi essenziali per la produzione: abbinamento di Saga al modello di posta in uscita transazionale

Sia in Choreography che in Orchestration, l’esecuzione di una transazione locale ($T_i$) richiede la pubblicazione di un evento di dominio o di un comando sulla rete.

Se il tuo servizio aggiorna il proprio database SQL e quindi pubblica un messaggio in Kafka, un problema tecnico di rete dopo il commit del database provoca la perdita di eventi silenziosi. Al contrario, la pubblicazione del messaggio prima del commit del database porta all’elaborazione dell’evento fantasma.

Per risolvere questo problema, Sagas deve essere abbinato al Modello di posta in uscita transazionale:

[Service Database Transaction Boundary]
+-----------------------------------------------------+
| 1. INSERT INTO business_table (orders/payments)    |
| 2. INSERT INTO outbox_table (event_payload)        |
+-----------------------------------------------------+
                          |
             (CDC / Polling Message Relay)
                          |
                          v
               [Message Broker / Kafka]

Persistentemente il payload dell’evento in una tabella outbox all’interno della stessa transazione del database locale, l’atomicità è garantita. Un processo in background asincrono (come Debezium o un relè di polling) legge dalla tabella della posta in uscita e pubblica eventi in Kafka in modo affidabile.

Inoltre, ogni consumatore del servizio downstream deve implementare l’Idempotenza (utilizzando idempotency_key univoco o intestazioni di deduplicazione dei messaggi) in modo che le consegne di messaggi duplicati durante i tentativi non causino addebiti duplicati o allocazioni di inventario.


Lista di controllo delle decisioni architettoniche

Utilizza questa matrice decisionale pratica durante la progettazione di transazioni distribuite per applicazioni di microservizi:

                  Do you need cross-service data consistency?
                                     |
                    +----------------+----------------+
                    | No                              | Yes
                    v                                 v
         Standard Single Service            Can you accept Eventual
            Local Database                    Consistency (BASE)?
                                                      |
                                     +----------------+----------------+
                                     | No                              | Yes
                                     v                                 v
                          Use Monolithic Core            Adopt Saga Pattern
                          with Single ACID DB                 |
                                                              |
                                             How complex is the workflow?
                                                              |
                                            +-----------------+-----------------+
                                            | Simple (2-3 steps)                | Complex (4+ steps/branches)
                                            v                                   v
                                   Choreography Saga                   Orchestration Saga
                                   (Event-Driven Bus)                  (Temporal/Custom State Machine)

Conclusione e punti salienti

Il Saga Pattern è un modello architetturale essenziale per la gestione delle transazioni distribuite oltre i confini dei microservizi senza bloccare le risorse o sacrificare la disponibilità del sistema.

Elenco di controllo riepilogativo:

  1. Abbandona 2PC/XA nei microservizi cloud-native: il commit a due fasi causa blocchi serrati, latenza elevata e gravi colli di bottiglia nella disponibilità.
  2. Dividi le transazioni in passaggi locali: dividi le operazioni globali in transazioni locali ($T_1 \dots T_n$) abbinate all’inversione delle transazioni di compensazione ($C_1 \dots C_{n-1}$).
  3. Scegli lo stile architettonico giusto:
    • Utilizza la Coreografia per flussi semplici, guidati da eventi in 2-3 fasi con accoppiamento libero.
    • Utilizza Orchestration per flussi di lavoro aziendali complessi che richiedono visibilità centralizzata, ramificazione e monitoraggio della macchina a stati.
  4. Implementare contromisure di isolamento: protezione da letture sporche e aggiornamenti persi utilizzando i blocchi semantici (flag PENDING) e il blocco ottimistico di rilettura.
  5. Garantire una messaggistica affidabile: associare sempre le implementazioni di Saga al modello di posta in uscita transazionale e imporre consumatori idempotenti per gestire i tentativi in ​​modo sicuro.

Implementando attentamente il modello Saga, puoi creare microservizi altamente disponibili e scalabili che rimangono resilienti e coerenti anche in caso di guasto delle reti.