Pattern saga: transaction phân tán không cần 2PC
Cách phối hợp workflow nghiệp vụ nhiều bước xuyên service trong .NET. Choreography vs orchestration, MassTransit saga, và compensation event hoàn tác.
Mục lục
Bài toán reliability khó nhất trong microservice là giữ data nhất quán xuyên service mà không có distributed transaction. Saga là câu trả lời ngành công nghiệp rút gọn thành - chuỗi transaction local cộng bước undo bù trừ. Chương này cho thấy triển khai saga trong .NET với MassTransit, khi nào chọn choreography vs orchestration, và failure mode quyết định mỗi cái.
Khi nào saga thành không tránh khỏi?
Ba tín hiệu.
Workflow vượt ranh giới service. Đặt hàng chạm Inventory, Payment, Shipping - mỗi bên sở hữu database. Không bọc được vào một transaction; 2PC dễ vỡ vận hành và gần như không managed service nào hỗ trợ.
Lỗi cục bộ không có thể chấp nhận. Trừ thẻ mà không giữ inventory là email refund-and-apologise; giữ inventory mà không trừ thẻ là mất doanh thu. Hệ phải kết thúc ở "tất cả xong" hoặc "tất cả hoàn tác".
Bước chậm hoặc bất đồng bộ. Gửi yêu cầu fulfilment đến kho third-party mất nhiều giờ; transaction đồng bộ thậm chí không khả thi. Transaction theo bước của saga gỡ ghép thời gian.
Nếu workflow của bạn nằm trọn trong một database, dùng một transaction EF Core và bỏ chương này.
Số nào và trade-off nào nên ngân sách?
Thuộc tính Choreography Orchestration
Coupling lỏng (event) chặt hơn (coordinator)
Hiển thị trải qua các service một log
Thêm bước đổi >= 2 service đổi orchestrator
Phức tạp debug cao (mạng event) thấp (state machine)
Tự trị team cao trung bình
Phần lớn team .NET nên bắt đầu orchestration. "Một state machine đọc được" thắng "mạng event phi tập trung" cho hệ thống vừa. Lên cấp choreography khi team service đủ lớn để phối hợp đổi orchestrator thành điểm nghẽn.
Hình dạng saga trông thế nào?
Orchestration với coordinator rõ ràng:
sequenceDiagram
participant C as Client
participant S as OrderSaga
participant I as Inventory
participant P as Payment
participant Sh as Shipping
C->>S: PlaceOrder
S->>I: ReserveInventory
I-->>S: Reserved
S->>P: ChargeCard
P-->>S: Charged
S->>Sh: Ship
Sh-->>S: Shipped
S-->>C: OrderCompleted
Nếu ChargeCard fail, saga phát ReleaseInventory để bù. Nếu
Ship fail, phát cả RefundCard và ReleaseInventory.
Orchestrator sở hữu luật.
Cấu hình .NET 10 với MassTransit Saga State Machine?
public class OrderSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = "";
public Guid OrderId { get; set; }
public Guid UserId { get; set; }
public decimal Amount { get; set; }
public Guid? ReservationId { get; set; }
public Guid? ChargeId { get; set; }
}
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
public State Reserving { get; private set; } = null!;
public State Charging { get; private set; } = null!;
public State Shipping { get; private set; } = null!;
public State Completed { get; private set; } = null!;
public State Failed { get; private set; } = null!;
public Event<PlaceOrder> Started { get; private set; } = null!;
public Event<InventoryReserved> InventoryReserved { get; private set; } = null!;
public Event<InventoryReservationFailed> InventoryFailed { get; private set; } = null!;
public Event<PaymentCharged> PaymentCharged { get; private set; } = null!;
public Event<PaymentChargeFailed> PaymentFailed { get; private set; } = null!;
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Initially(
When(Started)
.Then(ctx => { ctx.Saga.OrderId = ctx.Message.OrderId;
ctx.Saga.Amount = ctx.Message.Amount; })
.Publish(ctx => new ReserveInventory(ctx.Saga.OrderId, ctx.Message.Items))
.TransitionTo(Reserving));
During(Reserving,
When(InventoryReserved)
.Then(ctx => ctx.Saga.ReservationId = ctx.Message.ReservationId)
.Publish(ctx => new ChargeCard(ctx.Saga.OrderId, ctx.Saga.Amount))
.TransitionTo(Charging),
When(InventoryFailed)
.TransitionTo(Failed));
During(Charging,
When(PaymentCharged)
.Publish(ctx => new ShipOrder(ctx.Saga.OrderId))
.TransitionTo(Shipping),
When(PaymentFailed)
.Publish(ctx => new ReleaseInventory(ctx.Saga.ReservationId!.Value)) // bù
.TransitionTo(Failed));
// ... state Shipping, Completed lược
}
}
Ba chi tiết. State machine là tài liệu của workflow; engineer mới đọc và hiểu hệ. Persistence vào EF Core là đăng ký một dòng; khi crash, orchestrator resume từ state cuối đã persist. Compensation chỉ là publish event khác - cùng hạ tầng MassTransit xử lý nó.
Saga tạo failure mode nào?
- Saga kẹt - một bước fail liên tục, compensation fail, saga
ngồi ở state trung gian. Phòng: alert trên
saga_age_seconds1 giờ; thủ tục phục hồi tay.
- Vòng lặp compensation - compensation chính nó fail, kích bù thêm. Phòng: cap retry trên compensation; nếu fail, gọi người.
- Event không thứ tự - InventoryReserved đến sau khi saga đã bỏ. Phòng: state machine bỏ qua event không hợp lệ ở state hiện tại; handler downstream idempotent.
- State saga drift - DB state và các service tham gia không khớp. Phòng: job đối soát đêm so state saga với record service downstream.
Chương 13 emit event chuyển state saga thành trace OpenTelemetry - cả workflow là một Jaeger span.
Khi nào saga là quá liều?
Ba mùi.
Một: một write database giả dạng workflow. Nếu "bước" đều nằm trong một DbContext, transaction là hình đúng, không phải saga.
Hai: workflow đồng bộ thời gian chặt. Nếu user chờ kết quả và mỗi bước phải xong trong mili giây, eventual consistency của saga là mô hình sai. Chuỗi gọi trực tiếp (với handler resilience chương 11) đơn giản hơn.
Ba: khi compensation bất khả thi. "Gửi thông báo" không có nghịch đảo - không un-send được. Nếu workflow chứa bước không thể bù, sắp xếp lại để chúng chạy cuối (không bước sau nào fail) hoặc chấp nhận chúng có thể chạy trong state lỗi.
Đi tiếp đâu từ đây?
Bạn đã hoàn tất nhóm reliability. Chương kế tiếp: Observability OpenTelemetry trong .NET - metric, trace, log cho phép thấy saga, breaker, outbox của bạn có thực sự chạy trên production không.