2PC가 마이크로서비스에서 작동하지 않는 이유

Il 2단계 커밋(2PC) 분산 트랜잭션을 위한 고전적인 프로토콜입니다. 코디네이터는 모든 참가자에게 준비를 요청합니다(준비 단계), 커밋을 주문합니다. 이는 동종 데이터베이스(예: PostgreSQL 인스턴스 2개)에서는 잘 작동하지만 마이크로서비스에서는 문제가 있습니다.

  • 연결: 거래 중에 모든 서비스를 사용할 수 있어야 합니다.
  • 성능: 리소스는 커밋될 때까지 잠긴 상태로 유지됩니다(분산 잠금).
  • 제한된 지원: 많은 클라우드 서비스, REST API, NoSQL 데이터베이스는 2PC를 지원하지 않습니다.
  • 확장성: 코디네이터는 단일 실패 지점이 됩니다.

Saga 패턴은 하나의 대규모 분산 트랜잭션을 하나의 트랜잭션으로 대체합니다. 현지 거래 순서, 각각은 독립적으로 완료되었으며 이벤트나 오케스트레이터를 통해 조정되었습니다.

예: 전자상거래 구매 사가

구매를 위한 Saga 단계:

  1. 주문 생성 (주문 서비스) → OrdineCreato
  2. 재고 예약 (재고서비스) → 예약재고
  3. 결제 처리 (결제서비스) → 결제확인
  4. 주문 확인 (주문서비스) → 주문확인

결제가 실패할 경우 보상 거래는 다음과 같습니다.

  1. 재고 해제 (인벤토리 서비스)
  2. 주문 취소 (주문 서비스)

안무 사가: 서비스 자율성

에서 안무사가, 중앙 오케스트레이터가 없습니다. 각 서비스는 이전 서비스에서 생성된 이벤트를 수신하고 그에 따라 반응합니다. 차례로 새로운 이벤트를 생성합니다. 조정 논리가 분산됩니다. 서비스 간: 각 서비스는 자신의 단계와 반응하는 이벤트만 알고 있습니다.

찬성: 최대 디커플링, 단일 실패 지점 없음, 구현이 간단합니다. 에 맞서: 디버그하기 어려움(saga 로직이 분산됨), 가시성 없음 무용담 상태의 핵심은 제대로 관리되지 않은 사건의 순환 위험입니다.

// 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 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());
    }
}

AWS Step Functions를 사용한 Saga(서버리스)

AWS의 서버리스 아키텍처의 경우, 단계 함수 매니지드 서비스 입니다 Orchestration Saga를 구현합니다. 각 단계는 Lambda 함수 또는 AWS 작업입니다. Step Functions는 정의에 따라 상태, 재시도 및 보상을 자동으로 관리합니다. 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"
    }
  }
}

보상 거래의 멱등성

보상 거래는 다음과 같아야합니다. 멱등성: 여러 번 호출되는 경우 (네트워크 또는 오케스트레이터 재시도로 인해) 결과는 동일해야 합니다. 가장 일반적인 기술은 다음과 같은 방법을 사용하는 것입니다. 멱등성 키: orderId + 작업 유형은 중복 작업을 방지하는 고유 키를 형성합니다.

// 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()));
}

사가 주 추적: 주에 대한 가시성

Orchestration Saga에 대한 비판 중 하나는 상태 추적이 어렵다는 것입니다. 대용량 시스템에서. 명시적인 Saga 추적 테이블은 이 문제를 해결합니다.

-- 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;

비교: 안무와 오케스트레이션

나는 기다린다 안무 관현악법
연결 낮음(이벤트) 중간(오케스트레이터가 서비스를 알고 있음)
상태 가시성 하드(분산) 높음(중앙 집중식)
디버깅 하드(분산 논리) 쉬움(명시적 상태)
서비스의 복잡성 높음(각 서비스는 보상을 알고 있음) 낮음(간단한 서비스, 오케스트레이터의 논리)
확장성 높음(단일 포인트 없음) 중간(규모 조정에 따른 조정자)
이상적인 사용 사례 몇 가지 단계, 자율 서비스, 독립 팀 많은 단계, 복잡한 논리, 필요한 가시성

시리즈의 다음 단계

  • 6조 – AWS EventBridge: 안무사가는 메시지 버스를 이용하여 교류행사. EventBridge는 지능형 라우팅을 위한 이상적인 AWS 관리형 버스입니다. Lambda와 SQS 간의 이벤트.
  • 기사 10 – 보낼 편지함 패턴: Saga 이벤트의 원자적 전달을 보장합니다. 로컬 데이터베이스 업데이트와 함께.

다른 시리즈와의 연계

  • 이벤트 소싱 + CQRS(4조): Saga의 각 단계는 이벤트를 생성합니다. 이는 집계 상태를 재구성하기 위한 진실의 소스로 사용될 수 있습니다. Axon Framework는 기본적으로 Saga + Event Sourcing 조합을 지원합니다.
  • 아파치 카프카(시리즈 38): Kafka는 이상적인 메시지 버스입니다. 안무사가 제작 중, 내구성 및 순서 보장 키 파티션당.