Chuyển đến nội dung chính

Bài 10: Saga Pattern — Distributed Transactions

Tại sao 2PC không phù hợp cho microservices, Saga Pattern (Choreography vs Orchestration), compensating transactions, Saga Orchestrator implementation, error handling và dead letter queue.

🏗️ Kiến trúc — Bài 10 Bài 10: Saga Pattern — Distributed Transactions

Cloud Native Microservices Architecture

Phần 3: Data Management trong Microservices

xdev.asia

Bài 10: Saga Pattern — Distributed Transactions

Giới thiệu

Trong monolith, một business transaction có thể bao gồm nhiều database operations trong cùng một ACID transaction. Trong microservices, mỗi service có database riêng — không thể dùng distributed transaction truyền thống (2PC) vì nó tạo tight coupling và ảnh hưởng performance. Saga Pattern là giải pháp thay thế.


1. Vấn đề với Distributed Transactions

1.1 Two-Phase Commit (2PC) — Tại sao không phù hợp

2PC Flow:
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ Transaction  │    │  Service A   │    │  Service B   │
│ Coordinator  │    │  (Order)     │    │  (Payment)   │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       │── Phase 1: PREPARE ──────────────────▶│
       │◀── VOTE YES ────────────────────────── │
       │── Phase 1: PREPARE ──▶│               │
       │◀── VOTE YES ──────── │               │
       │                      │               │
       │── Phase 2: COMMIT ──▶│               │
       │── Phase 2: COMMIT ──────────────────▶│
       │                      │               │

Vấn đề của 2PC trong microservices:

Vấn đềGiải thích
Single Point of FailureCoordinator chết → tất cả participant bị lock
Performance bottleneckLock resources trong suốt quá trình 2PC
Tight couplingTất cả services phải available cùng lúc
Not scalableLock contention tăng theo số participant
Network partitionNếu mạng đứt giữa Phase 1 và Phase 2 → inconsistent state

1.2 CAP Theorem nhắc lại

Trong hệ thống phân tán, chỉ có thể đạt được 2 trong 3:

  • Consistency: Mọi read đều trả về write mới nhất
  • Availability: Mọi request đều nhận được response
  • Partition tolerance: Hệ thống tiếp tục hoạt động khi mạng bị chia cắt

Microservices chọn AP (Availability + Partition tolerance) → chấp nhận Eventual Consistency.


2. Saga Pattern

2.1 Concept

Saga là chuỗi local transactions, mỗi transaction thực hiện bởi một service. Nếu một step fail, saga thực hiện compensating transactions để undo các step trước đó.

Saga = T1 → T2 → T3 → ... → Tn

Nếu Ti fail:
  Compensate: C(i-1) → C(i-2) → ... → C1

Ví dụ Order Saga:
  T1: Create Order (status: PENDING)
  T2: Reserve Payment
  T3: Reserve Inventory
  T4: Confirm Order (status: CONFIRMED)

Nếu T3 fail:
  C2: Refund Payment
  C1: Cancel Order (status: CANCELLED)

2.2 Compensating Transactions

Mỗi transaction trong saga phải có compensating transaction tương ứng:

StepActionCompensating Action
T1Create OrderCancel Order
T2Reserve PaymentRefund Payment
T3Reserve InventoryRelease Inventory
T4Schedule ShippingCancel Shipping
T5Send Confirmation EmailSend Cancellation Email

Lưu ý quan trọng:

  • Compensating transactions phải là idempotent (chạy nhiều lần cho cùng kết quả)
  • Compensating transactions không thể fail (phải retry cho đến khi thành công)
  • Một số actions không thể compensate (ví dụ: gửi email đã gửi) → dùng "semantic undo" (gửi email hủy)

3. Choreography Saga

3.1 Concept

Mỗi service publish event khi hoàn thành local transaction, service tiếp theo subscribe và xử lý:

┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Order   │    │   Payment    │    │  Inventory   │    │  Shipping    │
│  Service │    │   Service    │    │  Service     │    │  Service     │
└────┬─────┘    └──────┬───────┘    └──────┬───────┘    └──────┬───────┘
     │                 │                   │                   │
     │── OrderCreated ▶│                   │                   │
     │                 │── PaymentReserved ▶│                   │
     │                 │                   │── InventoryReserved ▶│
     │                 │                   │                   │── ShipmentScheduled
     │◀───────────────────────────────────────────────────────── │
     │  OrderConfirmed │                   │                   │
     │                 │                   │                   │
     │ === FAILURE === │                   │                   │
     │                 │                   │── InventoryFailed ▶│
     │                 │◀─ CompensatePayment─                  │
     │◀─ PaymentRefunded─                  │                   │
     │  OrderCancelled │                   │                   │

3.2 Implementation Example

// Order Service — publishes OrderCreated
@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderCommand cmd) {
        Order order = new Order(cmd.getCustomerId(), cmd.getItems());
        order.setStatus(OrderStatus.PENDING);
        orderRepository.save(order);

        // Publish event
        eventPublisher.publish(new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotalAmount()
        ));

        return order;
    }

    // Compensating handler
    @EventHandler
    public void on(PaymentFailedEvent event) {
        Order order = orderRepository.findById(event.getOrderId());
        order.setStatus(OrderStatus.CANCELLED);
        order.setFailureReason(event.getReason());
        orderRepository.save(order);
    }
}

// Payment Service — listens to OrderCreated
@Service
public class PaymentService {

    @EventHandler
    public void on(OrderCreatedEvent event) {
        try {
            Payment payment = paymentGateway.reserve(
                event.getCustomerId(),
                event.getTotalAmount()
            );
            eventPublisher.publish(new PaymentReservedEvent(
                event.getOrderId(),
                payment.getId()
            ));
        } catch (InsufficientFundsException e) {
            eventPublisher.publish(new PaymentFailedEvent(
                event.getOrderId(),
                "Insufficient funds"
            ));
        }
    }
}

3.3 Ưu nhược điểm

Ưu điểmNhược điểm
Loose coupling giữa servicesKhó theo dõi flow phức tạp
Đơn giản cho saga ít stepsCyclic dependencies có thể xảy ra
Không có single point of failureKhó debug khi có lỗi
Tự nhiên với event-driven architectureTesting phức tạp

4. Orchestration Saga

4.1 Concept

Một Saga Orchestrator trung tâm điều phối toàn bộ workflow, gửi command cho từng service và xử lý response:

                    ┌──────────────────────┐
                    │  Order Saga          │
                    │  Orchestrator        │
                    │                      │
                    │  State Machine:      │
                    │  CREATED             │
                    │  → PAYMENT_PENDING   │
                    │  → INVENTORY_PENDING │
                    │  → SHIPPING_PENDING  │
                    │  → CONFIRMED         │
                    │  or → COMPENSATING   │
                    │  → CANCELLED         │
                    └────────┬─────────────┘
                             │
            ┌────────────────┼────────────────┐
            │                │                │
     ┌──────▼──────┐  ┌─────▼──────┐  ┌──────▼──────┐
     │  Payment    │  │ Inventory  │  │  Shipping   │
     │  Service    │  │ Service    │  │  Service    │
     └─────────────┘  └────────────┘  └─────────────┘

4.2 State Machine Implementation

public class OrderSaga {

    public enum State {
        CREATED,
        PAYMENT_PENDING,
        PAYMENT_RESERVED,
        INVENTORY_PENDING,
        INVENTORY_RESERVED,
        SHIPPING_PENDING,
        CONFIRMED,
        COMPENSATING_INVENTORY,
        COMPENSATING_PAYMENT,
        CANCELLED
    }

    @Autowired
    private SagaRepository sagaRepository;

    public void start(CreateOrderCommand cmd) {
        SagaState saga = new SagaState(cmd.getOrderId(), State.CREATED);
        sagaRepository.save(saga);
        
        // Step 1: Reserve Payment
        saga.setState(State.PAYMENT_PENDING);
        commandGateway.send(new ReservePaymentCommand(
            cmd.getOrderId(), cmd.getAmount()
        ));
    }

    @SagaEventHandler
    public void on(PaymentReservedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.INVENTORY_PENDING);
        
        // Step 2: Reserve Inventory
        commandGateway.send(new ReserveInventoryCommand(
            event.getOrderId(), saga.getItems()
        ));
    }

    @SagaEventHandler
    public void on(InventoryReservedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.SHIPPING_PENDING);
        
        // Step 3: Schedule Shipping
        commandGateway.send(new ScheduleShippingCommand(
            event.getOrderId(), saga.getAddress()
        ));
    }

    @SagaEventHandler
    public void on(ShipmentScheduledEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.CONFIRMED);
        
        commandGateway.send(new ConfirmOrderCommand(event.getOrderId()));
    }

    // === COMPENSATION ===
    
    @SagaEventHandler
    public void on(InventoryReservationFailedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.COMPENSATING_PAYMENT);
        
        // Compensate: Refund Payment
        commandGateway.send(new RefundPaymentCommand(
            event.getOrderId(), saga.getPaymentId()
        ));
    }

    @SagaEventHandler
    public void on(PaymentRefundedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.CANCELLED);
        
        commandGateway.send(new CancelOrderCommand(
            event.getOrderId(), "Inventory not available"
        ));
    }
}

4.3 Ưu nhược điểm

Ưu điểmNhược điểm
Dễ hiểu workflow (centralized)Orchestrator là single point of failure
Dễ debug và monitorRisk of god class (quá nhiều logic)
Dễ thêm/bớt stepsCoupling giữa orchestrator và services
Compensation logic rõ ràngCần persistent saga state

5. So sánh Choreography vs Orchestration

Tiêu chíChoreographyOrchestration
CouplingRất looseMedium (qua orchestrator)
ComplexityTăng theo số serviceTập trung trong orchestrator
VisibilityKhó nhìn overall flowRõ ràng trong state machine
Failure handlingPhân tánTập trung
TestingKhó (distributed)Dễ hơn (test orchestrator)
ScalabilityTốtTốt (orchestrator stateless)
Khuyến nghị≤ 4 services trong saga> 4 services hoặc complex flow

6. Error Handling & Dead Letter Queue

6.1 Dead Letter Queue (DLQ)

Messages không thể xử lý sau nhiều lần retry được chuyển vào DLQ:

                    ┌─────────────┐
                    │  Main Queue │
                    │ (order.cmds)│
                    └──────┬──────┘
                           │
                    ┌──────▼──────┐
                    │  Consumer   │
                    │  (Service)  │
                    └──────┬──────┘
                           │
                  Success? ─┼─ No (after max retries)
                  │         │
                  ▼         ▼
               ┌─────┐  ┌──────────┐
               │ ACK │  │   DLQ    │
               └─────┘  │(order.   │
                         │ cmds.dlq)│
                         └──────────┘
                              │
                         ┌────▼────┐
                         │ Alert + │
                         │ Manual  │
                         │ Review  │
                         └─────────┘

6.2 Idempotency

Đảm bảo xử lý message nhiều lần cho cùng kết quả:

@Service
public class PaymentService {

    @EventHandler
    public void on(ReservePaymentCommand cmd) {
        // Idempotency check
        String idempotencyKey = "payment:" + cmd.getOrderId();
        if (processedStore.exists(idempotencyKey)) {
            log.info("Already processed payment for order {}", cmd.getOrderId());
            return; // Skip duplicate
        }

        Payment payment = processPayment(cmd);

        // Mark as processed
        processedStore.save(idempotencyKey, payment.getId());
    }
}

6.3 Saga Timeout

public class OrderSaga {

    @SagaTimeout(duration = "5m")
    public void onTimeout(SagaState saga) {
        log.warn("Saga timeout for order {}", saga.getOrderId());
        
        // Compensate based on current state
        switch (saga.getState()) {
            case INVENTORY_PENDING:
                compensatePayment(saga);
                break;
            case SHIPPING_PENDING:
                compensateInventory(saga);
                compensatePayment(saga);
                break;
        }
        
        saga.setState(State.CANCELLED);
        saga.setFailureReason("Saga timeout");
    }
}

7. Best Practices

7.1 Saga Design Guidelines

1. Mỗi step phải có compensating action
2. Compensating actions phải idempotent
3. Sử dụng correlation ID (saga ID) xuyên suốt
4. Persist saga state (survive service restart)
5. Set timeout cho mỗi saga instance
6. Monitor saga metrics (success rate, duration, failure reasons)
7. DLQ cho messages không xử lý được
8. Tránh saga quá nhiều steps (> 7 steps → xem lại design)

7.2 Chọn Choreography hay Orchestration?

Flow đơn giản (2-4 services)?
  └── Choreography

Flow phức tạp (> 4 services, conditional logic)?
  └── Orchestration

Cần visibility cao vào business process?
  └── Orchestration

Muốn minimize coupling?
  └── Choreography

Team experience với event-driven?
  ├── Nhiều → Choreography
  └── Ít → Orchestration

Tóm tắt

  • 2PC không phù hợp cho microservices do tight coupling và lock contention
  • Saga Pattern sử dụng chuỗi local transactions + compensating transactions
  • Choreography: Services giao tiếp qua events, decentralized
  • Orchestration: Saga Orchestrator điều phối centralized qua state machine
  • Compensating transactions phải idempotent và không được fail
  • Sử dụng DLQ, timeout và idempotency cho error handling