Điều kiện tiên quyết: Đây là Phần 8 của Khóa Học System Design. Bạn nên tham khảo lại Phần 7: Thiết Kế API Kháng Lặp trước khi tìm hiểu sâu — các thao tác hoàn trả (compensating transactions) trong mô hình Saga bắt buộc phải đảm bảo tính kháng lặp (idempotency).
Answer-first: Trong kiến trúc microservices và AI-Native phân tán 2026, mô hình Saga (Saga Pattern) giải quyết bài toán quản lý giao dịch phân tán (distributed transactions) xuyên suốt nhiều microservices bằng cách chia nhỏ một giao dịch lớn thành một chuỗi các giao dịch cục bộ (local transactions). Khi một bước trong chuỗi gặp sự cố, hệ thống tự động kích hoạt chuỗi các hành động hoàn trả (compensating transactions) theo thứ tự ngược lại (reverse LIFO order) để khôi phục trạng thái nhất quán của toàn bộ hệ thống.
Hạn Chế Của Giao Thức 2PC Trong Kiến Trúc Microservices
Answer-first: Giao thức Two-Phase Commit (2PC) là một mô hình đồng bộ hóa blocking (blocking protocol), tạo ra điểm nghẽn chịu lỗi đơn điểm (single point of failure) tại bộ điều phối (coordinator). Nếu coordinator gặp sự cố trong khoảng thời gian giữa giai đoạn Prepare và Commit, toàn bộ các services tham gia sẽ bị khóa tài nguyên (resource lock) vô thời hạn — gây nguy cơ treo toàn bộ hệ thống phân tán.
Kịch Bản Lỗi Của Giao Thức 2PC
sequenceDiagram
participant Coord as Coordinator (Bộ Điều Phối)
participant S1 as Order Service
participant S2 as Payment Service
participant S3 as Inventory Service
Coord->>S1: Gửi yêu cầu Prepare
Coord->>S2: Gửi yêu cầu Prepare
Coord->>S3: Gửi yêu cầu Prepare
S1-->>Coord: Trả về Ready
S2-->>Coord: Trả về Ready
S3-->>Coord: Trả về Ready
Note over Coord: 💥 Coordinator gặp sự cố (CRASHES) sau khi gửi Prepare, chưa gửi Commit
Note over S1,S3: Tất cả các Services bị tạm khóa tài nguyên (blocked)!
Note over S1,S3: Không thể tự ý Rollback hay Commit!
Các hạn chế khác của 2PC:
- Tính Blocking: Tất cả các services phải chờ phản hồi từ coordinator, gây hiện tượng tích tụ độ trễ và giảm tính sẵn sàng của hệ thống.
- Điểm chết đơn điểm (Single Point of Failure): Sự cố tại coordinator làm sụp đổ toàn bộ chuỗi giao dịch.
- Không tương thích giữa các công nghệ: Khó triển khai 2PC trên các hệ thống microservices đa ngôn ngữ và sử dụng nhiều loại cơ sở dữ liệu khác nhau.
So Sánh Saga Orchestration vs Saga Choreography
Answer-first: Saga Orchestration áp dụng một bộ điều phối trung tâm (central orchestrator - ví dụ Temporal workflow) trực tiếp điều phối từng bước thực thi trong chuỗi — giúp dễ dàng giám sát trạng thái, truy vết lỗi và xử lý bù trừ. Saga Choreography dựa vào việc phát bản tin sự kiện (event-driven) — các services tự lắng nghe sự kiện và kích hoạt bước kế tiếp, giúp tăng tính độc lập (decoupling) nhưng gây khó khăn lớn trong việc truy vết luồng sự kiện khi xảy ra lỗi.
Luồng Xử Lý Của Saga Pattern
graph LR
T1["T1: Tạo đơn hàng ✅"] --> T2["T2: Giữ hàng trong kho ✅"]
T2 --> T3["T3: Thanh toán ❌"]
T3 --> C2["C2: Giải phóng hàng trong kho\n(Compensating)"]
C2 --> C1["C1: Hủy đơn hàng\n(Compensating)"]
style T3 fill:#f8d7da,stroke:#dc3545
style C2 fill:#fff3cd,stroke:#f0a500
style C1 fill:#fff3cd,stroke:#f0a500
style T1 fill:#d4edda,stroke:#28a745
style T2 fill:#d4edda,stroke:#28a745
Đặc tính của kiến trúc Saga:
- Đảm bảo ACD không có tính I (ACD without Isolation): Đạt tính Nguyên tử (Atomicity), Nhất quán (Consistency), và Bền vững (Durability). Không có tính Cô lập (Isolation) — các trạng thái trung gian của giao dịch có thể hiển thị đối với các truy vấn khác.
- Nhất quán cuối cùng (Eventual Consistency): Hệ thống đạt trạng thái nhất quán sau khi tất cả các giao dịch cục bộ hoặc các hành động hoàn trả kết thúc.
- Hành động hoàn trả phải kháng lặp: Khi một tác vụ compensating bị thất bại và được thử lại, nó phải đảm bảo trả về cùng một kết quả nhất quán.
Triển Khai Saga Orchestration Bằng Temporal Go SDK
Answer-first: Đối tượng workflow.Saga trong Temporal SDK hỗ trợ cơ chế bồi đền theo nguyên tắc LIFO (Last In, First Out) — tự động thực thi các compensating actions theo thứ tự ngược lại so với các bước thành công trước đó. Điều này đảm bảo tính đúng đắn về mặt nghiệp vụ: khi hoàn tiền (refund) thành công mới tiếp tục giải phóng hàng trong kho (release inventory) và cuối cùng mới cập nhật hủy đơn hàng (cancel order).
package saga
import (
"fmt"
"time"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
type OrderSagaInput struct {
OrderID string
UserID string
Items []OrderItem
Amount float64
Currency string
}
type OrderItem struct {
ProductID string
Quantity int
}
// OrderSagaWorkflow điều phối giao dịch phân tán cho quy trình đơn hàng
func OrderSagaWorkflow(ctx workflow.Context, input OrderSagaInput) error {
activityOpts := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 5,
InitialInterval: time.Second,
MaximumInterval: 30 * time.Second,
BackoffCoefficient: 2.0,
// Các lỗi nghiệp vụ không thể retry
NonRetryableErrorTypes: []string{"PAYMENT_DECLINED", "INVENTORY_PERMANENTLY_UNAVAILABLE"},
},
}
ctx = workflow.WithActivityOptions(ctx, activityOpts)
var saga workflow.Saga
saga.SetParallelCompensation(false) // Thực thi compensation theo thứ tự ngược LIFO
// ─── Bước 1: Tạo đơn hàng (Create Order) ─────────────────────────────────────────────
var orderResult CreateOrderResult
if err := workflow.ExecuteActivity(ctx, CreateOrderActivity, input).Get(ctx, &orderResult); err != nil {
return fmt.Errorf("lỗi tạo đơn hàng: %w", err)
}
// Đăng ký compensation action ngay sau khi bước này thành công
saga.AddCompensation(CancelOrderActivity, orderResult.OrderID)
// ─── Bước 2: Giữ hàng trong kho (Reserve Inventory) ────────────────────────────────────────
var reserveResult ReserveInventoryResult
if err := workflow.ExecuteActivity(ctx, ReserveInventoryActivity, orderResult.OrderID, input.Items).Get(ctx, &reserveResult); err != nil {
saga.Compensate(ctx) // Thực thi chuộc lỗi CancelOrderActivity
return fmt.Errorf("lỗi giữ hàng trong kho: %w", err)
}
saga.AddCompensation(ReleaseInventoryActivity, reserveResult.ReservationID)
// ─── Bước 3: Xử lý thanh toán (Process Payment) ──────────────────────────────────────────
var paymentResult ProcessPaymentResult
if err := workflow.ExecuteActivity(ctx, ProcessPaymentActivity, orderResult.OrderID, input.Amount).Get(ctx, &paymentResult); err != nil {
saga.Compensate(ctx) // Thực thi compensation ngược: ReleaseInventoryActivity -> CancelOrderActivity
return fmt.Errorf("lỗi xử lý thanh toán: %w", err)
}
saga.AddCompensation(RefundPaymentActivity, paymentResult.TransactionID)
// ─── Bước 4: Thông báo đơn hàng (Notify Fulfillment) ───────────────────────────────────────
if err := workflow.ExecuteActivity(ctx, NotifyFulfillmentActivity, orderResult.OrderID).Get(ctx, nil); err != nil {
saga.Compensate(ctx) // Thực thi compensation hoàn chỉnh (LIFO)
return fmt.Errorf("lỗi thông báo đơn hàng: %w", err)
}
workflow.GetLogger(ctx).Info("Saga đơn hàng hoàn tất thành công", "orderID", orderResult.OrderID)
return nil
}
// ─── Các Activity implementations ────────────────────────────────────────────────────────────
type CreateOrderResult struct{ OrderID string }
type ReserveInventoryResult struct{ ReservationID string }
type ProcessPaymentResult struct{ TransactionID string }
func CreateOrderActivity(input OrderSagaInput) (CreateOrderResult, error) {
// Thực thi chèn đơn hàng vào database (idempotent)
return CreateOrderResult{OrderID: "order-uuid"}, nil
}
func CancelOrderActivity(orderID string) error {
// Cập nhật trạng thái đơn hàng thành cancelled (idempotent)
return nil
}
func ReserveInventoryActivity(orderID string, items []OrderItem) (ReserveInventoryResult, error) {
return ReserveInventoryResult{ReservationID: fmt.Sprintf("res-%s", orderID)}, nil
}
func ReleaseInventoryActivity(reservationID string) error {
// Giải phóng giữ hàng trong kho (idempotent)
return nil
}
func ProcessPaymentActivity(orderID string, amount float64) (ProcessPaymentResult, error) {
return ProcessPaymentResult{TransactionID: fmt.Sprintf("tx-%s", orderID)}, nil
}
func RefundPaymentActivity(transactionID string) error {
// Hoàn tiền giao dịch (idempotent)
return nil
}
func NotifyFulfillmentActivity(orderID string) error {
return nil
}
[!IMPORTANT] Thứ tự thực thi Compensating Actions (LIFO): Phương thức
saga.Compensate()của Temporal đảm bảo thứ tự thực thi ngược lại chính xác so với luồng thành công. Bước 3 bị lỗi sẽ kích hoạt hoàn trả của Bước 2 (Release Inventory) trước, rồi mới tới Bước 1 (Cancel Order), đảm bảo tính đúng đắn theo logic nghiệp vụ.
Mô Hình Transactional Outbox Pattern
Answer-first: Mô hình Transactional Outbox đảm bảo rằng dữ liệu sự kiện (event) được ghi vào bảng Outbox trong cùng một cơ sở dữ liệu transaction nguyên tử (atomic transaction) với bản ghi nghiệp vụ chính. Sau đó, một tiến trình Change Data Capture (CDC) như Debezium sẽ theo dõi nhật ký WAL (Write-Ahead Log) của database để đọc các bản ghi Outbox và gửi tin nhắn tới Kafka mà không làm thất thoát sự kiện.
Tại Sao Cần Mô Hình Transactional Outbox
Vấn đề khi không sử dụng Outbox:
- Tiến trình ứng dụng ghi thành công dữ liệu đơn hàng vào cơ sở dữ liệu.
- Ứng dụng gặp sự cố (crash) trước khi gọi câu lệnh gửi sự kiện tới Kafka (
kafka.Produce). - Dữ liệu đơn hàng tồn tại trong database nhưng các dịch vụ hạ lưu (Downstream Services) không nhận được sự kiện -> Hệ thống bị mất đồng bộ dữ liệu.
Giải pháp với Transactional Outbox:
- Ứng dụng chèn bản ghi đơn hàng và bản ghi Outbox event trong cùng một database transaction duy nhất.
- Bộ công cụ Debezium CDC theo dõi nhật ký WAL của PostgreSQL để phát hiện bản ghi Outbox mới.
- Debezium tự động đẩy tin nhắn sự kiện tới Kafka.
- Trường hợp Debezium gặp sự cố, khi khởi động lại nó sẽ tiếp tục đọc từ mốc WAL cũ mà không làm thất thoát bất kỳ sự kiện nào.
-- Cấu trúc bảng lưu trữ Outbox
CREATE TABLE outbox_table (
id UUID NOT NULL DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(100) NOT NULL, -- Loại đối tượng: 'order', 'payment'
aggregate_id VARCHAR(255) NOT NULL, -- Mã định danh đối tượng (Order UUID)
event_type VARCHAR(100) NOT NULL, -- Tên sự kiện: 'ORDER_CREATED'
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
// Ghi bản ghi nghiệp vụ và outbox event trong cùng 1 transaction
func (s *OrderService) CreateOrder(ctx context.Context, userID string, amount float64) (string, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()
// 1. Thực thi chèn bản ghi đơn hàng
var orderID string
err = tx.QueryRowContext(ctx,
`INSERT INTO orders (user_id, amount, status) VALUES ($1, $2, 'pending') RETURNING id`,
userID, amount,
).Scan(&orderID)
if err != nil {
return "", err
}
// 2. Chèn bản ghi outbox event trong cùng transaction
payload, _ := json.Marshal(map[string]interface{}{
"order_id": orderID, "user_id": userID, "amount": amount,
})
_, err = tx.ExecContext(ctx,
`INSERT INTO outbox_table (aggregate_type, aggregate_id, event_type, payload)
VALUES ('order', $1, 'ORDER_CREATED', $2)`,
orderID, payload,
)
if err != nil {
return "", err
}
// 3. Commit transaction - Debezium sẽ đọc WAL và gửi message tới Kafka
return orderID, tx.Commit()
}
Cấu Hình Debezium PostgreSQL Outbox EventRouter
{
"name": "postgres-outbox-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres-db.internal",
"database.port": "5432",
"database.user": "debezium",
"database.password": "${file:/secrets/debezium.properties:db.password}",
"database.dbname": "orders_db",
"database.server.name": "orders-dbserver",
"plugin.name": "pgoutput",
"slot.name": "debezium_outbox_slot",
"table.include.list": "public.outbox_table",
"tombstones.on.delete": "false",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.table.field.event.id": "id",
"transforms.outbox.table.field.event.key": "aggregate_id",
"transforms.outbox.table.field.event.payload": "payload",
"transforms.outbox.route.by.field": "aggregate_type",
"transforms.outbox.route.topic.replacement": "events.${routedByValue}",
"transforms.outbox.table.expand.json.payload": "true",
"key.converter": "org.apache.kafka.connect.storage.StringConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false"
}
}
[!NOTE] Cấu hình
route.topic.replacement: Giá trịaggregate_type = 'order'sẽ được định tuyến tự động tới Kafka topicevents.order. Nếuaggregate_type = 'payment', tin nhắn sẽ tới topicevents.payment.Cấu hình WAL trong
postgresql.conf:wal_level = logical max_wal_senders = 4 max_replication_slots = 4
Câu Hỏi Thường Gặp (FAQ)
Sự khác biệt cốt lõi giữa Saga Orchestration và Saga Choreography?
Saga Orchestration (ví dụ sử dụng Temporal): Sử dụng một bộ điều phối trung tâm để quản lý toàn bộ luồng thực thi và trạng thái giao dịch. Giúp dễ dàng giám sát, truy vết lỗi và quản lý tập trung các hành động bồi đền (compensating actions). Saga Choreography: Các microservices tự phát và lắng nghe sự kiện để nối tiếp chuỗi giao dịch. Giúp giảm mức độ phụ thuộc giữa các services nhưng gây khó khăn khi phải truy vết luồng xử lý trên nhiều Kafka topics.
Yêu cầu đối với các giao dịch hoàn trả (Compensating Transactions)?
Các compensating transactions phải đáp ứng 3 yêu cầu: (1) Tính Kháng Lặp (Idempotent) — có thể thực thi lại nhiều lần mà vẫn mang lại kết quả giống nhau; (2) Tính Đúng Đắn Về Nghiệp Vụ — thực hiện đảo ngược trạng thái nghiệp vụ (như hoàn tiền, giải phóng giữ kho, hủy đơn) thay vì chỉ thực thi SQL ROLLBACK; (3) Khả Năng Thử Lại (Retryable) — có thể được thử lại liên tục cho đến khi hoàn tất thành công.
Khi nào nên áp dụng mô hình Transactional Outbox?
Áp dụng Transactional Outbox khi ứng dụng cần đảm bảo sự kiện được gửi tới Kafka đúng 100% mỗi khi dữ liệu nghiệp vụ ghi vào database thành công. Mô hình này giúp loại bỏ rủi ro mất mát sự kiện khi hệ thống gặp sự cố ngắt điện hoặc crash đột ngột.
🔗 Bay Sang Bài Tới: Phần 9: Phân Mảnh Nhất Quán Khớp Ngàm Bánh Răng (Consistent Hashing) — Hạt Vỡ Mộng Ảo (Virtual Nodes) & Rập Khuôn Vòng Băm CRC32 Trong Go — Bới lông chọc lỗ xem trò băm chẻ dư (modulo hashing) thọt ngáo hầm làm sao, nhẩm nhức nách con tính phơi bụng nứt hạt ảo (virtual node variance math), và rèn cái khiên sắt bọc thép thread-safe GetN vung búa.