21 KiB
21 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| saga-pattern | SAGA Pattern - quản lý distributed transactions qua chuỗi local transactions với compensating actions. Use for checkout flows, order processing, và multi-service orchestration. | .NET 10+, MassTransit, RabbitMQ, EF Core |
|
SAGA Pattern / Mẫu SAGA
Pattern quản lý distributed transactions trong microservices bằng chuỗi local transactions.
When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Multi-service transactions required / Cần transactions đa dịch vụ
- Distributed transaction (2PC) not feasible / 2PC không khả thi
- Long-running business processes / Quy trình nghiệp vụ chạy lâu
- Need compensating transactions / Cần transactions bù trừ
Core Concepts / Khái Niệm Cốt Lõi
Why SAGA? / Tại Sao Dùng SAGA?
┌─────────────────────────────────────────────────────────────┐
│ ❌ DISTRIBUTED TRANSACTION (2PC) - PROBLEMS │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │Order │────▶│Payment│────▶│Inventory│ │
│ │ DB │ │ DB │ │ DB │ │
│ └───────┘ └───────┘ └───────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ Transaction Coordinator │
│ │
│ Problems: │
│ • Locks resources across services │
│ • Single point of failure │
│ • Poor performance │
│ • Not always supported (NoSQL, external APIs) │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ✅ SAGA - LOCAL TRANSACTIONS │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1 Step 2 Step 3 │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │Create │─────▶│Reserve│─────▶│Charge │ │
│ │Order │ │Stock │ │Payment│ │
│ └───────┘ └───────┘ └───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [Local Tx] [Local Tx] [Local Tx] │
│ │
│ If Step 3 fails: │
│ ┌───────┐ ┌───────┐ │
│ │Release│◀─────│Cancel │ │
│ │Stock │ │Order │ │
│ └───────┘ └───────┘ │
│ Compensate Compensate │
│ │
└─────────────────────────────────────────────────────────────┘
Choreography vs Orchestration
┌─────────────────────────────────────────────────────────────┐
│ CHOREOGRAPHY │
├─────────────────────────────────────────────────────────────┤
│ │
│ Order Event Bus Inventory Payment │
│ │ │ │ │ │
│ │──Create──▶│ │ │ │
│ │ │──OrderCreated─▶│ │ │
│ │ │ │──Reserve──▶│ │
│ │ │ │ │──Charge────▶ │
│ │ │◀─────PaymentProcessed──────│ │
│ │
│ ✅ Loose coupling, simple services │
│ ❌ Hard to track flow, complex rollback │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ORCHESTRATION │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ SAGA │ │
│ │ Orchestrator│ │
│ └─────┬───────┘ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Order │ │Inventory│ │ Payment │ │
│ │ Service │ │ Service │ │ Service │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ✅ Clear flow, centralized error handling │
│ ❌ More coupling, orchestrator complexity │
│ │
└─────────────────────────────────────────────────────────────┘
SAGA Components / Thành Phần SAGA
| Component | Purpose | Example |
|---|---|---|
| Saga State Machine | Tracks saga state | OrderSaga |
| Saga Step | Individual transaction | ReserveInventory |
| Compensating Action | Undo step on failure | ReleaseInventory |
| Saga Data | Shared state across steps | OrderId, Items |
Key Patterns / Mẫu Chính
Saga State Machine with MassTransit
/// <summary>
/// EN: Order saga state tracking.
/// VI: Theo dõi state của Order saga.
/// </summary>
public class OrderSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = default!;
// EN: Saga data / VI: Dữ liệu saga
public Guid OrderId { get; set; }
public string UserId { get; set; } = default!;
public decimal TotalAmount { get; set; }
public string? PaymentId { get; set; }
public string? FailureReason { get; set; }
}
/// <summary>
/// EN: Order saga state machine definition.
/// VI: Định nghĩa state machine cho Order saga.
/// </summary>
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
// EN: States / VI: Các trạng thái
public State OrderCreated { get; private set; } = null!;
public State InventoryReserved { get; private set; } = null!;
public State PaymentProcessed { get; private set; } = null!;
public State Completed { get; private set; } = null!;
public State Failed { get; private set; } = null!;
// EN: Events / VI: Các sự kiện
public Event<OrderSubmitted> OrderSubmittedEvent { get; private set; } = null!;
public Event<InventoryReserved> InventoryReservedEvent { get; private set; } = null!;
public Event<InventoryReservationFailed> InventoryFailedEvent { get; private set; } = null!;
public Event<PaymentProcessed> PaymentProcessedEvent { get; private set; } = null!;
public Event<PaymentFailed> PaymentFailedEvent { get; private set; } = null!;
public OrderSaga()
{
InstanceState(x => x.CurrentState);
// EN: Define events / VI: Định nghĩa events
Event(() => OrderSubmittedEvent, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => InventoryReservedEvent, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => InventoryFailedEvent, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentProcessedEvent, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentFailedEvent, x => x.CorrelateById(m => m.Message.OrderId));
// EN: Define state transitions / VI: Định nghĩa chuyển trạng thái
Initially(
When(OrderSubmittedEvent)
.Then(context =>
{
context.Saga.OrderId = context.Message.OrderId;
context.Saga.UserId = context.Message.UserId;
context.Saga.TotalAmount = context.Message.TotalAmount;
})
.Publish(context => new ReserveInventory
{
OrderId = context.Saga.OrderId,
Items = context.Message.Items
})
.TransitionTo(OrderCreated)
);
During(OrderCreated,
When(InventoryReservedEvent)
.Publish(context => new ProcessPayment
{
OrderId = context.Saga.OrderId,
UserId = context.Saga.UserId,
Amount = context.Saga.TotalAmount
})
.TransitionTo(InventoryReserved),
When(InventoryFailedEvent)
.Then(context => context.Saga.FailureReason = context.Message.Reason)
.Publish(context => new CancelOrder
{
OrderId = context.Saga.OrderId,
Reason = context.Message.Reason
})
.TransitionTo(Failed)
);
During(InventoryReserved,
When(PaymentProcessedEvent)
.Then(context => context.Saga.PaymentId = context.Message.PaymentId)
.Publish(context => new CompleteOrder
{
OrderId = context.Saga.OrderId,
PaymentId = context.Message.PaymentId
})
.TransitionTo(Completed)
.Finalize(),
When(PaymentFailedEvent)
.Then(context => context.Saga.FailureReason = context.Message.Reason)
// EN: Compensating action - release inventory
// VI: Action bù trừ - giải phóng inventory
.Publish(context => new ReleaseInventory
{
OrderId = context.Saga.OrderId
})
.Publish(context => new CancelOrder
{
OrderId = context.Saga.OrderId,
Reason = context.Message.Reason
})
.TransitionTo(Failed)
);
SetCompletedWhenFinalized();
}
}
Saga Events / Events của SAGA
/// <summary>
/// EN: Events for order saga.
/// VI: Events cho order saga.
/// </summary>
// EN: Trigger events / VI: Events kích hoạt
public record OrderSubmitted
{
public Guid OrderId { get; init; }
public string UserId { get; init; } = default!;
public decimal TotalAmount { get; init; }
public List<OrderItemInfo> Items { get; init; } = new();
}
// EN: Command messages / VI: Command messages
public record ReserveInventory
{
public Guid OrderId { get; init; }
public List<OrderItemInfo> Items { get; init; } = new();
}
public record ProcessPayment
{
public Guid OrderId { get; init; }
public string UserId { get; init; } = default!;
public decimal Amount { get; init; }
}
public record ReleaseInventory // Compensating
{
public Guid OrderId { get; init; }
}
public record CancelOrder // Compensating
{
public Guid OrderId { get; init; }
public string Reason { get; init; } = default!;
}
public record CompleteOrder
{
public Guid OrderId { get; init; }
public string PaymentId { get; init; } = default!;
}
// EN: Response events / VI: Events phản hồi
public record InventoryReserved
{
public Guid OrderId { get; init; }
}
public record InventoryReservationFailed
{
public Guid OrderId { get; init; }
public string Reason { get; init; } = default!;
}
public record PaymentProcessed
{
public Guid OrderId { get; init; }
public string PaymentId { get; init; } = default!;
}
public record PaymentFailed
{
public Guid OrderId { get; init; }
public string Reason { get; init; } = default!;
}
Saga Consumer / Consumer của SAGA
/// <summary>
/// EN: Inventory service consumer for saga.
/// VI: Consumer của Inventory service cho saga.
/// </summary>
public class ReserveInventoryConsumer : IConsumer<ReserveInventory>
{
private readonly IInventoryRepository _repository;
private readonly ILogger<ReserveInventoryConsumer> _logger;
public ReserveInventoryConsumer(
IInventoryRepository repository,
ILogger<ReserveInventoryConsumer> logger)
{
_repository = repository;
_logger = logger;
}
public async Task Consume(ConsumeContext<ReserveInventory> context)
{
var message = context.Message;
try
{
foreach (var item in message.Items)
{
var success = await _repository.ReserveAsync(
item.ProductId,
item.Quantity,
message.OrderId,
context.CancellationToken);
if (!success)
{
// EN: Not enough stock, publish failure
// VI: Không đủ hàng, publish failure
await context.Publish(new InventoryReservationFailed
{
OrderId = message.OrderId,
Reason = $"Insufficient stock for product {item.ProductId}"
});
return;
}
}
// EN: All items reserved successfully
// VI: Tất cả items đã được reserve thành công
await context.Publish(new InventoryReserved
{
OrderId = message.OrderId
});
_logger.LogInformation("Inventory reserved for order {OrderId}", message.OrderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to reserve inventory for order {OrderId}", message.OrderId);
await context.Publish(new InventoryReservationFailed
{
OrderId = message.OrderId,
Reason = ex.Message
});
}
}
}
/// <summary>
/// EN: Compensating consumer to release inventory.
/// VI: Consumer bù trừ để giải phóng inventory.
/// </summary>
public class ReleaseInventoryConsumer : IConsumer<ReleaseInventory>
{
private readonly IInventoryRepository _repository;
private readonly ILogger<ReleaseInventoryConsumer> _logger;
public ReleaseInventoryConsumer(
IInventoryRepository repository,
ILogger<ReleaseInventoryConsumer> logger)
{
_repository = repository;
_logger = logger;
}
public async Task Consume(ConsumeContext<ReleaseInventory> context)
{
await _repository.ReleaseReservationAsync(
context.Message.OrderId,
context.CancellationToken);
_logger.LogInformation(
"Released inventory reservation for order {OrderId}",
context.Message.OrderId);
}
}
Common Mistakes / Lỗi Thường Gặp
1. Missing Compensating Actions
// ❌ BAD: No compensation handling
During(InventoryReserved,
When(PaymentFailedEvent)
.TransitionTo(Failed) // Inventory still reserved!
);
// ✅ GOOD: Proper compensation
During(InventoryReserved,
When(PaymentFailedEvent)
.Publish(context => new ReleaseInventory
{
OrderId = context.Saga.OrderId
})
.TransitionTo(Failed)
);
2. Non-Idempotent Compensations
// ❌ BAD: Can fail if called twice
public async Task Consume(ConsumeContext<ReleaseInventory> context)
{
var reservation = await _repo.GetReservationAsync(context.Message.OrderId);
await _repo.DeleteAsync(reservation); // Throws if already deleted
}
// ✅ GOOD: Idempotent compensation
public async Task Consume(ConsumeContext<ReleaseInventory> context)
{
var reservation = await _repo.GetReservationAsync(context.Message.OrderId);
if (reservation != null)
{
await _repo.DeleteAsync(reservation);
}
// Safe to call multiple times
}
3. No Timeout Handling
// ❌ BAD: Saga can hang forever
public OrderSaga()
{
During(OrderCreated,
When(InventoryReservedEvent)
.TransitionTo(InventoryReserved)
);
// What if InventoryReservedEvent never arrives?
}
// ✅ GOOD: Add timeout handling
public OrderSaga()
{
Schedule(() => ReservationTimeout, x => x.ReservationTimeoutToken,
s => s.Delay = TimeSpan.FromMinutes(5));
During(OrderCreated,
When(InventoryReservedEvent)
.Unschedule(ReservationTimeout)
.TransitionTo(InventoryReserved),
When(ReservationTimeout.Received)
.Then(ctx => ctx.Saga.FailureReason = "Reservation timeout")
.Publish(ctx => new CancelOrder { OrderId = ctx.Saga.OrderId })
.TransitionTo(Failed)
);
}
Quick Reference / Tham Chiếu Nhanh
Choreography vs Orchestration
| Aspect | Choreography | Orchestration |
|---|---|---|
| Coupling | Low | Higher |
| Complexity | Distributed | Centralized |
| Debugging | Hard | Easier |
| Single point of failure | No | Yes (orchestrator) |
| Best for | Simple flows | Complex flows |
SAGA Design Checklist
| Step | Required |
|---|---|
| Define all saga steps | ✅ |
| Define compensating action for each step | ✅ |
| Make compensations idempotent | ✅ |
| Add timeout handling | ✅ |
| Log state transitions | ✅ |
| Design for partial failures | ✅ |
Resources / Tài Nguyên
- Detailed Examples - Full code examples
- Outbox Pattern - Reliable event publishing
- Inter-service Communication - MassTransit
- Event Sourcing - Event-based persistence
- Error Handling - Resilience patterns