ADR-0003. adjustStock liền mạch với điều kiện chặn forceNonNegative
| Trường | Giá trị |
|---|---|
| Trạng thái | Accepted |
| Ngày | 2026-02-10 |
| Người quyết định | Phat Nguyen |
| Thay thế | - |
Bối cảnh
- Sự kiện thanh toán Sale thành công có thể gửi xuống inventory theo cơ chế at-least-once.
- Hai client (ví dụ POS + admin) có thể đồng thời cố trừ tồn kho cho cùng một item.
- Cách làm ngây thơ
SELECT current; UPDATE current - delta;là một race condition kinh điển khi có truy cập đồng thời. - Một số item phải chặn bán âm hoàn toàn (nguyên liệu thô với
allowOversell=false), số khác phải cho phép (sản phẩm cho đặt trước).
Quyết định
InventoryStockRepository.adjustStock({ stockId, adjustOnHand, adjustAvailable, adjustReserved, forceNonNegative }) thực hiện đọc và ghi trong một câu lệnh UPDATE SQL duy nhất với điều kiện chặn đặt ngay trong câu lệnh:
sql
UPDATE "InventoryStock"
SET quantity_on_hand = quantity_on_hand + $adjust_on_hand,
quantity_available = quantity_available + $adjust_available,
quantity_reserved = quantity_reserved + $adjust_reserved
WHERE id = $stock_id
AND ($forceNonNegative = false
OR (quantity_on_hand + $adjust_on_hand >= 0
AND quantity_available + $adjust_available >= 0
AND quantity_reserved + $adjust_reserved >= 0))
RETURNING quantity_on_hand, quantity_available, quantity_reserved;Khi forceNonNegative=true và bất kỳ số lượng nào sau khi điều chỉnh sẽ âm, câu UPDATE không khớp dòng nào; repository trả về null. Bên gọi ghi tracking note OVERSELL_BLOCKED và bỏ qua việc trừ kho một cách lặng lẽ.
Hệ quả
| Ưu điểm | Nhược điểm |
|---|---|
| An toàn trước mọi mức truy cập đồng thời | Bên gọi phải kiểm tra giá trị trả về null và xử lý |
| Một lần đi-về duy nhất tới DB | Không có lỗi chi tiết: chỉ "guard failed" |
| Hoạt động dưới serializable, read-committed, v.v. | SQL khó đọc hơn so với cấp ORM |
| Tương thích với PostgreSQL + Drizzle | Tính khả chuyển giữa các loại CSDL có chỗ tế nhị |
Phương án thay thế đã cân nhắc
| Phương án | Ưu điểm | Nhược điểm | Lý do từ chối |
|---|---|---|---|
Khóa lạc quan (cột version + thử lại) | Khả chuyển giữa các DB | Bão thử lại khi tranh chấp cao | Hiệu năng tệ với SKU nóng |
Khóa bi quan SELECT FOR UPDATE rồi UPDATE | Pattern quen thuộc | Gấp đôi lần đi-về; giữ khóa lâu hơn | Chậm hơn; cùng độ đúng |
| Mutex ở cấp ứng dụng (khóa Redis) | Đa CSDL | Rủi ro khóa cũ kẹt lại, có điểm hỏng đơn lẻ | Mong manh hơn primitive của DB |
Tham chiếu
core/src/repositories/inventory/inventory-stock.repository.ts:36-76(adjustStock)inventory/src/common/constants.ts:8(InventoryTrackingNotes.OVERSELL_BLOCKED)inventory/src/services/inventory-worker.service.ts(pattern bên gọi)