Saga Pattern: Quản lý Distributed Transactions trong Microservices

Posted on: 4/21/2026 5:13:44 AM

Hãy tưởng tượng bạn đặt một đơn hàng trên sàn thương mại điện tử: hệ thống cần tạo đơn, trừ tiền tài khoản, giảm tồn kho, và đặt lịch giao hàng — mỗi thao tác nằm ở một microservice khác nhau với database riêng. Nếu bước "trừ tiền" thành công nhưng bước "giảm tồn kho" thất bại vì hết hàng, điều gì xảy ra? Bạn không thể dùng một ROLLBACK duy nhất vì dữ liệu nằm rải rác qua 4 database độc lập. Đây chính là bài toán Distributed Transaction — và Saga Pattern là giải pháp đã được chứng minh hiệu quả trong thực tế production, từ Netflix đến Uber, từ Shopee đến Grab.

Bài viết này đi sâu vào kiến trúc Saga Pattern: từ lý thuyết nền tảng, so sánh hai phương pháp ChoreographyOrchestration, đến triển khai thực chiến với MassTransit trên .NET 10 và Temporal cho durable execution. Tất cả đều hướng đến production-ready, không chỉ demo.

87% Hệ thống microservices cần distributed transactions
2PC ✗ Two-Phase Commit không scale được
3 loại Transaction trong Saga (Compensable, Pivot, Retryable)
<100ms Saga step latency mục tiêu

1. Vấn đề: Tại sao Distributed Transaction khó?

Trong kiến trúc monolith, một business operation thường nằm gọn trong một database transaction duy nhất — đảm bảo ACID (Atomicity, Consistency, Isolation, Durability). Khi chuyển sang microservices, mỗi service sở hữu database riêng (database-per-service pattern), và bạn mất đi sự đảm bảo ACID xuyên suốt.

Two-Phase Commit (2PC) — giải pháp cũ, giới hạn lớn

Phương pháp truyền thống để xử lý distributed transaction là Two-Phase Commit (2PC): một coordinator yêu cầu tất cả participant "chuẩn bị" (phase 1), rồi ra lệnh "commit" hoặc "rollback" (phase 2). Tuy đúng về mặt lý thuyết, 2PC có những hạn chế nghiêm trọng trong môi trường microservices:

Đặc điểmTwo-Phase CommitSaga Pattern
Cơ chếLock tất cả resource cho đến khi commitChuỗi local transactions + compensating
LatencyCao (chờ tất cả participant)Thấp (mỗi step độc lập)
AvailabilityThấp — 1 participant down = block tất cảCao — failure chỉ trigger compensation
ScalabilityKém (lock contention tăng theo participant)Tốt (event-driven, async)
IsolationĐầy đủ (serializable)Không đầy đủ (cần countermeasures)
Phù hợpSingle database clusterMicroservices, cross-service workflows

⚠ Cảnh báo thực tế

Nhiều message broker phổ biến như RabbitMQ, Apache Kafka, hay Azure Service Bus không hỗ trợ 2PC. Nếu hệ thống của bạn giao tiếp qua message queue, 2PC đơn giản là không khả thi — bạn buộc phải dùng Saga hoặc pattern tương đương.

2. Saga Pattern là gì?

Saga Pattern chia một distributed transaction thành chuỗi local transactions — mỗi transaction thực thi trong phạm vi một service duy nhất, cập nhật database của service đó, và phát ra event/message để kích hoạt bước tiếp theo. Nếu một bước thất bại, saga thực thi chuỗi compensating transactions ngược lại để hoàn tác các bước đã hoàn thành.

sequenceDiagram
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service
    participant S as Shipping Service

    O->>O: T1: Tạo đơn (PENDING)
    O->>P: Event: OrderCreated
    P->>P: T2: Trừ tiền
    P->>I: Event: PaymentCompleted
    I->>I: T3: Giảm tồn kho
    I->>S: Event: InventoryReserved
    S->>S: T4: Tạo lịch giao
    S->>O: Event: ShippingScheduled
    O->>O: Cập nhật: CONFIRMED

Hình 1: Saga thành công — chuỗi 4 local transactions hoàn tất tuần tự

Ba loại transaction trong Saga

Không phải mọi bước trong saga đều giống nhau. Microsoft Azure Architecture Center phân loại rõ ràng:

Compensable Có thể hoàn tác bằng compensating transaction. Ví dụ: tạo đơn hàng → huỷ đơn
Pivot Điểm "không thể quay lại" — ranh giới giữa pha compensable và retryable
Retryable Sau pivot, các bước này idempotent và sẽ được retry cho đến khi thành công
graph LR
    T1["T1: Tạo đơn
(Compensable)"] T2["T2: Trừ tiền
(Compensable)"] T3["T3: Xác nhận kho
(Pivot)"] T4["T4: Giao hàng
(Retryable)"] T5["T5: Gửi email
(Retryable)"] T1 --> T2 --> T3 --> T4 --> T5 style T1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style T2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style T3 fill:#e94560,stroke:#fff,color:#fff style T4 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50 style T5 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50

Hình 2: Phân loại transaction — Compensable (hồng) → Pivot (đỏ) → Retryable (xanh)

3. Choreography vs Orchestration

Có hai phương pháp chính để triển khai Saga — mỗi phương pháp phù hợp với mức độ phức tạp khác nhau của hệ thống.

3.1 Choreography: Phi tập trung, event-driven

Trong mô hình Choreography, không có coordinator trung tâm. Mỗi service lắng nghe event từ service trước, thực hiện logic nghiệp vụ, rồi phát event tiếp theo. Giống như một nhóm nhạc jazz — mỗi nhạc sĩ tự nghe và phối hợp mà không cần nhạc trưởng.

graph LR
    OS["Order Service"]
    PS["Payment Service"]
    IS["Inventory Service"]
    SS["Shipping Service"]
    EB["Event Bus"]

    OS -->|OrderCreated| EB
    EB -->|OrderCreated| PS
    PS -->|PaymentCompleted| EB
    EB -->|PaymentCompleted| IS
    IS -->|InventoryReserved| EB
    EB -->|InventoryReserved| SS
    SS -->|ShippingScheduled| EB
    EB -->|ShippingScheduled| OS

    style OS fill:#e94560,stroke:#fff,color:#fff
    style PS fill:#2c3e50,stroke:#fff,color:#fff
    style IS fill:#2c3e50,stroke:#fff,color:#fff
    style SS fill:#2c3e50,stroke:#fff,color:#fff
    style EB fill:#f8f9fa,stroke:#e94560,color:#e94560

Hình 3: Choreography — services giao tiếp qua event bus, không có coordinator

Khi failure xảy ra, service phát ra event compensating và các service trước đó phải lắng nghe để tự hoàn tác:

sequenceDiagram
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service

    O->>P: Event: OrderCreated
    P->>P: T2: Trừ tiền ✓
    P->>I: Event: PaymentCompleted
    I->>I: T3: Giảm tồn kho ✗ (hết hàng!)
    I->>P: Event: InventoryFailed
    P->>P: C2: Hoàn tiền
    P->>O: Event: PaymentRefunded
    O->>O: C1: Huỷ đơn hàng

Hình 4: Compensation flow trong Choreography — mỗi service tự xử lý rollback

3.2 Orchestration: Tập trung, rõ ràng

Trong mô hình Orchestration, một Saga Orchestrator (còn gọi là Saga Manager) điều phối toàn bộ luồng. Nó biết thứ tự các bước, gửi command đến từng service, nhận kết quả, và quyết định bước tiếp theo hoặc trigger compensation. Giống như nhạc trưởng dàn nhạc giao hưởng.

graph TD
    ORCH["Saga Orchestrator
(State Machine)"] OS["Order Service"] PS["Payment Service"] IS["Inventory Service"] SS["Shipping Service"] ORCH -->|"1. CreateOrder"| OS OS -->|"OrderCreated"| ORCH ORCH -->|"2. ProcessPayment"| PS PS -->|"PaymentCompleted"| ORCH ORCH -->|"3. ReserveInventory"| IS IS -->|"InventoryReserved"| ORCH ORCH -->|"4. ScheduleShipping"| SS SS -->|"ShippingScheduled"| ORCH style ORCH fill:#e94560,stroke:#fff,color:#fff style OS fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50 style PS fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50 style IS fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50 style SS fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50

Hình 5: Orchestration — Saga Orchestrator điều phối toàn bộ luồng qua state machine

3.3 So sánh chi tiết

Tiêu chíChoreographyOrchestration
Điều phốiPhi tập trung — mỗi service tự biết bước tiếpTập trung — orchestrator quản lý toàn bộ flow
CouplingLoose coupling qua eventsService chỉ biết orchestrator, không biết nhau
VisibilityKhó trace — logic phân tán qua nhiều serviceDễ trace — state machine lưu trạng thái rõ ràng
Cyclic DependencyCó rủi ro — services subscribe event của nhauKhông — flow một chiều qua orchestrator
Single Point of FailureKhôngCó (orchestrator) — cần HA
Phù hợp2-4 services, workflow đơn giản5+ services, workflow phức tạp, cần audit
TestingKhó — cần chạy tất cả servicesDễ hơn — mock participant, test orchestrator
Thêm bước mớiPhức tạp — thay đổi event chainĐơn giản — thêm step vào state machine

💡 Quy tắc thực tế

Nếu bạn vẽ flow trên giấy mà cần hơn 4 mũi tên, hoặc có điều kiện rẽ nhánh phức tạp — hãy chọn Orchestration. Nếu flow đơn giản, tuyến tính, và bạn muốn tối đa decoupling — Choreography là đủ. Phần lớn hệ thống production lớn chọn Orchestration vì dễ debug và maintain hơn nhiều.

4. Compensating Transactions: Nghệ thuật hoàn tác

Compensating transaction không đơn giản là "undo" — nó là một business operation ngược để đưa hệ thống về trạng thái nhất quán. Quan trọng: compensating transaction tạo ra một trạng thái mới tương đương về mặt nghiệp vụ, không phải khôi phục chính xác trạng thái cũ.

Ví dụ cụ thể

Forward TransactionCompensating TransactionLưu ý
Tạo đơn hàng (status: PENDING)Cập nhật status → CANCELLEDKhông xoá row — giữ audit trail
Trừ tiền tài khoảnHoàn tiền + ghi transaction logCần ghi rõ reason: "Saga compensation"
Giảm tồn kho (stock -= quantity)Tăng tồn kho (stock += quantity)Phải kiểm tra concurrent modification
Gửi email xác nhậnKhông thể hoàn tác!Đây là lý do email nên là retryable step cuối cùng
Charge credit card qua gatewayGọi Refund APIMột số gateway cần 24h để process refund

⚠ Nguyên tắc vàng

Đặt các bước không thể hoàn tác (gửi email, gọi API bên thứ 3 không có undo, gửi SMS) ở cuối saga — sau pivot transaction. Như vậy nếu cần compensate, những bước này chưa thực thi nên không cần undo.

5. Data Anomalies và cách phòng tránh

Saga Pattern không cung cấp isolation level như database transaction. Điều này dẫn đến các data anomaly mà bạn cần chủ động xử lý:

Các loại anomaly thường gặp

AnomalyMô tảVí dụ
Lost UpdatesSaga A ghi đè kết quả của Saga B mà không biết2 đơn hàng cùng giảm tồn kho, chỉ 1 lần giảm được ghi nhận
Dirty ReadsSaga B đọc dữ liệu đang được Saga A sửa (chưa commit hoặc sẽ bị compensate)Service đọc stock đã giảm nhưng sau đó đơn bị huỷ — stock compensation chưa chạy
Fuzzy ReadsCùng một saga, 2 lần đọc cùng dữ liệu cho kết quả khác nhauBước 1 đọc giá sản phẩm = 100k, bước 3 đọc lại = 120k (bị cập nhật giữa chừng)

Countermeasures

Microsoft Azure Architecture Center đề xuất 6 chiến lược phòng chống:

Semantic Lock Đánh dấu record đang "in-progress" bằng flag ở application level
Commutative Update Thiết kế update có thể áp dụng theo bất kỳ thứ tự nào
Pessimistic View Sắp xếp saga để update xảy ra trong retryable steps
Reread Value Đọc lại dữ liệu trước khi update để phát hiện thay đổi

Ví dụ Semantic Lock trong thực tế — thay vì để Order ở trạng thái "Created", đánh dấu "PENDING_PAYMENT" để các service khác biết record đang trong saga:

public enum OrderStatus
{
    PendingPayment,   // Semantic lock — saga đang xử lý
    PendingInventory, // Semantic lock — chờ xác nhận kho
    Confirmed,        // Saga hoàn tất thành công
    Cancelled,        // Saga bị compensate
    Failed            // Saga thất bại không thể compensate
}

6. Triển khai với MassTransit trên .NET 10

MassTransit là thư viện message bus phổ biến nhất cho .NET, cung cấp Saga State Machine — cách triển khai orchestration saga đẹp và mạnh mẽ nhất trong hệ sinh thái .NET. State machine approach cho phép bạn định nghĩa saga dưới dạng finite state machine với trạng thái, event, và transition rõ ràng.

Cấu trúc dự án

src/
├── OrderSaga/
│   ├── OrderSagaState.cs         // State entity
│   ├── OrderSagaStateMachine.cs  // State machine definition
│   └── Events/
│       ├── OrderCreated.cs
│       ├── PaymentCompleted.cs
│       ├── PaymentFailed.cs
│       ├── InventoryReserved.cs
│       └── InventoryFailed.cs
├── OrderService/
├── PaymentService/
└── InventoryService/

Định nghĩa Saga State

public class OrderSagaState : SagaStateMachineInstance, ISagaVersion
{
    public Guid CorrelationId { get; set; }
    public int Version { get; set; }
    public string CurrentState { get; set; } = default!;

    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public decimal TotalAmount { get; set; }
    public int ItemCount { get; set; }

    public DateTime CreatedAt { get; set; }
    public DateTime? PaymentCompletedAt { get; set; }
    public DateTime? InventoryReservedAt { get; set; }
    public string? FailureReason { get; set; }
}

Định nghĩa State Machine

public class OrderSagaStateMachine : MassTransitStateMachine<OrderSagaState>
{
    public State AwaitingPayment { get; private set; } = default!;
    public State AwaitingInventory { get; private set; } = default!;
    public State Completed { get; private set; } = default!;
    public State Compensating { get; private set; } = default!;
    public State Failed { get; private set; } = default!;

    public Event<OrderCreated> OrderCreated { get; private set; } = default!;
    public Event<PaymentCompleted> PaymentCompleted { get; private set; } = default!;
    public Event<PaymentFailed> PaymentFailed { get; private set; } = default!;
    public Event<InventoryReserved> InventoryReserved { get; private set; } = default!;
    public Event<InventoryFailed> InventoryFailed { get; private set; } = default!;

    public OrderSagaStateMachine()
    {
        InstanceState(x => x.CurrentState);

        Event(() => OrderCreated,
            x => x.CorrelateById(ctx => ctx.Message.OrderId));
        Event(() => PaymentCompleted,
            x => x.CorrelateById(ctx => ctx.Message.OrderId));
        Event(() => PaymentFailed,
            x => x.CorrelateById(ctx => ctx.Message.OrderId));
        Event(() => InventoryReserved,
            x => x.CorrelateById(ctx => ctx.Message.OrderId));
        Event(() => InventoryFailed,
            x => x.CorrelateById(ctx => ctx.Message.OrderId));

        Initially(
            When(OrderCreated)
                .Then(ctx =>
                {
                    ctx.Saga.OrderId = ctx.Message.OrderId;
                    ctx.Saga.CustomerId = ctx.Message.CustomerId;
                    ctx.Saga.TotalAmount = ctx.Message.TotalAmount;
                    ctx.Saga.CreatedAt = DateTime.UtcNow;
                })
                .Publish(ctx => new ProcessPayment(
                    ctx.Saga.OrderId,
                    ctx.Saga.CustomerId,
                    ctx.Saga.TotalAmount))
                .TransitionTo(AwaitingPayment)
        );

        During(AwaitingPayment,
            When(PaymentCompleted)
                .Then(ctx =>
                    ctx.Saga.PaymentCompletedAt = DateTime.UtcNow)
                .Publish(ctx => new ReserveInventory(
                    ctx.Saga.OrderId,
                    ctx.Saga.ItemCount))
                .TransitionTo(AwaitingInventory),

            When(PaymentFailed)
                .Then(ctx =>
                    ctx.Saga.FailureReason = ctx.Message.Reason)
                .Publish(ctx => new CancelOrder(ctx.Saga.OrderId))
                .TransitionTo(Failed)
        );

        During(AwaitingInventory,
            When(InventoryReserved)
                .Then(ctx =>
                    ctx.Saga.InventoryReservedAt = DateTime.UtcNow)
                .Publish(ctx => new ConfirmOrder(ctx.Saga.OrderId))
                .TransitionTo(Completed),

            When(InventoryFailed)
                .Then(ctx =>
                    ctx.Saga.FailureReason = ctx.Message.Reason)
                .Publish(ctx => new RefundPayment(
                    ctx.Saga.OrderId,
                    ctx.Saga.TotalAmount))
                .Publish(ctx => new CancelOrder(ctx.Saga.OrderId))
                .TransitionTo(Compensating)
        );
    }
}
stateDiagram-v2
    [*] --> AwaitingPayment : OrderCreated / ProcessPayment
    AwaitingPayment --> AwaitingInventory : PaymentCompleted / ReserveInventory
    AwaitingPayment --> Failed : PaymentFailed / CancelOrder
    AwaitingInventory --> Completed : InventoryReserved / ConfirmOrder
    AwaitingInventory --> Compensating : InventoryFailed / RefundPayment + CancelOrder
    Compensating --> Failed : CompensationCompleted

Hình 6: State diagram của OrderSaga — MassTransit State Machine

Đăng ký trong Program.cs

builder.Services.AddMassTransit(x =>
{
    x.AddSagaStateMachine<OrderSagaStateMachine, OrderSagaState>()
        .EntityFrameworkRepository(r =>
        {
            r.ConcurrencyMode = ConcurrencyMode.Optimistic;
            r.AddDbContext<DbContext, OrderSagaDbContext>((provider, optionsBuilder) =>
            {
                optionsBuilder.UseSqlServer(
                    builder.Configuration.GetConnectionString("SagaDb"));
            });
        });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.ConfigureEndpoints(context);
    });
});

💡 Tại sao dùng Optimistic Concurrency?

MassTransit saga state được lưu trong database (EF Core, MongoDB, Redis...). Khi nhiều event đến cùng lúc cho cùng saga instance, Optimistic Concurrency dùng version column để phát hiện conflict — event bị conflict sẽ được retry tự động. Đây là cách an toàn nhất để xử lý concurrent saga updates mà không cần distributed lock.

7. Temporal: Durable Execution cho Saga

Nếu MassTransit là cách "truyền thống" để làm Saga với state machine + message broker, thì Temporal đại diện cho paradigm mới: Durable Execution. Thay vì bạn phải tự quản lý state, event, retry — Temporal đảm bảo code của bạn chạy đến hoàn tất, bất kể crash, network failure, hay deployment giữa chừng.

[Workflow]
public class OrderSagaWorkflow
{
    [WorkflowRun]
    public async Task<OrderResult> RunAsync(OrderRequest request)
    {
        var orderId = Workflow.NewGuid();

        // Step 1: Tạo đơn
        await Workflow.ExecuteActivityAsync(
            (OrderActivities a) => a.CreateOrderAsync(orderId, request),
            new() { StartToCloseTimeout = TimeSpan.FromSeconds(30) });

        try
        {
            // Step 2: Trừ tiền
            await Workflow.ExecuteActivityAsync(
                (PaymentActivities a) => a.ProcessPaymentAsync(
                    orderId, request.Amount),
                new()
                {
                    StartToCloseTimeout = TimeSpan.FromSeconds(30),
                    RetryPolicy = new()
                    {
                        MaximumAttempts = 3,
                        InitialInterval = TimeSpan.FromSeconds(1),
                        BackoffCoefficient = 2.0
                    }
                });

            // Step 3: Giảm tồn kho
            await Workflow.ExecuteActivityAsync(
                (InventoryActivities a) => a.ReserveInventoryAsync(
                    orderId, request.Items),
                new() { StartToCloseTimeout = TimeSpan.FromSeconds(30) });

            // Step 4: Xác nhận đơn
            await Workflow.ExecuteActivityAsync(
                (OrderActivities a) => a.ConfirmOrderAsync(orderId),
                new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });

            return new OrderResult(orderId, OrderStatus.Confirmed);
        }
        catch (ActivityFailureException ex)
        {
            // Compensation: hoàn tác theo thứ tự ngược
            await Workflow.ExecuteActivityAsync(
                (PaymentActivities a) => a.RefundPaymentAsync(
                    orderId, request.Amount),
                new() { StartToCloseTimeout = TimeSpan.FromSeconds(30) });

            await Workflow.ExecuteActivityAsync(
                (OrderActivities a) => a.CancelOrderAsync(
                    orderId, ex.Message),
                new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });

            return new OrderResult(orderId, OrderStatus.Cancelled);
        }
    }
}

MassTransit vs Temporal

Tiêu chíMassTransit SagaTemporal Workflow
Mô hình lập trìnhState machine declarativeImperative code (async/await)
PersistenceEF Core, MongoDB, RedisTemporal Server (PostgreSQL/MySQL/Cassandra)
Retry & TimeoutTự cấu hình qua middlewareBuilt-in, cấu hình per-activity
VisibilityCần tự build dashboardTemporal Web UI built-in
InfrastructureCần message broker (RabbitMQ/Kafka)Cần Temporal Server cluster
Learning curveVừa phải (quen .NET ecosystem)Cao hơn (paradigm mới)
Long-running workflowsHỗ trợ nhưng cần quản lý timeoutXuất sắc — workflow chạy hàng ngày/tuần
Phù hợpTeam .NET thuần, đã có message brokerWorkflow phức tạp, cần audit trail chi tiết

8. Idempotency: Yêu cầu bắt buộc

Trong distributed system, message có thể bị gửi lại (at-least-once delivery). Nếu saga handler không idempotent, việc xử lý lại cùng một event sẽ gây ra side effect sai — ví dụ trừ tiền 2 lần cho cùng một đơn hàng. Idempotency không phải nice-to-have, mà là bắt buộc.

Các kỹ thuật đảm bảo idempotency

1. Idempotency Key: Mỗi message mang theo một unique key. Handler kiểm tra key trước khi xử lý:

public async Task Handle(ProcessPayment message)
{
    var idempotencyKey = $"payment:{message.OrderId}";

    var alreadyProcessed = await _db.ProcessedMessages
        .AnyAsync(m => m.Key == idempotencyKey);

    if (alreadyProcessed)
        return; // Đã xử lý rồi, skip

    await _paymentGateway.ChargeAsync(
        message.CustomerId, message.Amount);

    _db.ProcessedMessages.Add(new ProcessedMessage
    {
        Key = idempotencyKey,
        ProcessedAt = DateTime.UtcNow
    });

    await _db.SaveChangesAsync();
}

2. Conditional Update (Optimistic): Dùng WHERE clause để chỉ update khi trạng thái đúng:

UPDATE Orders
SET Status = 'Confirmed', Version = Version + 1
WHERE Id = @OrderId AND Status = 'PendingInventory' AND Version = @ExpectedVersion

3. Outbox Pattern: Ghi message vào database trong cùng transaction với business data, rồi publish sau. Điều này đảm bảo "exactly-once" semantic khi kết hợp với idempotent consumer:

sequenceDiagram
    participant S as Service
    participant DB as Database
    participant OB as Outbox Publisher
    participant MB as Message Broker

    S->>DB: BEGIN TRANSACTION
    S->>DB: UPDATE business data
    S->>DB: INSERT INTO Outbox (message)
    S->>DB: COMMIT

    OB->>DB: Poll Outbox table
    OB->>MB: Publish message
    OB->>DB: Mark as published

Hình 7: Outbox Pattern — đảm bảo atomicity giữa business data và message publishing

MassTransit Outbox tích hợp sẵn

MassTransit cung cấp Transactional Outbox tích hợp với EF Core. Chỉ cần bật cfg.AddEntityFrameworkOutbox<OrderDbContext>() — message sẽ tự động được ghi vào outbox table trong cùng transaction và publish bởi background worker. Không cần tự implement.

9. Monitoring và Observability cho Saga

Saga trải rộng qua nhiều service và có thể chạy trong vài giây đến vài ngày. Thiếu observability nghĩa là bạn mù khi production gặp sự cố.

Metrics cần theo dõi

MetricMô tảAlert threshold gợi ý
saga_started_totalSố saga được khởi tạoSpike đột ngột > 3x baseline
saga_completed_totalSố saga hoàn tất thành côngDrop > 20% so với started
saga_compensated_totalSố saga bị compensateRate > 5% trong 15 phút
saga_duration_secondsThời gian chạy saga (histogram)P99 > 30s
saga_step_failures_totalSố lần step thất bại (trước retry)Sustained rate > 10/min
saga_stuck_countSaga ở cùng trạng thái quá lâuBất kỳ saga nào stuck > 10 phút

Distributed Tracing

Propagate Correlation ID (chính là saga instance ID) qua tất cả message và HTTP call. Với OpenTelemetry, bạn có thể trace toàn bộ saga flow từ đầu đến cuối:

// MassTransit tự động propagate CorrelationId qua message header
// Kết hợp với OpenTelemetry:
services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("MassTransit")
        .AddAspNetCoreInstrumentation()
        .AddSqlClientInstrumentation()
        .AddOtlpExporter());

10. Khi nào nên và không nên dùng Saga

Nên dùng khi:

  • Business process trải qua nhiều service có database riêng
  • Cần đảm bảo eventual consistency xuyên suốt các service
  • Workflow có compensating logic rõ ràng cho mỗi bước
  • Hệ thống cần scale independent — mỗi service scale riêng
  • Các bước có thể chạy async — không yêu cầu response tức thì

Không nên dùng khi:

  • Tất cả data nằm trong một database duy nhất — dùng database transaction thông thường
  • Yêu cầu strong consistency (serializable isolation) — Saga chỉ cung cấp eventual consistency
  • Compensating transaction không khả thi về mặt nghiệp vụ cho nhiều bước
  • Workflow quá đơn giản (2 service, không có branching) — overhead saga không đáng
graph TD
    Q1{"Dữ liệu nằm trong
1 database?"} Q2{"Cần strong
consistency?"} Q3{"Workflow > 4 steps
hoặc có branching?"} A1["Dùng DB Transaction"] A2["Cân nhắc 2PC
hoặc thiết kế lại"] A3["Saga Choreography"] A4["Saga Orchestration"] Q1 -->|Có| A1 Q1 -->|Không| Q2 Q2 -->|Có| A2 Q2 -->|Không| Q3 Q3 -->|Không| A3 Q3 -->|Có| A4 style Q1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style Q2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style Q3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style A1 fill:#4CAF50,stroke:#fff,color:#fff style A2 fill:#ff9800,stroke:#fff,color:#fff style A3 fill:#2c3e50,stroke:#fff,color:#fff style A4 fill:#e94560,stroke:#fff,color:#fff

Hình 8: Decision tree — chọn phương pháp xử lý distributed transaction phù hợp

11. Kết luận

Saga Pattern không phải silver bullet — nó đánh đổi isolation để lấy availabilityscalability. Nhưng trong thế giới microservices nơi mỗi service cần sống độc lập, đây là pattern thực chiến nhất để duy trì data consistency xuyên suốt hệ thống.

Những điểm then chốt cần nhớ:

  • Choreography cho workflow đơn giản, Orchestration cho workflow phức tạp — phần lớn production chọn orchestration
  • Đặt bước không thể hoàn tác sau pivot transaction
  • Idempotency là bắt buộc — dùng idempotency key + conditional update + outbox pattern
  • Đầu tư vào monitoring: metrics cho saga lifecycle, distributed tracing, alert cho stuck sagas
  • MassTransit là lựa chọn tuyệt vời cho team .NET, Temporal cho workflow cực phức tạp

💡 Lời khuyên thực tế

Bắt đầu đơn giản. Không phải mọi cross-service operation đều cần saga. Trước khi implement saga, hãy tự hỏi: "Có thể thiết kế lại để operation nằm gọn trong một service không?" Đôi khi, đúng service boundary sẽ loại bỏ nhu cầu distributed transaction hoàn toàn.

Nguồn tham khảo: