Saga Pattern: Quản lý Distributed Transactions trong Microservices
Posted on: 4/21/2026 5:13:44 AM
Table of contents
- 1. Vấn đề: Tại sao Distributed Transaction khó?
- 2. Saga Pattern là gì?
- 3. Choreography vs Orchestration
- 4. Compensating Transactions: Nghệ thuật hoàn tác
- 5. Data Anomalies và cách phòng tránh
- 6. Triển khai với MassTransit trên .NET 10
- 7. Temporal: Durable Execution cho Saga
- 8. Idempotency: Yêu cầu bắt buộc
- 9. Monitoring và Observability cho Saga
- 10. Khi nào nên và không nên dùng Saga
- 11. Kết luận
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 Choreography và Orchestration, đế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.
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ểm | Two-Phase Commit | Saga Pattern |
|---|---|---|
| Cơ chế | Lock tất cả resource cho đến khi commit | Chuỗi local transactions + compensating |
| Latency | Cao (chờ tất cả participant) | Thấp (mỗi step độc lập) |
| Availability | Thấp — 1 participant down = block tất cả | Cao — failure chỉ trigger compensation |
| Scalability | Kém (lock contention tăng theo participant) | Tốt (event-driven, async) |
| Isolation | Đầy đủ (serializable) | Không đầy đủ (cần countermeasures) |
| Phù hợp | Single database cluster | Microservices, 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:
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í | Choreography | Orchestration |
|---|---|---|
| Điều phối | Phi tập trung — mỗi service tự biết bước tiếp | Tập trung — orchestrator quản lý toàn bộ flow |
| Coupling | Loose coupling qua events | Service chỉ biết orchestrator, không biết nhau |
| Visibility | Khó trace — logic phân tán qua nhiều service | Dễ trace — state machine lưu trạng thái rõ ràng |
| Cyclic Dependency | Có rủi ro — services subscribe event của nhau | Không — flow một chiều qua orchestrator |
| Single Point of Failure | Không | Có (orchestrator) — cần HA |
| Phù hợp | 2-4 services, workflow đơn giản | 5+ services, workflow phức tạp, cần audit |
| Testing | Khó — cần chạy tất cả services | Dễ hơn — mock participant, test orchestrator |
| Thêm bước mới | Phứ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 Transaction | Compensating Transaction | Lưu ý |
|---|---|---|
| Tạo đơn hàng (status: PENDING) | Cập nhật status → CANCELLED | Không xoá row — giữ audit trail |
| Trừ tiền tài khoản | Hoàn tiền + ghi transaction log | Cầ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ận | Không thể hoàn tác! | Đây là lý do email nên là retryable step cuối cùng |
| Charge credit card qua gateway | Gọi Refund API | Mộ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
| Anomaly | Mô tả | Ví dụ |
|---|---|---|
| Lost Updates | Saga A ghi đè kết quả của Saga B mà không biết | 2 đơn hàng cùng giảm tồn kho, chỉ 1 lần giảm được ghi nhận |
| Dirty Reads | Saga 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 Reads | Cùng một saga, 2 lần đọc cùng dữ liệu cho kết quả khác nhau | Bướ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:
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 Saga | Temporal Workflow |
|---|---|---|
| Mô hình lập trình | State machine declarative | Imperative code (async/await) |
| Persistence | EF Core, MongoDB, Redis | Temporal Server (PostgreSQL/MySQL/Cassandra) |
| Retry & Timeout | Tự cấu hình qua middleware | Built-in, cấu hình per-activity |
| Visibility | Cần tự build dashboard | Temporal Web UI built-in |
| Infrastructure | Cần message broker (RabbitMQ/Kafka) | Cần Temporal Server cluster |
| Learning curve | Vừa phải (quen .NET ecosystem) | Cao hơn (paradigm mới) |
| Long-running workflows | Hỗ trợ nhưng cần quản lý timeout | Xuất sắc — workflow chạy hàng ngày/tuần |
| Phù hợp | Team .NET thuần, đã có message broker | Workflow 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
| Metric | Mô tả | Alert threshold gợi ý |
|---|---|---|
saga_started_total | Số saga được khởi tạo | Spike đột ngột > 3x baseline |
saga_completed_total | Số saga hoàn tất thành công | Drop > 20% so với started |
saga_compensated_total | Số saga bị compensate | Rate > 5% trong 15 phút |
saga_duration_seconds | Thời gian chạy saga (histogram) | P99 > 30s |
saga_step_failures_total | Số lần step thất bại (trước retry) | Sustained rate > 10/min |
saga_stuck_count | Saga ở cùng trạng thái quá lâu | Bấ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 availability và scalability. 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:
Monorepo 2026: Turborepo, Nx và pnpm Workspaces — Quản lý code cho team quy mô lớn
ClickHouse: Cỗ máy phân tích real-time xử lý hàng tỷ rows trong mili-giây
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.