Hầu hết các lập trình viên Go xây dựng microservices đều biết đến mẫu Choreography Saga: service A phát ra (emit) một sự kiện, service B phản ứng, service C phản ứng với B, và cứ tiếp tục như vậy. Nếu bước C thất bại, các services sẽ phát ra các sự kiện “bù trừ” (compensation) theo thứ tự ngược lại. Mẫu này hoạt động một cách mượt mà đối với các luồng đơn giản, nhưng lại phá vỡ tính hiệu quả khi số lượng bước tăng lên: việc debug một saga thất bại đòi hỏi phải lần theo dấu vết (tracing) các sự kiện qua năm topic của message broker, và việc triển khai logic bù trừ đòi hỏi mỗi service phải hiểu toàn bộ trạng thái của saga.

Dapr Workflow cung cấp một mô hình khác: Orchestrated Saga (Saga Điều Phối). Một hàm điều phối viên (orchestrator) duy nhất sở hữu toàn bộ vòng đời của giao dịch, gọi từng bước một cách tường minh, và quản lý việc bù trừ trong một hàm Go duy nhất, dễ đọc. Bộ điều phối này mang tính bền bỉ (durable) — nó sống sót qua các lần khởi động lại tiến trình (process restarts) mà không làm mất vị trí của nó trong luồng.

Bài viết này đi qua một bản triển khai Go hoàn chỉnh của Orchestrated Saga sử dụng Dapr Workflow, lấy ví dụ là việc chuyển tiền qua ba microservices tài chính.

Để có ngữ cảnh rộng hơn về microservices tài chính, hãy xem Kiến Trúc Microservices Tài Chính: Saga & Sổ CáiLộ Trình Học Tập Dành Cho Lập Trình Viên Core Banking. Các mẫu kiến trúc core banking hiện đại, bao gồm event sourcing và thiết kế sổ cái, được đề cập trong Phần 4: Kiến Trúc Core Banking Hiện Đại.


Choreography (Vũ Đạo) vs. Orchestration (Điều Phối): Khi Nào Dapr Workflow Trở Thành Sự Lựa Chọn Tốt Hơn

Cả hai mẫu đều triển khai mô hình giao dịch phân tán Saga. Sự lựa chọn giữa chúng mang tính kiến trúc:

Yếu TốChoreography (Sự kiện Pub/Sub)Orchestration (Dapr Workflow)
Sự Liên Kết (Coupling)Các service tách rời khỏi nhauCác service liên kết với API của orchestrator
Khả Năng Quan Sát LuồngPhân tán trên các event logsTập trung trong một hàm orchestrator
Logic Bù Trừ (Compensation)Mỗi service tự triển khai logic riêngOrchestrator quản lý việc bù trừ theo trình tự
Gỡ Lỗi (Debugging)Yêu cầu theo dõi (tracing) qua nhiều topicMột nhật ký lịch sử workflow duy nhất
Số Bước Lý Tưởng2–4 bướcHơn 4 bước
Khôi Phục Sau LỗiPhát lại (replay) sự kiện, xử lý DLQ trên từng serviceTích hợp sẵn tính năng phát lại và trạng thái bền bỉ

Khi nào nên chọn Dapr Workflow Orchestration:

  • Saga có 4+ bước có thứ tự phải thực thi theo trình tự nghiêm ngặt
  • Logic bù trừ phức tạp và phụ thuộc vào thứ tự
  • Bạn cần một hồ sơ lịch sử trạng thái của từng giao dịch có thể đọc và kiểm tra được
  • Domain nghiệp vụ yêu cầu các đảm bảo tính nhất quán mạnh mẽ (giao dịch tài chính, xử lý đơn hàng)

Khi nào Choreography tốt hơn:

  • Các service thực sự cần phải được tách rời (decoupled) và có thể triển khai độc lập mà không cần biết về luồng tổng thể
  • Các bước có thể thực thi đồng thời không có sự phụ thuộc vào thứ tự
  • Bản thân luồng sự kiện (event stream) cung cấp đủ nhật ký kiểm toán (audit trail) bạn cần

Cấu Trúc Bên Trong Dapr Workflow: Durable Execution Hoạt Động Như Thế Nào

Dapr Workflow được xây dựng dựa trên Durable Task Framework. Việc hiểu mô hình thực thi của nó là rất quan trọng để viết các orchestrators đúng đắn.

Khi một hàm orchestrator thực thi, nó không chạy như một hàm Go bình thường. Nó chạy dưới dạng một cỗ máy trạng thái dựa trên phát lại (replay-based state machine):

  1. Lần chạy đầu tiên: Orchestrator chạy từng bước một, lên lịch từng activity (lời gọi service bên ngoài) dưới dạng một tác vụ bất đồng bộ (async task).
  2. Sau khi tiến trình khởi động lại hoặc gặp sự cố: Khi workflow runtime khởi động lại orchestrator, nó sẽ phát lại (replay) toàn bộ lịch sử của các sự kiện đã được ghi nhận từ state store phụ trợ. Các activities đã hoàn thành sẽ không bị thực thi lại — kết quả đã ghi của chúng được trả về ngay lập tức.
  3. Yêu cầu tính tất định (Determinism): Vì orchestrator được phát lại, nó phải mang tính tất định (deterministic). Mọi hoạt động không mang tính tất định (số ngẫu nhiên, time.Now(), đọc biến môi trường) sẽ tạo ra các kết quả khác nhau khi phát lại và làm hỏng trạng thái workflow.
graph TD
    START[Bắt Đầu Workflow] --> REPLAY{Chạy Lần Đầu Hay Replay?}
    REPLAY -->|Chạy Lần Đầu| STEP1[Thực Thi Activity: Trừ Tiền (DebitSource)]
    STEP1 --> PERSIST[Lưu Trạng Thái: Trừ Tiền Xong (DebitCompleted)]
    PERSIST --> STEP2[Thực Thi Activity: Cộng Tiền (CreditTarget)]
    STEP2 --> PERSIST2[Lưu Trạng Thái: Cộng Tiền Xong (CreditCompleted)]
    PERSIST2 --> DONE[Hoàn Thành Workflow]
    
    REPLAY -->|Replay| REPLAY1[Replay Sự kiện: DebitCompleted - ngay lập tức]
    REPLAY1 --> REPLAY2[Replay Sự kiện: CreditCompleted - ngay lập tức]
    REPLAY2 --> STEP3[Thực thi Activity đang chờ xử lý tiếp theo]

Điều này có nghĩa là mã Go orchestrator của bạn không bao giờ được phép gọi trực tiếp time.Now(). Thay vào đó, hãy sử dụng ctx.CurrentUTCDateTime() — Dapr Workflow cung cấp phương thức này để trả về thời gian tất định đã được ghi nhận khi bước đó thực thi lần đầu tiên.


Cài Đặt Dapr Workflow Trong Một Dự Án Microservices Go

Đầu tiên, thêm Dapr Go SDK:

go get github.com/dapr/go-sdk@v1.11.0

Service Go của bạn cần runtime của Dapr Workflow được khởi tạo song song với Dapr client:

package main

import (
    "context"
    "log"

    dapr "github.com/dapr/go-sdk/client"
    "github.com/dapr/go-sdk/workflow"
)

func main() {
    // Khởi tạo Dapr workflow worker
    w, err := workflow.NewWorker()
    if err != nil {
        log.Fatalf("failed to create workflow worker: %v", err)
    }

    // Đăng ký orchestrator và tất cả các hàm activity
    if err := w.RegisterWorkflow(FundTransferWorkflow); err != nil {
        log.Fatalf("failed to register workflow: %v", err)
    }
    if err := w.RegisterActivity(DebitSourceAccount); err != nil {
        log.Fatalf("failed to register activity: %v", err)
    }
    if err := w.RegisterActivity(CreditTargetAccount); err != nil {
        log.Fatalf("failed to register activity: %v", err)
    }
    if err := w.RegisterActivity(RecordLedgerEntry); err != nil {
        log.Fatalf("failed to register activity: %v", err)
    }
    if err := w.RegisterActivity(CompensateDebitSourceAccount); err != nil {
        log.Fatalf("failed to register activity: %v", err)
    }

    // Khởi động worker (kết nối với Dapr sidecar)
    if err := w.Start(); err != nil {
        log.Fatalf("failed to start workflow worker: %v", err)
    }
    defer w.Shutdown()

    // ... khởi động server HTTP/gRPC của bạn
}

Bước 1: Định Nghĩa Hàm Workflow Orchestrator

Hàm orchestrator là trái tim của Saga. Nó xác định thứ tự thực thi, xử lý lỗi và kích hoạt quy trình bù trừ.

// FundTransferInput mang các tham số đầu vào của saga
type FundTransferInput struct {
    TransactionID string  `json:"transaction_id"`
    SourceAccount string  `json:"source_account"`
    TargetAccount string  `json:"target_account"`
    Amount        float64 `json:"amount"`
    Currency      string  `json:"currency"`
}

// FundTransferWorkflow là Saga orchestrator.
// Nó BẮT BUỘC phải mang tính tất định (deterministic): không dùng time.Now(), không dùng rand, không dùng env vars.
func FundTransferWorkflow(ctx *workflow.WorkflowContext) (any, error) {
    var input FundTransferInput
    if err := ctx.GetInput(&input); err != nil {
        return nil, fmt.Errorf("failed to get workflow input: %w", err)
    }

    // --- Bước 1: Trừ tiền tài khoản nguồn (Debit) ---
    var debitResult DebitResult
    if err := ctx.CallActivity(DebitSourceAccount, workflow.ActivityInput(input)).Await(&debitResult); err != nil {
        // Trừ tiền thất bại — không cần bù trừ (chưa có gì bị trừ)
        return nil, fmt.Errorf("debit failed: %w", err)
    }

    // --- Bước 2: Cộng tiền tài khoản đích (Credit) ---
    var creditResult CreditResult
    if err := ctx.CallActivity(CreditTargetAccount, workflow.ActivityInput(input)).Await(&creditResult); err != nil {
        // Cộng tiền thất bại — phải bù trừ cho bước trừ tiền (debit)
        ctx.GetLogger().Warn("CreditTargetAccount failed, compensating debit", "error", err)
        
        compensateInput := CompensateInput{
            TransactionID: input.TransactionID,
            Account:       input.SourceAccount,
            Amount:        input.Amount,
            Reason:        fmt.Sprintf("credit_failed: %v", err),
        }
        var compensateResult CompensateResult
        if compErr := ctx.CallActivity(CompensateDebitSourceAccount, 
            workflow.ActivityInput(compensateInput)).Await(&compensateResult); compErr != nil {
            // Bản thân việc bù trừ bị lỗi — đây là tình huống báo động nguy cấp
            return nil, fmt.Errorf("CRITICAL: compensation failed after credit failure: debit_err=%v, comp_err=%w", err, compErr)
        }
        return nil, fmt.Errorf("transfer failed and compensated: %w", err)
    }

    // --- Bước 3: Ghi nhận bản ghi sổ cái (Nhật ký kiểm toán / Audit trail) ---
    ledgerInput := LedgerInput{
        TransactionID: input.TransactionID,
        DebitResult:   debitResult,
        CreditResult:  creditResult,
        Timestamp:     ctx.CurrentUTCDateTime(), // thời gian tất định (deterministic time)
    }
    var ledgerResult LedgerResult
    if err := ctx.CallActivity(RecordLedgerEntry, workflow.ActivityInput(ledgerInput)).Await(&ledgerResult); err != nil {
        // Việc ghi sổ cái thất bại — đây là một vấn đề về tính nhất quán nhưng tiền đã di chuyển chính xác.
        // Kích hoạt một cảnh báo đối soát (reconciliation alert) thay vì bù trừ toàn bộ giao dịch chuyển tiền.
        ctx.GetLogger().Error("LedgerEntry failed after successful transfer — reconciliation required",
            "transaction_id", input.TransactionID,
            "error", err)
        // Trả về thành công một phần (partial success) để biểu thị giao dịch đã hoàn thành nhưng nhật ký kiểm toán cần được sửa chữa
    }

    return &FundTransferResult{
        TransactionID:  input.TransactionID,
        Status:         "completed",
        LedgerRecorded: err == nil,
    }, nil
}

Bước 2: Triển Khai Các Hàm Activity (Từng Bước Riêng Lẻ Của Saga)

Mỗi activity là một hàm bị cô lập (isolated), mang tính lũy đẳng (idempotent) để thực hiện một bước duy nhất. Các activities có thể có các tác dụng phụ (side effects - ghi cơ sở dữ liệu, gọi API) — không giống như orchestrator, chúng không cần phải mang tính tất định.

// DebitSourceAccount trừ tiền tài khoản nguồn một cách nguyên tử (atomically).
// Hàm này BẮT BUỘC phải là idempotent (lũy đẳng): nếu được gọi hai lần với cùng một TransactionID, 
// lần gọi thứ hai phải là một no-op (chứ không phải trừ tiền hai lần).
func DebitSourceAccount(ctx context.Context, input FundTransferInput) (DebitResult, error) {
    db := dbFromContext(ctx) // retrieve GORM *gorm.DB from context

    var result DebitResult
    err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // Kiểm tra tính lũy đẳng (Idempotency check): giao dịch này đã được xử lý chưa?
        var existing LedgerTransaction
        if err := tx.Where("external_id = ? AND type = ?", 
            input.TransactionID, "debit").First(&existing).Error; err == nil {
            // Đã được xử lý — trả về kết quả đã lưu trữ (cached)
            result = DebitResult{
                DebitID:   existing.ID,
                NewBalance: existing.PostBalance,
                Idempotent: true,
            }
            return nil
        } else if !errors.Is(err, gorm.ErrRecordNotFound) {
            return fmt.Errorf("idempotency check failed: %w", err)
        }

        // Khóa hàng tài khoản và kiểm tra số dư
        var account Account
        if err := tx.Set("gorm:query_option", "FOR UPDATE").
            Where("account_number = ?", input.SourceAccount).
            First(&account).Error; err != nil {
            return fmt.Errorf("account not found: %w", err)
        }
        if account.Balance < input.Amount {
            return ErrInsufficientFunds
        }

        // Trừ tiền
        newBalance := account.Balance - input.Amount
        if err := tx.Model(&account).Update("balance", newBalance).Error; err != nil {
            return fmt.Errorf("balance update failed: %w", err)
        }

        // Ghi lại bản ghi sổ cái (ledger entry)
        ledger := LedgerTransaction{
            ExternalID:  input.TransactionID,
            Type:        "debit",
            AccountNo:   input.SourceAccount,
            Amount:      input.Amount,
            PostBalance: newBalance,
            CreatedAt:   time.Now(),
        }
        if err := tx.Create(&ledger).Error; err != nil {
            return fmt.Errorf("ledger insert failed: %w", err)
        }

        result = DebitResult{DebitID: ledger.ID, NewBalance: newBalance}
        return nil
    })

    return result, err
}

Mẫu (pattern) then chốt ở đây là tính lũy đẳng thông qua ExternalID (idempotency via ExternalID): trước khi thực hiện trừ tiền, hàm sẽ kiểm tra xem một bản ghi sổ cái có cùng TransactionID đã tồn tại hay chưa. Nếu đã có, nó sẽ trả về kết quả đã được tính toán trước đó mà không lặp lại thao tác. Điều này rất quan trọng vì Dapr Workflow có thể thử lại (retry) các lệnh gọi activity trong trường hợp lỗi mạng tạm thời, và nếu không có tính lũy đẳng, việc retry sẽ dẫn đến việc bị trừ tiền hai lần.

Mẫu lũy đẳng này, kết hợp với việc xử lý giao dịch trong Go được minh họa ở đây, phản ánh các mẫu được mô tả trong Mở Rộng Cơ Sở Dữ Liệu MySQL: Vitess & GORM Sharding đối với các thao tác ghi dữ liệu tài chính thông lượng cao.


Bước 3: Thiết Kế Và Kích Hoạt Các Giao Dịch Bù Trừ Khi Xảy Ra Lỗi

Phần bù trừ (compensation) cho việc trừ tiền chính là việc cộng tiền trở lại tài khoản nguồn — nhưng nó phải được ghi nhận dưới dạng một sự đảo ngược (reversal), chứ không chỉ đơn giản là xóa bỏ (delete), để duy trì tính toàn vẹn của nhật ký kiểm toán.

func CompensateDebitSourceAccount(ctx context.Context, input CompensateInput) (CompensateResult, error) {
    db := dbFromContext(ctx)

    var result CompensateResult
    err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // Kiểm tra tính lũy đẳng cho chính quy trình bù trừ này
        var existing LedgerTransaction
        compensateID := input.TransactionID + ":compensate"
        if err := tx.Where("external_id = ? AND type = ?", 
            compensateID, "compensation").First(&existing).Error; err == nil {
            result = CompensateResult{CompensationID: existing.ID, Idempotent: true}
            return nil
        }

        // Tìm bản ghi trừ tiền (debit) ban đầu
        var originalDebit LedgerTransaction
        if err := tx.Where("external_id = ? AND type = ?", 
            input.TransactionID, "debit").First(&originalDebit).Error; err != nil {
            return fmt.Errorf("original debit not found for compensation: %w", err)
        }

        // Đảo ngược lại số dư (balance) đã thay đổi
        var account Account
        if err := tx.Set("gorm:query_option", "FOR UPDATE").
            Where("account_number = ?", input.Account).
            First(&account).Error; err != nil {
            return fmt.Errorf("account not found during compensation: %w", err)
        }

        newBalance := account.Balance + input.Amount
        if err := tx.Model(&account).Update("balance", newBalance).Error; err != nil {
            return fmt.Errorf("compensation balance update failed: %w", err)
        }

        // Ghi lại giao dịch bù trừ thành một bản ghi sổ cái tách biệt
        compensation := LedgerTransaction{
            ExternalID:       compensateID,
            Type:             "compensation",
            AccountNo:        input.Account,
            Amount:           input.Amount,
            PostBalance:      newBalance,
            LinkedExternalID: input.TransactionID,
            Reason:           input.Reason,
            CreatedAt:        time.Now(),
        }
        if err := tx.Create(&compensation).Error; err != nil {
            return fmt.Errorf("compensation ledger insert failed: %w", err)
        }

        result = CompensateResult{CompensationID: compensation.ID}
        return nil
    })
    return result, err
}

Bước 4: Quan Sát Trạng Thái Saga — Truy Vấn Trạng Thái Và Lịch Sử Workflow

Một trong những tính năng mạnh mẽ nhất của Dapr Workflow là khả năng hiển thị trạng thái được tích hợp sẵn (built-in state visibility). Mọi phiên bản workflow (workflow instance) đều lưu trữ toàn bộ lịch sử thực thi của nó trong backend đã được cấu hình (Redis hoặc một SQL database).

// Truy vấn trạng thái workflow từ bất kỳ service nào bằng Dapr client
func GetTransactionStatus(ctx context.Context, transactionID string) (*WorkflowStatus, error) {
    client, err := dapr.NewClient()
    if err != nil {
        return nil, err
    }
    defer client.Close()

    resp, err := client.GetWorkflowBeta1(ctx, &dapr.GetWorkflowRequest{
        InstanceID:        transactionID,
        WorkflowComponent: "dapr",
    })
    if err != nil {
        return nil, fmt.Errorf("failed to get workflow state: %w", err)
    }

    return &WorkflowStatus{
        InstanceID:     resp.InstanceID,
        RuntimeStatus:  resp.RuntimeStatus,    // RUNNING, COMPLETED, FAILED
        CreatedAt:      resp.CreatedAt,
        LastUpdated:    resp.LastUpdatedAt,
        FailureDetails: resp.FailureDetails,
    }, nil
}

Endpoint này có thể cung cấp dữ liệu cho một API trạng thái giao dịch theo thời gian thực (real-time) cho ứng dụng ngân hàng của bạn — người tiêu dùng (ứng dụng di động, trang tổng quan giám sát - ops dashboards) có thể thăm dò (poll) hoặc đăng ký nhận trạng thái giao dịch mà không cần phải truy cập vào các hàng đợi thông báo (message queues) nội bộ.


Các Mẫu Cho Production: Lũy Đẳng (Idempotency), Hết Thời Gian (Timeouts), Và DLQ Cho Dapr Workflows

Timeouts Của Activity

Mỗi activity đều nên có một khoảng thời gian chờ (timeout) để tránh cho orchestrator phải chờ vô thời hạn đối với một service hạ nguồn đang bị treo:

// Yêu cầu: github.com/dapr/go-sdk v1.9+ (workflow.ActivityOptions.RetryPolicy được thêm vào ở v1.9)
// import "github.com/dapr/go-sdk/workflow"

// Thiết lập timeout 30 giây cho mỗi lệnh gọi activity
opts := workflow.ActivityOptions{
    StartToCloseTimeout: 30 * time.Second,
    RetryPolicy: &workflow.RetryPolicy{
        MaxAttempts:        3,
        InitialInterval:    time.Second,
        BackoffCoefficient: 2.0,
        MaxInterval:        10 * time.Second,
    },
}
if err := ctx.CallActivity(CreditTargetAccount, 
    workflow.ActivityInput(input), 
    workflow.WithActivityOptions(opts)).Await(&creditResult); err != nil {
    // Xử lý timeout hoặc hết số lần thử lại (retry exhaustion)
}

Timeout Cấp Độ Workflow

Thiết lập một giới hạn thời gian tối đa cho toàn bộ bộ khung của một saga:

// Khởi chạy workflow với tổng thời gian chờ tối đa 5 phút
resp, err := client.StartWorkflowBeta1(ctx, &dapr.StartWorkflowRequest{
    WorkflowComponent: "dapr",
    WorkflowName:      "FundTransferWorkflow",
    InstanceID:        transferRequest.TransactionID,
    Options: map[string]string{
        "workflow-timeout": "300s", // 5 phút
    },
    Input: inputBytes,
})

Xử Lý Dead Letter (DLQ)

Các workflow thất bại sau khi đã cạn kiệt tất cả các lượt thử lại (retries) sẽ chuyển sang trạng thái FAILED với một payload (thông điệp mang theo) là FailureDetails. Hãy xây dựng một worker đối soát (reconciliation worker) có nhiệm vụ truy vấn các workflow instances bị thất bại và định tuyến chúng đến một DLQ (Dead Letter Queue) hoặc một hàng đợi cần được xem xét bởi con người:

func ReconcilationWorker(ctx context.Context, client dapr.Client) {
    ticker := time.NewTicker(1 * time.Minute)
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            // Liệt kê danh sách workflow đang ở trạng thái FAILED (logic triển khai tùy thuộc vào backend trạng thái của bạn)
            failedInstances := queryFailedWorkflows(ctx)
            for _, instance := range failedInstances {
                routeToDLQ(ctx, instance)
                alertOpsTeam(ctx, instance)
            }
        }
    }
}

Ví Dụ Thực Tế: Một Saga Chuyển Tiền Qua 3 Microservices Tài Chính

Tổng hợp lại toàn bộ, một luồng (flow) chuyển tiền theo cơ chế saga hoàn chỉnh sẽ như sau:

sequenceDiagram
    participant API as Transfer API
    participant DAPR as Dapr Workflow
    participant DEBIT as Account Service (Debit)
    participant CREDIT as Account Service (Credit)
    participant LEDGER as Ledger Service

    API->>DAPR: StartWorkflow(FundTransferWorkflow)
    DAPR->>DEBIT: Activity: DebitSourceAccount
    DEBIT-->>DAPR: DebitResult (newBalance, debitID)
    
    DAPR->>CREDIT: Activity: CreditTargetAccount
    alt Credit Succeeds (Cộng tiền thành công)
        CREDIT-->>DAPR: CreditResult
        DAPR->>LEDGER: Activity: RecordLedgerEntry
        LEDGER-->>DAPR: LedgerResult
        DAPR-->>API: Workflow COMPLETED (Workflow HOÀN THÀNH)
    else Credit Fails (Cộng tiền thất bại)
        CREDIT-->>DAPR: Error
        DAPR->>DEBIT: Activity: CompensateDebitSourceAccount (Bù trừ tài khoản nguồn)
        DEBIT-->>DAPR: CompensationResult
        DAPR-->>API: Workflow FAILED (compensated) (Workflow THẤT BẠI - Đã bù trừ)
    end

Luồng giao dịch này có thể được theo dõi (inspectable) ở mọi bước thông qua Dapr dashboard hoặc trạng thái workflow API. Mọi trạng thái trung gian (intermediate state) đều được lưu (persisted). Nếu máy chủ Dapr sidecar khởi động lại vào ngay giữa lúc đang chuyển tiền, workflow sẽ phát lại (replays) từ mốc chốt (checkpoint) thành công gần nhất mà không thực thi lại các activities đã hoàn tất.

Đối với các team đang cố kết hợp Dapr Workflow chung với toàn bộ hệ sinh thái hướng sự kiện (event-driven ecosystem) bao gồm Pub/Sub, State, và Bindings, mời tham khảo bài Làm Chủ Kiến Trúc Hướng Sự Kiện với Dapr cho cấu trúc vận dụng tổng quát hơn.


Các Câu Hỏi Thường Gặp

Dapr Workflow và Dapr Pub/Sub trong Saga Pattern khác nhau điểm nào?

Dapr Pub/Sub thực thi Saga theo định hướng choreography: các dịch vụ phản ứng lại sự kiện một cách riêng rẽ, không có bộ phối hợp trung gian chung nào (coordinator). Dapr Workflow thực thi Saga theo định hướng orchestration: một hàm phối hợp đơn nhất (orchestrator function) kêu gọi trực diện từng bước một và tự quản lý nghiệp vụ bù trừ. Workflow sở hữu tầm quan sát (observability) ưu việt hơn hẳn (có nhật ký lưu trữ một mối), logic bù trừ gọn nhẹ, có sự đảm bảo về lưu lại các trạng thái — tuy nhiên phải gánh thêm trọng trách phụ thuộc vào hệ thống điều phối viên mới.

Dapr Workflow làm cách nào đối phó với sự cố và chạy thử lại (retries)?

Dapr Workflow hỗ trợ quy trình lặp thử lại cho từng activity nhỏ (per-activity retry policies) tuỳ chỉnh với tổng số lượt vượt quá, biên độ đầu vào, độ nới giãn gia tăng liên tiếp và biên độ thời gian max. Các activity trục trặc tự động bắt tay thử lặp tuân theo quy tắc cho tới khi orchestrator phải dính thông báo trục trặc thực sự. Các hàm activity bắt buộc xây theo mô hình lũy đẳng (idempotent) để chịu đựng an toàn bước lặp lùi về (retry) — sử dụng thẻ ID (transaction ID) ngoài như một chứng minh riêng bên database để đánh dấu, vốn là cấu trúc nền quy chuẩn.

Liệu Dapr Workflow có chuẩn sẵn sàng để ra chiến trường (production-ready) chưa?

Dapr Workflow (nền móng từ Durable Task Framework) leo lên vị trí ổn định (stable status) trong bộ cài Dapr v1.12 (giữa năm 2024). Bộ Go SDK sở hữu sẵn hàm điều kiện lập trình Workflow APIs ở cấp stable rải từ v1.11. Để đem vận hành production cân nhắc kĩ chọn nền backend mượt mà (Redis Cluster hoặc Postgres thông qua dapr-workflow-backend) dùng trích xuất lưu giữ trạng thái, kèm việc kiểm tra lượng hao tốn RAM từ quy trình Dapr sidecar lúc cày mượt dưới sức ép ngốn tài nguyên cường độ nặng của chu trình này.


🤝 Kết nối với tôi

Bạn đang gặp phải những thách thức tương tự về kiến trúc hệ thống, mở rộng quy mô (scaling) hay dịch chuyển (migration)? Hãy kết nối với tôi trên LinkedIn, theo dõi GitHub của tôi, hoặc gửi một email để trao đổi nhé.