Việc mở rộng quy mô (scale) một nền tảng thương mại điện tử vượt qua cột mốc 10.000+ đơn hàng mỗi ngày, với mỗi đơn chứa nhiều SKU trải dài qua nhiều kho hàng biến động là lúc mà các kiến trúc ngây thơ sẽ sụp đổ. Việc đập thêm tiền nâng cấp phần cứng không còn là viên đạn bạc khi hệ thống phải đối mặt với các giao dịch phân tán (distributed transactions), điều kiện tương tranh (race conditions), và tính nhất quán cuối (eventual consistency).

Trong bài viết phân tích kỹ thuật chuyên sâu này, chúng ta sẽ xé toạc các lý thuyết “Hello World” về Microservices. Chúng ta sẽ xem xét chính xác cách hệ sinh thái 21-service phân tán của chúng tôi tương tác dưới tầng ngầm như thế nào. Tôi sẽ chia sẻ chính xác các mô hình kiến trúc Golang (Kratos), sự điều phối Saga cho quá trình thanh toán (checkout) phân tán, và cách chúng tôi xử lý race conditions dưới tải trọng cực lớn.

1. Bức tranh Phân tán (The Distributed Landscape)

Microservices mà không có các giới hạn ngữ cảnh rõ ràng (bounded contexts) sẽ thoái hóa thành một “Distributed Monolith” (Kiến trúc nguyên khối nhưng bị phân tán) cực kỳ nặng nề về độ trễ (latency). Chúng tôi đã khoanh vùng hệ sinh thái của mình một cách lỏng lẻo quanh 5 domain cốt lõi, ưu tiên tuyệt đối sự cách ly dữ liệu theo nguyên tắc “mỗi service một database” (database-per-service):

graph TD
    API[API Gateway]
    API --> Checkout[Checkout Service]
    API --> Cart[Cart Service]
    
    subgraph "Dapr Event Mesh (Pub/Sub)"
        Checkout -- sự kiện checkout.requested --> Dapr[Redis / Dapr]
        Dapr --> Order[Order Service]
        Dapr --> Warehouse[Warehouse Service]
        Dapr --> Pricing[Pricing Service]
    end
    
    Warehouse -- sự kiện inventory.reserved --> Dapr
    Pricing -- sự kiện pricing.validated --> Dapr
    Order -- rollback checkout.failed --> Dapr

Biểu đồ trên tóm gọn luồng biến động nhất của hệ thống: The Checkout Saga. Khi một người dùng thanh toán, chúng ta không thể chỉ đơn giản mở một transaction SQL bao trùm 4 table được nữa. Checkout phải đồng bộ bất đồng bộ (asynchronously) với Pricing (để xác thực tổng tiền), Warehouse (để khóa tồn kho), và Order (để tạo ra tập hợp đơn hàng cuối cùng).

2. Ép buộc Kiến trúc Sạch (Clean Architecture) với Kratos

Để quản lý 21 codebase riêng biệt, sự nhất quán trong đội ngũ kỹ sư là bắt buộc. Chúng tôi sử dụng Kratos (v2) để ép buộc nghiêm ngặt Clean Architecture trong Golang. Bằng cách phân chia ranh giới vật lý, chúng tôi ngăn chặn việc logic database bị rò rỉ (bleeding) vào các handler của HTTP hoặc gRPC.

Dưới đây là một bản thiết kế Kratos chuẩn mực trông như thế nào trong hệ sinh thái của chúng tôi:

// internal/biz/order.go (Tầng Business Logic)
type OrderUsecase struct {
    repo OrderRepo
    log  *log.Helper
}

func (uc *OrderUsecase) CreateOrder(ctx context.Context, o *Order) error {
    if o.TotalAmount <= 0 {
        return v1.ErrorInvalidAmount("order amount must be positive")
    }
    // Tầng Biz HOÀN TOÀN KHÔNG BIẾT gì về PostgreSQL hay GORM
    return uc.repo.Save(ctx, o)
}

// internal/data/order.go (Tầng Data Persistence)
type orderRepo struct {
    data *Data
    log  *log.Helper
}

// Implement interface từ tầng Biz
func (r *orderRepo) Save(ctx context.Context, o *biz.Order) error {
    // Các database transactions được cách ly an toàn tại đây
    return r.data.db.WithContext(ctx).Create(o).Error
}

Chúng tôi liên kết các tầng này lại với nhau một cách linh hoạt bằng cách sử dụng Google Wire để thực hiện Dependency Injection ở thời điểm biên dịch (compile-time). Điều này cho phép các developer viết unit test với các repository được mock một cách dễ dàng, cách ly hoàn toàn lõi business khỏi các giao thức truyền tải (transport protocols).

3. Quái vật thực sự: Các Giao dịch Phân tán (Saga Pattern)

Lời khuyên chung chung nhất trong thế giới microservices là “Hãy dùng Pub/Sub”. Nhưng bạn xử lý thế nào khi Service A thành công nhưng Service B lại thất bại?

Trong hệ sinh thái của mình, chúng tôi đã triển khai Event-Choreography Saga Pattern sử dụng Dapr. Hãy cùng truy vết luồng ConfirmCheckout phức tạp:

  1. Checkout Service nhận request HTTP, xác thực giỏ hàng, và publish một sự kiện checkout.requested lên Dapr.
  2. Warehouse ServicePricing Service lắng nghe sự kiện này và xử lý độc lập dựa trên payload.

Xử lý Race Conditions trong Warehouse

Race conditions của kho hàng xảy ra khi hai request đến trong vòng chưa tới 1 giây cố gắng mua chiếc iPhone cuối cùng.

Nếu Warehouse Service chỉ đơn giản chạy SELECT stock FROM items WHERE id = ?, cả hai luồng (threads) chạy song song sẽ cùng thấy stock = 1, và cả hai sẽ cùng trừ đi 1, dẫn đến kết quả stock = -1.

Thay vào đó, service Warehouse của chúng tôi sử dụng Optimistic Concurrency Control (OCC) ngay tại tầng database:

// Optimistic Locking để ngăn chặn overselling (bán lố)
result := db.Exec(`
    UPDATE inventory 
    SET reserved_stock = reserved_stock + ?, version = version + 1 
    WHERE sku_id = ? 
      AND (total_stock - reserved_stock) >= ? 
      AND version = ?`, 
    qty, skuID, qty, currentVersion)

if result.RowsAffected == 0 {
    return ErrStockInsufficientOrRaceCondition
}

Nếu việc khóa (lock) thất bại do sự không khớp phiên bản tức thời, Warehouse sẽ publish một sự kiện inventory.reservation.failed.

Bước Rollback (Compensation)

Bởi vì state (trạng thái) đã bị phân tán, nếu Warehouse khóa thành công tồn kho nhưng Pricing báo cáo rằng voucher được áp dụng không hợp lệ, toàn bộ quá trình Saga phải bị hủy bỏ (abort).

Order Service thường đóng vai trò là điểm trũng (sink). Nếu nó thấy inventory.reservation.failed HOẶC pricing.validation.failed, nó sẽ bắn ra một sự kiện bù trừ (compensation event) khổng lồ: checkout.failed.

Các background workers (consumers) trong Warehouse Service sẽ bắt lấy sự kiện này và ngay lập tức kích hoạt Compensation Logic:

// Background Worker trả lại tồn kho đã reserve
func (w *WarehouseWorker) HandleCheckoutFailed(ctx context.Context, event CheckoutFailed) error {
    // Rollback lại số lượng tồn kho đã giữ bằng cách sử dụng transaction ID gốc
    return w.inventoryRepo.ReleaseReservedStock(ctx, event.TransactionID)
}

4. Thuần phục Eventual Consistency bằng Tính lũy đẳng (Idempotency)

Khi bạn phụ thuộc vào các sự kiện mạng (network events), việc mạng thực hiện retries chắc chắn sẽ xảy ra. Dapr đảm bảo cơ chế phân phối “At-Least-Once” (Ít nhất một lần delivery), nghĩa là Warehouse Service có thể sẽ nhận được cùng một sự kiện checkout.requested 2 lần nếu xảy ra lỗi timeout.

Để ngăn chặn việc khóa tồn kho 2 lần cho cùng một đơn, mọi Database trong hệ sinh thái của chúng tôi có liên quan đến các transactions đều sử dụng một Idempotency Key (Khóa Lũy đẳng).

CREATE TABLE processed_events (
    event_id VARCHAR(255) PRIMARY KEY,
    status VARCHAR(50),
    created_at TIMESTAMP
);

Trước khi xử lý một thông điệp Dapr mới tới, service mở một database transaction và cố gắng INSERT event_id đó vào. Nếu nó throw ra lỗi constraint violation (vi phạm khóa chính), có nghĩa là sự kiện này đã được xử lý rồi, và hệ thống sẽ an toàn ack (xác nhận) và drop thông điệp trùng lặp đó.

Kết luận

Việc migrate một hệ thống Monolith thương mại điện tử sang một hệ sinh thái 21-service không đơn giản chỉ là dựng lên một cái API Gateway rồi nghỉ khỏe. Nghề kỹ sư thực thụ mới thực sự bắt đầu khi bạn chạm tới các ranh giới (edges): rollback lại các đợt checkout đang dang dở một cách mượt mà, ngăn chặn database locks dưới tải trọng đồng thời cực lớn, và ép buộc các ranh giới domain nghiêm ngặt để codebase vẫn có thể đọc hiểu được.

Bằng cách ánh xạ (mapping) các contexts một cách tỉ mỉ, ép buộc sự tách biệt khắt khe thông qua Kratos, và sử dụng các Idempotent Saga patterns trên Dapr, chúng tôi đã tạo ra một hệ thống có khả năng hấp thụ các đợt bùng nổ traffic khổng lồ dịp Black Friday mà không làm rơi rớt một đơn hàng nào. Những sự phức tạp ban đầu của distributed state (trạng thái phân tán) khá đau đớn, nhưng khả năng scale và sự cách ly cho developer có được là một sự đầu tư hoàn toàn xứng đáng.


🤝 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é.