SAGA Pattern: Giải quyết bài toán giao dịch phân tán (Distributed Transactions)
7/19/2026 · 11p đọc
title: "SAGA Pattern: Giải quyết bài toán giao dịch phân tán (Distributed Transactions)"
series: "Software Architecture Masterclass"
part: "Phần 2 — Cấu trúc & Patterns"
order: 19
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["saga-pattern", "distributed-transactions", "microservices", "compensating-transaction", "orchestration", "choreography", "eventual-consistency"]
SAGA Pattern: Giải quyết bài toán giao dịch phân tán (Distributed Transactions)
Một khách đặt hàng trên hệ thống của bạn. Đơn hàng được tạo ở service Order. Thanh toán được trừ ở service Payment. Tồn kho được giữ chỗ ở service Inventory. Ba service, ba database riêng biệt, ba đội phát triển khác nhau. Mọi thứ chạy êm cho đến ngày Inventory hết hàng đúng lúc Payment đã trừ tiền xong.
Trong một monolith với một database duy nhất, đây là bài toán đã giải xong từ ba mươi năm trước: mở một transaction, BEGIN, chạy ba câu lệnh, nếu bước cuối lỗi thì ROLLBACK, toàn bộ biến mất như chưa từng xảy ra. ACID (Atomicity, Consistency, Isolation, Durability — bốn tính chất transaction cổ điển) lo hết. Nhưng khi ba bước đó nằm ở ba service với ba database khác nhau, không có ROLLBACK nào chạy xuyên qua ranh giới mạng và tiến trình được. Bạn không còn transaction — bạn có ba transaction cục bộ độc lập, và câu hỏi đau đầu là: ai đảm bảo cả ba cùng thành công hoặc cùng thất bại?
Đây không phải là câu hỏi lý thuyết. Đây là bài toán mà bất kỳ ai tách monolith thành microservices sẽ đụng phải ngay ở nghiệp vụ đầu tiên có ý nghĩa — một quy trình nghiệp vụ trải dài qua nhiều bounded context. Và câu trả lời không đẹp như ACID. Nó tên là SAGA.
The Problem
Xét lại ví dụ đặt hàng, chi tiết hóa thành các bước:
- Order Service: tạo đơn hàng, trạng thái
PENDING. - Payment Service: trừ tiền từ tài khoản/thẻ khách hàng.
- Inventory Service: giữ chỗ (reserve) số lượng hàng trong kho.
- Shipping Service: tạo lệnh vận chuyển.
Đây là bốn transaction cục bộ, mỗi transaction ACID trong phạm vi database riêng của nó. Vấn đề nảy sinh ở tính nhất quán giữa các bước:
- Nếu bước 3 (Inventory) thất bại vì hết hàng, thì bước 2 (Payment) đã trừ tiền phải được hoàn lại. Nhưng Payment Service không biết gì về việc Inventory vừa thất bại — nó là service độc lập, không có cơ chế "nhìn thấy" transaction đang treo ở nơi khác.
- Two-Phase Commit (2PC — giao thức đồng thuận hai pha giữa các resource manager) từng là câu trả lời cổ điển cho bài toán này trong hệ thống database phân tán. Nhưng 2PC đòi hỏi một transaction coordinator giữ khóa (lock) trên tất cả các resource tham gia cho đến khi mọi bên xác nhận "commit" — nghĩa là Payment Service phải khóa bản ghi tài khoản của khách trong lúc chờ Inventory và Shipping trả lời. Trong một hệ thống có độ trễ mạng, service có thể chậm hoặc down bất cứ lúc nào, khóa đó có thể treo hàng giây, hàng phút — hoặc mãi mãi nếu coordinator chết giữa chừng.
- Ngoài ra, 2PC yêu cầu tất cả participant hỗ trợ cùng một giao thức coordinator (thường là XA transaction) — điều gần như không tưởng khi bạn có Payment chạy trên PostgreSQL, Inventory chạy trên MongoDB, và một dịch vụ bên thứ ba (cổng thanh toán) hoàn toàn không expose transaction API nào cả.
Nói cách khác: 2PC lý thuyết đúng nhưng thực chiến không khả thi ở quy mô microservices, vì nó đánh đổi throughput và availability lấy consistency mạnh — đúng chiều ngược với thứ hệ thống phân tán cần để scale.
Vậy câu hỏi kiến trúc thật sự là: nếu không khóa được xuyên service, làm sao đảm bảo một chuỗi nghiệp vụ nhiều bước hoặc hoàn thành trọn vẹn, hoặc để lại hệ thống ở một trạng thái nhất quán có thể giải thích được?
The Theory
SAGA là câu trả lời: thay vì một transaction phân tán duy nhất, chia nghiệp vụ thành một chuỗi các transaction cục bộ (local transaction), mỗi transaction chỉ chạm vào một service/database. Mỗi bước trong chuỗi có một hành động bù trừ (compensating action) tương ứng — một transaction cục bộ khác, được thiết kế để đảo ngược tác động nghiệp vụ của bước gốc, nếu một bước sau đó trong chuỗi thất bại.
Không có ROLLBACK kỹ thuật ở đây. Compensating action không "chưa từng xảy ra" — nó là một hành động nghiệp vụ mới, chạy sau, để đưa hệ thống về một trạng thái tương đương (không nhất thiết giống hệt) trạng thái trước khi saga bắt đầu. Ví dụ: ReserveInventory bị bù bằng ReleaseInventory; ChargePayment bị bù bằng RefundPayment. Đây là lý do SAGA là mô hình eventual consistency (nhất quán cuối cùng) chứ không phải ACID — có một khoảng thời gian giữa lúc Payment đã trừ tiền và lúc Refund hoàn tất, hệ thống ở trạng thái trung gian mà bất kỳ ai đọc dữ liệu lúc đó đều thấy sự không nhất quán tạm thời.
Có hai kiểu phối hợp saga:
Choreography (biên đạo phi tập trung): mỗi service lắng nghe event từ service trước, thực hiện transaction cục bộ của mình, rồi phát ra event tiếp theo. Không có ai "chỉ huy" — luồng nghiệp vụ nổi lên (emergent) từ việc từng service phản ứng với event của nhau. Order Service tạo đơn → phát OrderCreated → Payment Service nghe, trừ tiền → phát PaymentCompleted → Inventory Service nghe, giữ chỗ → phát InventoryReserved... Nếu một bước thất bại, service đó phát event XxxFailed, và các service trước đó phải tự lắng nghe event thất bại để tự chạy compensating action của mình.
Orchestration (điều phối tập trung): có một Saga Orchestrator — một thành phần trung tâm biết toàn bộ định nghĩa của saga (thứ tự bước, ai gọi ai, compensating action nào ứng với bước nào). Orchestrator gọi lệnh (command) trực tiếp tới từng service, chờ phản hồi, và quyết định bước tiếp theo là tiến (proceed) hay lùi (compensate). Service không cần biết gì về saga hay các service khác — chúng chỉ nhận lệnh và trả kết quả.
Về bản chất, đây là sự đánh đổi kinh điển giữa coupling phi tập trung với logic ẩn (choreography) và coupling tập trung với logic tường minh (orchestration) — cùng một họ với đánh đổi giữa event-driven thuần túy và request-response có kiểm soát.
The Trade-off
SAGA so với 2PC/distributed transaction:
- Được: không khóa tài nguyên xuyên service, mỗi transaction cục bộ commit ngay và độc lập, hệ thống không bị nghẽn chờ coordinator, các service có thể dùng công nghệ lưu trữ khác nhau tùy ý (polyglot persistence), và một service down tạm thời không treo toàn bộ chuỗi — nó chỉ làm saga đó chậm hoặc phải compensate.
- Mất: không còn Isolation theo nghĩa ACID. Trong lúc saga đang chạy, dữ liệu ở trạng thái trung gian là quan sát được bởi các luồng đọc khác — khách hàng có thể thấy đơn hàng ở trạng thái "đã thanh toán" trong vài giây trước khi biết là hàng hết và tiền sắp được hoàn. Bạn phải thiết kế UI/UX và các luồng đọc dữ liệu chấp nhận được sự "nhìn thấy" trạng thái trung gian này — đây là chi phí thật, không phải chi tiết vặt.
Choreography so với Orchestration:
- Choreography được gì: không có single point of failure ở tầng điều phối, các service độc lập tuyệt đối, dễ thêm participant mới (chỉ cần subscribe thêm event) mà không đụng vào code của các service khác.
- Choreography mất gì: luồng nghiệp vụ biến mất khỏi bất kỳ một chỗ nào — nó chỉ tồn tại như là hệ quả của việc đọc code (hoặc log) của N service khác nhau và ráp lại trong đầu. Khi saga có 5-6 bước, debug một giao dịch bị kẹt giữa chừng nghĩa là lần theo dấu vết event qua nhiều service, nhiều log, không có nơi nào trả lời câu hỏi "saga #12345 đang ở bước nào". Rủi ro cyclic dependency giữa các event handler (A phát event khiến B phản ứng, B lại phát event khiến A phản ứng) cũng tăng theo số bước.
- Orchestration được gì: toàn bộ định nghĩa quy trình nằm ở MỘT chỗ — orchestrator — nên trace được trạng thái saga bất kỳ lúc nào ("đang ở bước 3/5, chờ Inventory phản hồi"), dễ viết test end-to-end cho toàn bộ luồng, dễ thêm logic retry/timeout tập trung, và khi cần thay đổi thứ tự bước nghiệp vụ, bạn chỉ sửa một chỗ.
- Orchestration mất gì: orchestrator trở thành điểm coupling tập trung — nó phải biết về mọi service tham gia, nghĩa là mỗi khi thêm một bước nghiệp vụ mới, bạn phải sửa orchestrator (không chỉ thêm service mới độc lập). Orchestrator cũng là một service phải được vận hành, scale, và giám sát riêng — nếu chọn tự viết thay vì dùng nền tảng workflow có sẵn, bạn đang tự gánh thêm một hệ thống trạng thái phân tán (state machine) cần persist đúng, đúng lúc down cũng phải resume lại chính xác từ nơi đã dừng.
Trade-off gốc rễ về compensating action: không phải mọi hành động đều đảo ngược được sạch sẽ. RefundPayment bù cho ChargePayment khá rõ ràng. Nhưng SendConfirmationEmail thì không có "Unsend" — email đã ra khỏi hộp thư của bạn, khách đã đọc. Với các hành động không thể đảo ngược, bạn buộc phải chọn một trong ba đường: (1) đẩy hành động đó xuống cuối saga, sau khi mọi bước có thể thất bại đã hoàn tất (giảm rủi ro nhưng không loại bỏ); (2) chấp nhận gửi email bù đắp (compensating email) nói "đơn hàng đã bị hủy, xin lỗi vì sự bất tiện" — bù bằng thông tin thay vì bù bằng hành động ngược; (3) tách hẳn hành động không đảo ngược được ra khỏi saga giao dịch, biến nó thành một side-effect bất đồng bộ chạy sau khi saga đã chắc chắn thành công. Đây chính là lý do phần lớn effort thiết kế SAGA không nằm ở "happy path" mà nằm ở việc liệt kê và thiết kế compensating action cho từng bước — và với một số bước, câu trả lời trung thực là "không có compensating action tốt, đừng đưa bước này vào saga".
The Architecture Diagram
Sơ đồ dưới minh họa saga đặt hàng theo mô hình Orchestration, gồm luồng thành công và luồng thất bại (kích hoạt chuỗi compensating action ngược lại):
sequenceDiagram
participant C as Client
participant O as Saga Orchestrator
participant OS as Order Service
participant PS as Payment Service
participant IS as Inventory Service
participant SS as Shipping Service
C->>O: Bắt đầu Saga: PlaceOrder
O->>OS: CreateOrder (PENDING)
OS-->>O: OrderCreated (OK)
O->>PS: ChargePayment
PS-->>O: PaymentCompleted (OK)
O->>IS: ReserveInventory
IS-->>O: InventoryReservationFailed (Hết hàng)
Note over O: Bước 3 thất bại.<br/>Bắt đầu chuỗi Compensating Action ngược lại
O->>PS: RefundPayment (compensate bước 2)
PS-->>O: PaymentRefunded (OK)
O->>OS: CancelOrder (compensate bước 1)
OS-->>O: OrderCancelled (OK)
O-->>C: Saga thất bại: Đơn hàng đã hủy, tiền đã hoàn
Note over SS: Shipping Service không bao giờ<br/>được gọi vì saga dừng ở bước 3
Implementation Guardrails
Mọi bước saga PHẢI có compensating action được định nghĩa và review trước khi viết dòng code đầu tiên cho happy path. Lập bảng ánh xạ "bước thuận → bước bù" cho toàn bộ saga ngay ở giai đoạn thiết kế. Nếu một bước không có compensating action khả thi (như gửi email, gọi webhook bên thứ ba không hỗ trợ hủy), đó là tín hiệu kiến trúc: bước đó không nên nằm trong saga giao dịch — hãy đẩy nó ra ngoài, chạy sau khi saga đã kết thúc thành công.
Dùng Orchestration, không phải choreography thuần, cho bất kỳ saga nào có từ 3 bước trở lên. Choreography chỉ nên dùng cho chuỗi 2 bước đơn giản, ít khả năng mở rộng. Với saga phức tạp hơn, chi phí mất khả năng truy vết trạng thái (traceability) của choreography lớn hơn nhiều so với chi phí coupling tập trung của orchestrator — và bạn sẽ cần trace được "saga này đang kẹt ở đâu" trong sản xuất, không có ngoại lệ.
Mỗi bước saga phải idempotent (chạy lại nhiều lần cho cùng kết quả) và mỗi compensating action cũng vậy. Mạng không đáng tin cậy — orchestrator có thể gửi lại lệnh do timeout dù service đích đã xử lý xong. Dùng idempotency key gắn theo saga instance ID cho từng lệnh.
Persist trạng thái saga (saga state) bền vững, không giữ trong memory. Orchestrator phải ghi trạng thái hiện tại (bước nào đã chạy, đã compensate hay chưa) vào một store bền vững sau MỖI bước, để khi orchestrator restart hoặc crash giữa chừng, nó resume đúng từ điểm dừng thay vì để saga treo lơ lửng vô thời hạn.
Architect's Verdict
SAGA không phải là một pattern hay ho để chọn — nó là cái giá bắt buộc phải trả một khi bạn đã quyết định tách một quy trình nghiệp vụ xuyên qua nhiều service với database riêng. Nếu nhóm của bạn không thể liệt kê rõ ràng, sạch sẽ compensating action cho từng bước của một quy trình, đó là dấu hiệu cảnh báo kiến trúc rõ ràng hơn bất kỳ smell nào khác: có thể các bước đó thuộc về cùng một bounded context và không nên bị tách service ngay từ đầu. Đừng chọn SAGA vì nó "đúng chuẩn microservices" — chọn nó khi ranh giới service đã đúng và cái giá của eventual consistency đã được nghiệp vụ chấp nhận công khai, không phải chấp nhận ngầm.
🔗 Bài viết liên quan: CQRS (Command Query Responsibility Segregation) · Event-Driven Architecture (EDA)
Bài trước: CQRS (Command Query Responsibility Segregation) · Bài tiếp theo: API Gateway & Service Mesh