← Chương trước: Phần 1 — Order Fulfillment | Mục lục Series | Chương tiếp theo: Phần 3 — Thuật toán phân bổ →


Answer-first: Quản lý tồn kho thời gian thực phân tách 4 trạng thái cốt lõi: Physical Stock, Reserved Stock, Safety Stock và ATP (Available to Promise). Bằng cách kết hợp Stock Reservation có thời hạn (TTL), Optimistic/Pessimistic Locking và kiến trúc Eventual Consistency, hệ thống ngăn chặn triệt để hiện tượng overselling trong các đợt flash sale tốc độ cao.


Tại sao tồn kho là bài toán khó?

Tưởng tượng: kho còn 1 chiếc iPhone cuối cùng. Lúc 14:00:00.000, hai khách hàng ở hai thành phố khác nhau cùng bấm “Mua” đồng thời. Nếu hệ thống không xử lý đúng, cả hai đều được xác nhận → overselling → một khách sẽ bị hủy đơn → trải nghiệm tệ, mất khách.

Đặc biệt trong môi trường sản xuất 2026, với sự phát triển của Livestream Shopping và Flash Sales tốc độ cao, cùng sự tích hợp IoT và AI dự báo nhu cầu đa kênh (Omnichannel), áp lực xử lý tồn kho thời gian thực càng trở nên khắc nghiệt. Việc từ bỏ mô hình đồng bộ batch (cuối ngày) để chuyển sang cập nhật stream tức thời không còn là lựa chọn mà là yêu cầu bắt buộc để giữ thị phần. Đội ngũ kho bãi cũng dần thay thế sức người bằng các xe tự hành (AMR) để theo kịp tốc độ của Allocation Engine.


Mô hình tồn kho: 4 loại số lượng

Với SKU: "IPHONE-16-256GB" tại Kho TP.HCM:

┌──────────────────────────────────────────────────────────┐
│  Physical Stock (Tồn kho vật lý):        100             │
│  ├── Reserved (Đã giữ cho đơn chưa giao): -15           │
│  ├── Safety Stock (Dự phòng tối thiểu):   -5            │
│  │                                                        │
│  └── Available to Sell (ATP):             80             │
│      (= Physical - Reserved - Safety)                     │
│                                                           │
│  On-Order (Hàng đang nhập về):            50             │
│  Available to Promise (Tương lai):        130            │
│  (= ATP + On-Order)                                      │
└──────────────────────────────────────────────────────────┘
LoạiÝ nghĩaAi dùng?
Physical StockSố hàng thực tế trong kho (đếm được)Warehouse team
ReservedHàng đã “giữ chỗ” cho đơn hàng đang xử lý nhưng chưa xuất khoAllocation Engine
Safety StockDự phòng tối thiểu — không bao giờ bán (phòng sai sót, hàng lỗi)Inventory Manager
ATP (Available to Sell)Số hàng thực sự có thể bán ngayStorefront (hiển thị cho khách)

Stock Reservation — Chống overselling

Luồng Reserve Stock

Khách bấm "Mua":

1. Order Service → Inventory Service: "Reserve 1 IPHONE-16 tại kho HCM"

2. Inventory Service:
   BEGIN TRANSACTION
     SELECT atp FROM inventory
       WHERE sku = 'IPHONE-16' AND warehouse = 'HCM'
       FOR UPDATE;  -- Pessimistic lock

     IF atp >= 1:
       UPDATE inventory SET reserved = reserved + 1 WHERE ...
       INSERT INTO reservations (order_id, sku, qty, expires_at)
         VALUES ('ORD-001', 'IPHONE-16', 1, NOW() + INTERVAL '30 minutes');
       RETURN: RESERVED
     ELSE:
       RETURN: OUT_OF_STOCK
   COMMIT

3. Nếu RESERVED: tiếp tục thanh toán
   Nếu OUT_OF_STOCK: thông báo hết hàng

Reservation Expiry — Tự hủy giữ chỗ

Nếu khách giữ hàng nhưng không thanh toán trong 30 phút, reservation tự động hết hạn:

-- Cron job chạy mỗi phút
UPDATE inventory
SET reserved = reserved - r.qty
FROM reservations r
WHERE r.sku = inventory.sku
  AND r.warehouse = inventory.warehouse
  AND r.status = 'ACTIVE'
  AND r.expires_at < NOW();

UPDATE reservations
SET status = 'EXPIRED'
WHERE status = 'ACTIVE'
  AND expires_at < NOW();

Database Schema cho Inventory

-- Bảng tồn kho chính
CREATE TABLE inventory (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    sku             VARCHAR(50) NOT NULL,
    warehouse_id    VARCHAR(20) NOT NULL,
    physical_qty    INT NOT NULL DEFAULT 0,
    reserved_qty    INT NOT NULL DEFAULT 0,
    safety_stock    INT NOT NULL DEFAULT 0,

    -- Available to Sell = physical - reserved - safety
    -- Computed column hoặc tính runtime
    
    version         BIGINT NOT NULL DEFAULT 1,  -- Optimistic locking
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE(sku, warehouse_id),
    CONSTRAINT positive_physical CHECK (physical_qty >= 0),
    CONSTRAINT positive_reserved CHECK (reserved_qty >= 0),
    CONSTRAINT no_oversell CHECK (physical_qty - reserved_qty - safety_stock >= 0)
);

-- Bảng reservation (giữ chỗ)
CREATE TABLE stock_reservations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id        VARCHAR(50) NOT NULL,
    sku             VARCHAR(50) NOT NULL,
    warehouse_id    VARCHAR(20) NOT NULL,
    quantity        INT NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    -- 'ACTIVE', 'COMMITTED' (đã xuất kho), 'EXPIRED', 'CANCELLED'
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,
    committed_at    TIMESTAMPTZ
);

CREATE INDEX idx_reservation_expiry ON stock_reservations(status, expires_at)
    WHERE status = 'ACTIVE';

Eventual Consistency trong tồn kho phân tán

Khi hệ thống có nhiều kho và nhiều service cùng đọc/ghi tồn kho, không thể dùng strong consistency (quá chậm). Giải pháp: Eventual Consistency với event-driven updates:

Luồng Event-Driven:

1. Order Service: "Đã confirm đơn ORD-001"
   → Publish event: order.confirmed {sku: IPHONE-16, qty: 1, warehouse: HCM}

2. Inventory Service (Consumer):
   → Nhận event → reservation status: ACTIVE → COMMITTED
   → physical_qty -= 1, reserved_qty -= 1

3. Warehouse Service (Consumer):
   → Nhận event → Tạo pick list cho nhân viên kho

4. Analytics Service (Consumer):
   → Nhận event → Cập nhật dashboard doanh số

Mỗi service cập nhật state riêng, async, eventually consistent.

Xử lý xung đột: Optimistic Locking

// Optimistic locking: kiểm tra version trước khi update
func (r *InventoryRepo) Reserve(ctx context.Context, sku, warehouse string, qty int) error {
    for retries := 0; retries < 3; retries++ {
        inv, err := r.GetBySKU(ctx, sku, warehouse)
        if err != nil { return err }

        atp := inv.PhysicalQty - inv.ReservedQty - inv.SafetyStock
        if atp < qty {
            return ErrOutOfStock
        }

        // Update with version check
        result := r.db.Exec(`
            UPDATE inventory
            SET reserved_qty = reserved_qty + ?, version = version + 1
            WHERE sku = ? AND warehouse_id = ? AND version = ?`,
            qty, sku, warehouse, inv.Version)

        if result.RowsAffected == 1 {
            return nil // Success
        }
        // Version mismatch → ai đó đã update trước → retry
    }
    return ErrConcurrencyConflict
}

Inventory Sync giữa nhiều kênh bán

Khi bán trên cả Shopee, Lazada, và website riêng, cùng một SKU:

         ┌──────────┐
         │ Inventory │  ← Source of truth
         │ Service   │
         └─────┬─────┘
               │
    ┌──────────┼──────────┐
    ▼          ▼          ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Shopee │ │ Lazada │ │Website │
│ Channel│ │Channel │ │Channel │
│ ATP: 80│ │ATP: 80 │ │ATP: 80 │
└────────┘ └────────┘ └────────┘

Vấn đề: Nếu Shopee bán 1, phải giảm ATP ở Lazada và Website ngay
→ Giải pháp: Event-driven sync hoặc Channel-specific allocation

Channel Allocation:
  Shopee: max 40 (50%)
  Lazada: max 24 (30%)
  Website: max 16 (20%)
  → Mỗi kênh có "quota" riêng → Không xung đột

Metrics quan trọng

MetricÝ nghĩaMục tiêu
Stockout Rate% thời gian SKU hết hàng< 2%
Oversell Rate% đơn bị hủy do bán vượt tồn< 0.1%
Inventory TurnoverSố lần xoay vòng hàng tồn/năm> 12 (monthly)
Reservation Expiry Rate% reservation bị expire (khách không thanh toán)< 15%
ATP AccuracyĐộ chính xác của số hàng có thể bán> 99.5%

← Chương trước: Phần 1 — Order Fulfillment | Mục lục Series | Chương tiếp theo: Phần 3 — Thuật toán phân bổ →


❓ Câu Hỏi Thường Gặp (FAQ)

Q1: Phần 2 — Inventory Management: Quản lý tồn kho thời gian thực giải quyết vấn đề cốt lõi nào trong kiến trúc hệ thống?

Hệ thống tồn kho thời gian thực là nền tảng cho mọi quyết định phân bổ. Tìm hiểu cách xử lý overselling, stock reservation, và eventual consistency trong tồn kho lớn.

Q2: Những lưu ý quan trọng nhất khi triển khai thực tế là gì?

Cần chú trọng phân tầng ranh giới trách nhiệm (bounded context), thiết lập cơ chế fallback dự phòng, và giám sát chặt chẽ qua metrics OpenTelemetry để phát hiện sớm các điểm nghẽn.

Q3: Làm sao để kiểm thử và đánh giá hiệu quả sau khi áp dụng?

Áp dụng kiểm thử tải (load test), benchmark độ trễ P95/P99 trước và sau triển khai, kết hợp tracing phân tán để xác minh tính ổn định dưới tải cao.