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:

  1. Vytvořit objednávku (Služba objednávek) → OrdineCreato
  2. Rezervovat inventář (Služba zásob) → Rezervované zásoby
  3. Zpracovat platbu (Platební služba) → Platba potvrzena
  4. Potvrdit objednávku (Objednávka) → Objednávka potvrzena

Pokud se platba nezdaří, kompenzační transakce jsou:

  1. Uvolnit inventář (Služba zásob)
  2. 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.