Mẫu Saga (Saga Pattern) và giao dịch phân tán
1. Tổng quan
A. Định nghĩa
Mẫu Saga (Saga Pattern) là phương thức xử lý giao dịch phân tán chia một giao dịch nghiệp vụ trải trên nhiều dịch vụ thành chuỗi các giao dịch cục bộ của từng dịch vụ để thực thi, và khi phát sinh thất bại giữa chừng thì hủy các bước đã thành công theo thứ tự ngược bằng giao dịch bù (Compensating Transaction), nhằm duy trì tính nhất quán của toàn hệ thống.
Saga là khái niệm do Hector Garcia-Molina đề xuất năm 1987 nhằm giảm nhẹ vấn đề chiếm giữ khóa của giao dịch chạy dài (long-lived transaction), và ngày nay được chú ý trở lại như mẫu tiêu biểu để bảo đảm nhất quán dữ liệu vượt qua ranh giới dịch vụ trong kiến trúc microservices (MSA). Cốt lõi của saga là thay thế tính nguyên tử (All-or-Nothing) mà giao dịch truyền thống bảo đảm bằng logic bù ở mức ứng dụng thay vì khóa cơ sở dữ liệu.
B. Bối cảnh ra đời và sự cần thiết
Trong hệ thống nguyên khối, một cơ sở dữ liệu sở hữu mọi bảng, nên có thể gộp cập nhật đơn hàng, thanh toán, tồn kho vào một giao dịch duy nhất để xử lý nguyên tử bằng một lần COMMIT và hoàn lại bằng ROLLBACK khi thất bại. Nhưng trong MSA, do tuân theo nguyên tắc DB per Service - mỗi dịch vụ sở hữu cơ sở dữ liệu độc lập - ranh giới giao dịch đơn lẻ bị chia nhỏ ra nhiều dịch vụ và nhiều DB vật lý. Khi đó, để xử lý nguyên tử một nghiệp vụ (ví dụ: đặt tour du lịch = vé máy bay + khách sạn + thuê xe) thành công hoặc thất bại, cần sự phối hợp giữa các dịch vụ.
Giải pháp chuẩn trước đây là cam kết hai pha (2PC, Two-Phase Commit) - kỹ thuật nhất quán mạnh trong đó bộ điều phối (Coordinator) chỉ thị chuẩn bị (prepare) và cam kết (commit) cho mọi bên tham gia - nhưng giữ khóa tài nguyên cho đến khi commit xong nên làm giảm mạnh tính sẵn sàng và khả năng mở rộng. Đặc biệt khi có nhiều bên tham gia hoặc lẫn dịch vụ phản hồi chậm, toàn bộ phải chờ, và khi bộ điều phối gặp sự cố thì rơi vào trạng thái chặn (blocking). Đây là lựa chọn hy sinh tính sẵn sàng (A) trong định lý CAP, không phù hợp với yêu cầu của các dịch vụ Internet quy mô lớn.
Saga giải quyết vấn đề này từ góc nhìn khác. Thay vì gộp toàn bộ vào một vùng khóa, nó xử lý mỗi bước bằng giao dịch cục bộ ngắn được commit ngay và hiệu chỉnh thất bại sau đó bằng bù. Kết quả là thời gian chiếm giữ khóa ngắn nên thông lượng và tính sẵn sàng cao, nhưng đổi lại phải chấp nhận nhất quán cuối cùng (Eventual Consistency), và phát sinh bài toán thiết kế mới là trạng thái trung gian có thể lộ ra bên ngoài. Tức là saga là sản phẩm của đánh đổi "đạt được tính sẵn sàng và tự chủ thay cho nhất quán mạnh".
2. Nguyên lý cơ bản và cấu trúc tổng thể của Saga
Saga gồm các cặp giao dịch xuôi (T1, T2, … Tn) tạo thành luồng bình thường và giao dịch bù (C1, C2, … Cn-1) hoàn lại từng giao dịch xuôi. Nếu mọi bước thành công bình thường thì T1→Tn được thực thi tuần tự, còn nếu thất bại ở bước k thì bù ngược các bước T1…Tk-1 đã thành công theo thứ tự Ck-1…C1.
flowchart LR
subgraph FORWARD["Thực thi xuôi (đường thành công)"]
T1["T1: Tạo đơn hàng"] --> T2["T2: Phê duyệt thanh toán"] --> T3["T3: Trừ tồn kho"] --> T4["T4: Lệnh giao hàng"]
end
T3 -. "Phát sinh thất bại" .-> F["Bắt đầu bù"]
subgraph COMP["Thực thi bù (rollback ngược)"]
C2["C2: Hủy thanh toán"] --> C1["C1: Hủy đơn hàng"]
end
F --> C2
Trong hình trên, nếu trừ tồn kho (T3) thất bại, hệ thống bù phê duyệt thanh toán và tạo đơn hàng đã thành công trước đó lần lượt bằng hủy thanh toán (C2), hủy đơn hàng (C1). Điểm quan trọng ở đây là bù không phải ROLLBACK vật lý mà là hủy về mặt ngữ nghĩa (semantic undo). Chẳng hạn, thanh toán đã được phê duyệt không thể hoàn lại nên được bù trừ bằng một giao dịch xuôi mới là "hoàn tiền". Do đó giao dịch bù phải được thiết kế như logic nghiệp vụ để lại dấu vết của giao dịch gốc nhưng vô hiệu hóa tác dụng của nó.
Khái niệm thường xuất hiện trong thiết kế bù là bước không thể hoàn lại (pivot transaction). Ví dụ, các bước có phí hủy lớn hoặc khó hoàn lại về mặt vật lý như xuất vé máy bay được đặt ở nửa sau của saga, tức là sau khi mọi bước trước đã thành công, để tối thiểu hóa chi phí bù. Như vậy, trong thiết kế saga, bản thân thứ tự các bước trở thành phương tiện quản lý rủi ro.
3. Phương thức thực thi — Choreography và Orchestration
Tùy theo ai điều phối việc chuyển bước của saga mà chia thành hai phương thức hiện thực. Lựa chọn này ảnh hưởng trực tiếp tới mức liên kết, khả năng quan sát, độ phức tạp nên là quyết định quan trọng nhất của thiết kế saga.
Phương thức Choreography (biên đạo) không có bộ điều phối trung tâm; mỗi dịch vụ phát sự kiện và dịch vụ khác đăng ký sự kiện đó để tự thực hiện bước tiếp theo của mình. Khi dịch vụ đơn hàng phát sự kiện OrderCreated, dịch vụ thanh toán nhận và thanh toán rồi phát PaymentCompleted, dịch vụ tồn kho lại đăng ký sự kiện này, và luồng cứ thế tiếp diễn. Không có phụ thuộc trực tiếp giữa các dịch vụ nên liên kết thấp, tự chủ cao, nhưng toàn bộ luồng bị rải rác trong chuỗi sự kiện nên khó nắm bắt trong một cái nhìn và có rủi ro phụ thuộc vòng hoặc bùng nổ sự kiện.
flowchart TB
subgraph Choreo["Choreography (dựa trên đăng ký sự kiện)"]
OS["Dịch vụ đơn hàng"] -- "Đã tạo đơn hàng" --> PS["Dịch vụ thanh toán"]
PS -- "Đã thanh toán xong" --> IS["Dịch vụ tồn kho"]
IS -- "Đã trừ tồn kho" --> SS["Dịch vụ giao hàng"]
IS -- "Thiếu tồn kho (bù)" --> PS
end
subgraph Orches["Orchestration (điều phối trung tâm)"]
O["Saga Orchestrator"] --> P2["Dịch vụ thanh toán"]
O --> I2["Dịch vụ tồn kho"]
O --> D2["Dịch vụ giao hàng"]
P2 -- "Phản hồi" --> O
I2 -- "Phản hồi" --> O
end
Phương thức Orchestration (điều phối) có bộ điều phối trung tâm gọi là saga orchestrator nắm giữ máy trạng thái (state machine), gửi lệnh tới từng dịch vụ và nhận phản hồi để quyết định bước tiếp theo. Luồng tập trung ở một nơi nên dễ quan sát, gỡ lỗi, giám sát và thuận tiện quản lý logic rẽ nhánh, bù phức tạp, nhưng orchestrator có thể trở thành điểm lỗi đơn và điểm tập trung logic nên cần thiết kế tính sẵn sàng cao cho chính nó. Trong thực tế, chiến lược kết hợp phổ biến là luồng đơn giản dùng choreography, còn luồng có nhiều dịch vụ tham gia và quy tắc bù phức tạp dùng orchestration.
| Phân loại | Choreography | Orchestration |
|---|---|---|
| Chủ thể điều phối | Không có (phát/đăng ký sự kiện) | Orchestrator trung tâm |
| Mức liên kết | Thấp (lỏng) | Tập trung vào orchestrator |
| Khả năng quan sát luồng | Thấp (phân tán) | Cao (tập trung một nơi) |
| Tình huống phù hợp | 2~4 dịch vụ tham gia, luồng đơn giản | Nhiều dịch vụ, bù/rẽ nhánh phức tạp |
| Rủi ro | Phụ thuộc vòng, khó truy vết | Điểm lỗi đơn, logic phình to |
4. Các yếu tố kỹ thuật cốt lõi khi hiện thực
Để saga thực sự vận hành tin cậy, nhất thiết phải có một số cơ chế hỗ trợ. Thứ nhất là tính idempotent (Idempotency). Do thử lại mạng có thể khiến cùng một thông điệp đến trùng lặp, mỗi bước và bù phải cho kết quả giống nhau dù được thực thi nhiều lần. Thông thường lưu ID giao dịch hoặc ID thông điệp để lọc xử lý trùng lặp.
Thứ hai là phát sự kiện nguyên tử. Nếu cập nhật DB cục bộ và phát sự kiện trải trên hai hệ thống riêng (DB và message broker), sẽ phát sinh vấn đề ghi kép (dual write) khi DB đã commit nhưng phát sự kiện thất bại. Để ngăn điều này, dùng kèm mẫu Transactional Outbox - ghi sự kiện vào bảng outbox của DB trong cùng giao dịch và một relay riêng đọc để phát. Như vậy "thay đổi trạng thái và phát sự kiện" được gộp nguyên tử trong cùng giao dịch cục bộ.
Thứ ba là theo dõi trạng thái và timeout. Kiểu orchestrator phải lưu bền trạng thái tiến trình của từng instance saga để khi khởi động lại sau sự cố có thể tiếp tục hoặc bù. Với bước không có phản hồi, cần quy tắc timeout và thử lại, và cuối cùng chuyển sang bù hoặc can thiệp thủ công (ví dụ: dead letter, thông báo người vận hành).
5. So sánh với phương thức khác — Tại sao là Saga
So sánh saga với 2PC và phương thức sự kiện đơn giản sẽ làm rõ bối cảnh lựa chọn. 2PC bảo đảm nhất quán mạnh tức thì nhưng khả năng mở rộng thấp do khóa và chặn, trở thành nút thắt trong môi trường MSA có nhiều bên tham gia và độ trễ lớn. Ngược lại, saga commit ngay từng bước nên không giữ khóa lâu, thông lượng và tính sẵn sàng cao, nhưng vì hiệu chỉnh được thực hiện sau nên tồn tại cửa sổ thời gian quan sát được sự bất nhất ở giữa.
| Hạng mục | 2PC | Saga |
|---|---|---|
| Tính nhất quán | Nhất quán mạnh (tức thì) | Nhất quán cuối cùng |
| Chiếm giữ khóa | Thời gian dài đến khi commit | Chỉ trong bước cục bộ |
| Sẵn sàng/mở rộng | Thấp (chặn) | Cao |
| Cách rollback | DB ROLLBACK | Giao dịch bù (hủy ngữ nghĩa) |
| Tính cô lập | Bảo đảm | Không bảo đảm (cần biện pháp riêng) |
Ở đây vấn đề khó nhất về mặt thực tế là thiếu tính cô lập. Trong khi saga đang tiến hành, giao dịch khác có thể đọc trạng thái trung gian chưa được xác nhận (tương tự dirty read), có thể phát sinh hiện tượng bất thường như người dùng khác tra cứu chỗ ngồi sắp bị hủy đặt. Để giảm nhẹ, thiết kế kèm các biện pháp như đặt khóa ngữ nghĩa (semantic lock) "đang xử lý/đã xác nhận" bằng trường trạng thái, commit sau khi đọc lại xác nhận (reread), cập nhật có thể hoán đổi (commutative update).
Ví dụ cụ thể, xử lý đơn hàng của một sàn thương mại điện tử lớn được chia cho đơn hàng, thanh toán, tồn kho, điểm thưởng, giao hàng thuộc các nhóm và DB khác nhau; nếu gộp hàng nghìn đơn hàng mỗi giây bằng 2PC, độ trễ phản hồi của cổng thanh toán sẽ làm tê liệt toàn bộ. Thực tế các nền tảng này xử lý bất đồng bộ từng bước bằng tổ hợp saga + outbox + message broker (Kafka, v.v.), và khi thanh toán thất bại thì khớp lại tính nhất quán bằng bù hoàn tiền tự động, hủy đơn hàng. Kết quả là thông lượng thường ngày tăng mạnh, còn với người dùng thì thể hiện rõ trạng thái trung gian như "đang xác nhận thanh toán" qua UX để hấp thụ tự nhiên tính nhất quán cuối cùng.
6. Các điểm cần lưu ý và hàm ý
Từ góc độ Kỹ sư chuyên nghiệp, việc áp dụng saga phải được xử lý không như lựa chọn mẫu đơn thuần mà như quyết định kiến trúc về mô hình nhất quán.
- Tiêu chí quyết định áp dụng: Quyết toán cốt lõi mà nhất quán mạnh là bắt buộc về pháp lý, tài chính thì gom vào giao dịch cục bộ trong một dịch vụ, chỉ áp dụng saga có chọn lọc cho luồng dài vượt ranh giới dịch vụ. Biến mọi thứ thành saga ngược lại làm độ phức tạp bùng nổ, nên phải quyết định cùng với thiết kế ranh giới dịch vụ (Bounded Context của DDD).
- Thiết kế ưu tiên khả năng bù: Đặt các bước không thể hoàn lại (xuất vé, xuất kho, quyết toán bên ngoài) ở nửa sau của saga, và phản ánh quy tắc nghiệp vụ như phí hủy, chính sách hoàn tiền vào logic bù. Bản thân bù cũng có thể thất bại nên cần chuẩn bị đường thử lại, dead letter, can thiệp của người vận hành.
- Khả năng quan sát và vận hành: Do luồng rải rác trên nhiều dịch vụ, nếu không có truy vết phân tán (Distributed Tracing), dashboard trạng thái saga, phát hiện và cảnh báo saga chưa hoàn tất thì việc tìm nguyên nhân sự cố rất khó. Phương thức orchestration có lợi thế ở điểm này, và kết nối toàn bộ các đoạn bằng ID tương quan (correlation ID).
- Chấp nhận đánh đổi và thiết kế UX: Saga hy sinh tính cô lập và nhất quán tức thì để đổi lấy tính sẵn sàng, tự chủ, khả năng mở rộng. Do đó thiết kế hiệu chỉnh ở chiều trải nghiệm người dùng như hiển thị trạng thái "đang xử lý", chống trùng lặp (idempotency), thông báo kết quả cuối cùng phải đi cặp với thiết kế kỹ thuật.
- Triển vọng: Với sự trưởng thành của workflow engine quản lý orchestration dựa trên trạng thái bằng mã (ví dụ: Temporal, loại Camunda) và nền tảng event streaming, saga đang phát triển theo hướng được chuẩn hóa bằng framework workflow/orchestration thay vì tự hiện thực thủ công. Khi kết hợp với CQRS, event sourcing, có thể tận dụng chính lịch sử tiến trình saga như tài sản kiểm toán.
Tóm tắt một câu: Mẫu Saga là kỹ thuật hiện thực tính nguyên tử của giao dịch phân tán ở mức ứng dụng bằng chuỗi giao dịch cục bộ theo dịch vụ và giao dịch bù khi thất bại, là chiến lược nhất quán cuối cùng đổi nhất quán mạnh của 2PC lấy tính sẵn sàng và khả năng mở rộng, trong đó lựa chọn choreography/orchestration và thiết kế idempotency, outbox, bù quyết định thành bại.