Lập trình · 21/09/2026

Saga Pattern cho Backend: Điều phối giao dịch phân tán và hành động bù

Một đơn hàng đi qua dịch vụ thanh toán, tồn kho và vận chuyển không thể được bảo vệ bằng một transaction cơ sở dữ liệu duy nhất. Mỗi dịch vụ sở hữu dữ liệu riêng, có thể phản hồi chậm hoặc ngừng hoạt động tại một thời điểm bất kỳ. Nếu thanh toán đã được giữ tiền nhưng bước giữ hàng thất bại, backend phải biết bước nào đã hoàn tất, việc gì cần bù và làm thế nào để tiếp tục sau khi tiến trình khởi động lại. Saga Pattern cung cấp một mô hình để giải quyết loại workflow này bằng chuỗi local transact

Saga Pattern cho Backend: Điều phối giao dịch phân tán và hành động bù

Một đơn hàng đi qua dịch vụ thanh toán, tồn kho và vận chuyển không thể được bảo vệ bằng một transaction cơ sở dữ liệu duy nhất. Mỗi dịch vụ sở hữu dữ liệu riêng, có thể phản hồi chậm hoặc ngừng hoạt động tại một thời điểm bất kỳ. Nếu thanh toán đã được giữ tiền nhưng bước giữ hàng thất bại, backend phải biết bước nào đã hoàn tất, việc gì cần bù và làm thế nào để tiếp tục sau khi tiến trình khởi động lại. Saga Pattern cung cấp một mô hình để giải quyết loại workflow này bằng chuỗi local transaction cùng các hành động bù có chủ đích.

Bài viết đi từ ranh giới áp dụng đến thiết kế production: phân biệt Saga với transaction ACID và Transactional Outbox, mô hình hóa state machine, chọn choreography hoặc orchestration, định nghĩa compensation theo nghiệp vụ, xử lý message trùng và sai thứ tự, đặt timeout, retry, quan sát và kiểm thử lỗi. Mục tiêu không phải giả lập một transaction toàn cục hoàn hảo, mà là làm cho trạng thái trung gian, thất bại và phục hồi trở nên rõ ràng, có thể kiểm soát.

1. Vì sao transaction cục bộ không đủ?

Trong một monolith dùng chung database, thao tác tạo đơn, trừ tồn kho và ghi thanh toán có thể nằm trong một transaction. Database bảo đảm hoặc tất cả thay đổi commit, hoặc tất cả rollback. Khi các khả năng này được tách thành nhiều dịch vụ và nhiều kho dữ liệu, transaction của dịch vụ đơn hàng không thể rollback một thay đổi đã commit trong dịch vụ thanh toán.

Giữ transaction phân tán kiểu two-phase commit trên toàn bộ dependency thường làm tăng ghép nối, giữ khóa lâu và yêu cầu mọi thành phần hỗ trợ cùng giao thức. Trong nhiều hệ thống microservices và tích hợp bên ngoài, điều đó không khả thi. Saga chọn một hợp đồng khác: mỗi bước commit cục bộ; nếu bước sau thất bại, workflow chạy các thao tác nghiệp vụ để bù cho những bước trước.

Cơ chếPhạm vi bảo đảmĐiểm cần nhớ
Database transactionMột database hoặc resource managerRollback kỹ thuật trước khi commit
Transactional OutboxGhi dữ liệu và ý định phát sự kiện cùng transactionKhông tự điều phối workflow nhiều bước
SagaChuỗi local transaction giữa nhiều participantKhôi phục bằng hành động bù và trạng thái workflow
Saga không tạo ra tính nguyên tử tức thời trên toàn hệ thống. Nó tạo một quy trình nhất quán cuối cùng với đường đi thành công và đường phục hồi được thiết kế trước.

2. Bắt đầu từ invariant và ranh giới nghiệp vụ

Trước khi vẽ chuỗi message, hãy viết rõ invariant. Một đơn chỉ được xác nhận khi đã giữ được thanh toán và hàng; một mã giảm giá không được tiêu thụ hai lần; tiền hoàn không được vượt số tiền đã thu. Sau đó xác định aggregate nào sở hữu mỗi quyết định và thời điểm nào trạng thái trung gian được phép hiển thị cho người dùng.

Không phải mọi luồng qua hai dịch vụ đều cần Saga. Một truy vấn đọc fan-out không phải giao dịch. Một tác vụ có thể đưa hoàn toàn vào một service cũng không nên bị chia nhỏ chỉ để dùng pattern. Saga phù hợp khi workflow có nhiều local transaction, kéo dài hơn một request thông thường, cần tiếp tục qua lỗi tạm thời và có hành động nghiệp vụ để tiến tới hoặc bù lại.

  • Đặt tên rõ trạng thái như PAYMENT_PENDING, STOCK_RESERVED, CONFIRMED, COMPENSATINGCANCELLED.
  • Chỉ ra trạng thái terminal, trạng thái có thể retry và trạng thái cần can thiệp thủ công.
  • Xác định ai sở hữu deadline tổng thể và ai được quyền hủy workflow.
  • Không dùng từ mơ hồ như “đã xử lý” khi từng participant hiểu khác nhau.

3. Thiết kế một Saga đặt hàng

Một Saga đặt hàng có thể gồm: tạo order ở trạng thái chờ, giữ hạn mức thanh toán, giữ tồn kho, tạo yêu cầu giao hàng và xác nhận order. Mỗi bước ghi dữ liệu của participant trong local transaction. Sự kiện hoặc command chỉ nên được phát đáng tin cậy sau commit, thường bằng Transactional Outbox.

START
  -> CreateOrder
  -> AuthorizePayment
  -> ReserveInventory
  -> RequestShipment
  -> ConfirmOrder
  -> COMPLETED

on ReserveInventoryFailed:
  -> ReleasePaymentAuthorization
  -> CancelOrder
  -> COMPENSATED

Đây là sơ đồ nghiệp vụ, không phải cam kết rằng mọi bước xảy ra đúng một lần. Message broker có thể giao lại; consumer có thể commit rồi chết trước khi acknowledge; phản hồi có thể đến sau timeout. Thiết kế phải chịu được at-least-once delivery bằng idempotency và phải coi mỗi transition là một phép so sánh trạng thái có điều kiện.

4. Choreography: participant phản ứng với sự kiện

Với choreography, không có một tiến trình trung tâm ra lệnh từng bước. Order Service phát OrderCreated; Payment Service phản ứng và phát PaymentAuthorized; Inventory Service phản ứng rồi phát kết quả. Cách này giảm phụ thuộc trực tiếp vào một orchestrator và phù hợp với workflow ngắn, tuyến tính, có ít participant.

Nhược điểm xuất hiện khi luồng lớn dần. Logic điều phối bị phân tán trong nhiều consumer, khó nhìn toàn cảnh, vòng lặp sự kiện có thể hình thành và việc đổi thứ tự bước đòi hỏi sửa nhiều service. Để tránh “event soup”, mỗi sự kiện cần diễn tả một sự thật đã xảy ra, có schema và owner rõ ràng. Không dùng event chung chung như ProcessNext để che command dưới tên event.

  • Ưu tiên choreography khi chuỗi ngắn và mỗi phản ứng có ý nghĩa domain tự nhiên.
  • Duy trì correlation ID và saga ID xuyên suốt mọi message.
  • Ghi lại participant nào phát sự kiện và contract version nào được dùng.
  • Đặt giới hạn để một event không vô tình kích hoạt mạng lưới không thể dự đoán.

5. Orchestration: state machine điều khiển workflow

Với orchestration, một Saga Orchestrator lưu trạng thái và gửi command cho participant. Nó nhận reply, kiểm tra transition, quyết định bước kế tiếp hoặc bắt đầu bù. Orchestrator không nên sở hữu logic nội bộ của thanh toán hay tồn kho; nó sở hữu logic tiến trình: bước nào chạy, điều kiện chuyển bước và cách phản ứng khi hết hạn.

Orchestration làm luồng phức tạp dễ quan sát và thay đổi hơn, nhưng tạo một thành phần quan trọng cần độ bền cao. Trạng thái workflow phải được lưu trước hoặc cùng lúc với ý định gửi message, không chỉ nằm trong bộ nhớ. Nhiều instance orchestrator cần cơ chế claim công việc an toàn bằng conditional update, queue partition hoặc optimistic locking.

transition(sagaId, expectedState, reply):
  begin transaction
    saga = load sagaId
    if saga.state != expectedState: return DUPLICATE_OR_STALE
    next = decide(saga, reply)
    update saga set state = next.state, version = version + 1
    insert outbox(next.command)
  commit

Transaction trên bảo đảm thay đổi state và command kế tiếp không bị tách đôi. Outbox relay có thể phát command nhiều lần, vì vậy participant vẫn phải idempotent. Orchestrator cũng lưu lịch sử transition hoặc audit event để đội vận hành hiểu vì sao Saga đi đến trạng thái hiện tại.

6. Compensation không phải rollback kỹ thuật

Sau khi local transaction đã commit, dữ liệu và tác động bên ngoài có thể đã được nhìn thấy. Compensation là một hành động nghiệp vụ mới nhằm trung hòa ảnh hưởng, không phải quay thời gian về trước. Hủy một authorization thanh toán khác với hoàn tiền sau khi capture; giải phóng giữ chỗ khác với tăng tồn kho tùy tiện; gửi email xin lỗi không thể “thu hồi” email xác nhận đã đến hộp thư.

Bước tiếnHành động bù có thể cóRủi ro cần xử lý
Authorize paymentVoid/release authorizationAuthorization có thể đã hết hạn hoặc được capture
Reserve inventoryRelease đúng reservation IDKhông cộng tồn kho hai lần khi message trùng
Create shipmentCancel shipment requestHàng có thể đã bàn giao cho hãng vận chuyển
Apply couponRestore redemption theo policyMã có thể đã hết hạn trong lúc Saga chạy

Mỗi compensation cần idempotency key, điều kiện trạng thái và kết quả rõ: đã bù, không cần bù, tạm lỗi hay không thể bù tự động. Với tác động không đảo ngược, workflow cần pivot point: trước điểm đó còn có thể hủy, sau điểm đó chỉ tiến tiếp hoặc chuyển sang xử lý ngoại lệ bằng con người.

7. Idempotency, thứ tự và message đến muộn

Exactly-once ở cấp nghiệp vụ không thể chỉ dựa vào broker. Participant nên lưu message_id hoặc idempotency_key cùng kết quả xử lý trong chính local transaction. Khi nhận lại message, consumer trả lại kết quả cũ hoặc bỏ qua an toàn. Khóa idempotency cần gắn với thao tác và đối tượng, không chỉ với endpoint.

Message sai thứ tự phải được kiểm tra bằng state và version. Một reply PaymentAuthorized đến sau khi Saga đã hủy không được phép xác nhận đơn. Orchestrator có thể nhận biết reply cũ qua saga_id, step_id, expected state và attempt. Nếu tác động muộn đã xảy ra ở participant, nó có thể cần phát lệnh bù bổ sung thay vì chỉ bỏ qua reply.

if inbox.contains(message.id):
    return inbox.previousResult(message.id)

if order.state != "PAYMENT_PENDING":
    return STALE_TRANSITION

applyPaymentResult()
inbox.record(message.id, result)
outbox.add(nextEvent)
commit()

8. Timeout, retry và deadline tổng thể

Timeout không chứng minh bước đã thất bại; nó chỉ chứng minh caller chưa nhận được kết quả đúng hạn. Vì vậy không nên bù ngay một cách mù quáng. Trước tiên, orchestrator có thể truy vấn trạng thái bằng operation ID hoặc gửi lại command với cùng idempotency key. Participant phải phân biệt chưa thấy yêu cầu, đang xử lý, đã thành công và đã thất bại dứt khoát.

Retry dùng exponential backoff, jitter và giới hạn attempt. Lỗi validation hoặc từ chối nghiệp vụ không nên retry như lỗi mạng. Mỗi bước có timeout riêng, nhưng Saga cũng cần deadline tổng thể để workflow không nằm ở trạng thái pending vô hạn. Khi quá deadline, state machine chuyển sang bù hoặc hàng đợi can thiệp tùy khả năng đảo ngược.

  • Ghi thời điểm next_attempt_at thay vì giữ thread chờ.
  • Không dùng retry để che dependency lỗi kéo dài; kết hợp circuit breaker và giới hạn concurrency.
  • Đưa message poison vào dead-letter queue cùng ngữ cảnh chẩn đoán, không tự động coi DLQ là đã bù.
  • Tách retry của bước tiến và retry của compensation vì mức ưu tiên vận hành khác nhau.

9. Dữ liệu, schema và khả năng tiến hóa

Bản ghi Saga tối thiểu thường có saga_id, loại workflow, business key, state, version, deadline, bước hiện tại, số attempt và timestamps. Payload lớn không nên được sao chép vô hạn qua từng message; lưu tham chiếu ổn định và snapshot dữ liệu thật sự cần cho quyết định hoặc compensation.

Workflow có thể kéo dài qua một lần deploy, nên code mới phải hiểu Saga được tạo bởi phiên bản cũ. Lưu workflow_version, version message schema và giữ handler tương thích trong thời gian các instance cũ còn tồn tại. Không đổi nghĩa của state cũ tại chỗ. Với migration lớn, cho workflow cũ chạy hết hoặc viết bước nâng cấp trạng thái có thể audit.

10. Observability và công cụ vận hành

Dashboard chỉ đếm message thành công là chưa đủ. Cần biết số Saga theo state, tuổi của Saga pending lâu nhất, latency từng bước, tỷ lệ retry, compensation, failure vĩnh viễn và độ trễ outbox. Log có cấu trúc phải mang saga_id, business key, step, attempt, message ID và trace ID; tránh ghi dữ liệu thanh toán hoặc thông tin nhạy cảm.

Đội vận hành cần công cụ xem timeline, retry một bước an toàn, kích hoạt compensation đã kiểm tra hoặc đánh dấu xử lý thủ công. Nút “chạy lại toàn bộ” rất nguy hiểm vì có thể lặp tác động đã thành công. Mọi thao tác quản trị phải có phân quyền, audit log và dùng cùng cơ chế idempotency như luồng tự động.

Một Saga chưa có màn hình trạng thái, cảnh báo stuck workflow và runbook phục hồi vẫn chưa sẵn sàng cho production, dù happy path đã chạy đúng.

11. Kiểm thử failure matrix thay vì chỉ happy path

Test từng participant là cần thiết nhưng chưa đủ. Integration test phải chèn lỗi trước commit, sau commit nhưng trước acknowledge, khi phát outbox, khi reply trùng và khi reply đến muộn. Với mỗi điểm lỗi, kiểm tra state cuối, số tác động nghiệp vụ và khả năng tiếp tục sau khi orchestrator khởi động lại.

Tình huống testKỳ vọng
Command được giao hai lầnMột tác động nghiệp vụ, kết quả ổn định
Participant commit rồi mất phản hồiRetry cùng key không lặp tác động
Bước thứ ba từ chối nghiệp vụBù bước hai và một theo thứ tự hợp lệ
Orchestrator chết giữa transitionKhởi động lại và tiếp tục từ state bền vững
Compensation tạm lỗiRetry độc lập, không báo Saga đã hủy hoàn tất
Reply cũ đến sau khi hủyKhông hồi sinh workflow; bù bổ sung nếu cần

Dùng fake clock để kiểm tra timeout và backoff xác định thay vì sleep ngẫu nhiên. Contract test bảo vệ schema message giữa producer và consumer. Cuối cùng, diễn tập trên môi trường gần production với broker, database và chiến lược partition thật vì nhiều race condition không xuất hiện trong test in-memory.

Checklist triển khai production

  • Invariant, trạng thái và owner của từng quyết định đã rõ.
  • Mỗi bước là local transaction; state và outbox được commit nguyên tử.
  • Command, reply, bước tiến và compensation đều idempotent.
  • Timeout được coi là kết quả chưa biết, không tự động đồng nghĩa thất bại.
  • Compensation phản ánh nghiệp vụ thật và có đường xử lý khi không thể đảo ngược.
  • Saga có deadline, retry budget, dead-letter policy và trạng thái can thiệp thủ công.
  • Message mang saga ID, step ID, schema version và correlation context.
  • Dashboard, cảnh báo stuck workflow, timeline và audit công cụ vận hành đã sẵn sàng.
  • Failure matrix đã kiểm tra duplicate, crash, timeout, sai thứ tự và lỗi khi bù.

Saga Pattern hữu ích khi tính nhất quán của một quy trình phải vượt qua ranh giới dịch vụ mà không có transaction toàn cục. Thiết kế tốt không che giấu trạng thái trung gian: nó mô hình hóa state machine rõ ràng, commit ý định bằng outbox, dùng idempotency để chịu giao lại và coi compensation là nghiệp vụ hạng nhất. Khi deadline, quan sát, công cụ vận hành và kiểm thử lỗi được xây cùng happy path, workflow phân tán mới có thể phục hồi dự đoán được trong production.

Nguồn tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.