ADR-0002. Khoá bi quan SELECT FOR UPDATE trên đơn khi thay đổi item
| Trường | Giá trị |
|---|---|
| Trạng thái | Accepted |
| Ngày | 2026-02-08 |
| Người quyết định | Phat Nguyen |
| Thay thế cho | - |
Bối cảnh
- Hai thiết bị (POS-A và POS-B) thường thêm cùng một item vào cùng một đơn DRAFT cùng lúc.
- Logic gộp tự nhiên - "nếu
(orderId, itemType, itemId)đã tồn tại thì tăng số lượng" - có khoảng tranh chấp: cả hai transaction đều thấy chưa có dòng nào, cả hai cùng chèn, và kết quả là item bị trùng. - Điều này từng xảy ra trên production khi một khách gọi "burger × 2" trên tablet A và "burger × 1" trên tablet B cùng lúc - kết quả là hai dòng burger riêng biệt thay vì một dòng số lượng 3.
Quyết định
Trong SaleOrderService.addSaleOrderItem, service mở một transaction và phát SELECT * FROM "SaleOrder" WHERE id = $1 FOR UPDATE trước khi đọc item rồi chèn/gộp. Dòng đơn đóng vai trò khoá điều phối cho TẤT CẢ thao tác ở cấp item trên đơn đó.
Cùng cách làm này áp dụng cho:
- Cập nhật hàng loạt item (
SaleOrderItemService.update) - Xoá sạch item (
SaleOrderService.clearOrderItems) - Thanh toán (
CheckoutService.checkout) - Gộp / tách
Hệ quả
| Ưu | Nhược |
|---|---|
| Gộp không tranh chấp: item trùng luôn được gộp một cách xác định | Khoá giữ tới khi transaction commit - thao tác chậm chặn người khác |
| Một cơ chế duy nhất cho mọi tranh chấp trên dòng đơn | Thao tác chỉ đọc không dùng chung khoá (chỉ thao tác ghi mới tuần tự hoá) |
| Tương thích với mô hình đồng thời của PostgreSQL | Thời gian chờ khoá hiện trên metrics DB - có thể gây cảnh báo nhầm |
| Không cần mutex ở cấp ứng dụng | Transaction kéo dài gây chờ dây chuyền |
Các phương án đã cân nhắc
| Phương án | Ưu | Nhược | Lý do từ chối |
|---|---|---|---|
Khoá lạc quan (cột version + thử lại) | Khả chuyển giữa các DB, không phải chờ | Bão thử lại khi nhiều thiết bị cùng thêm một item; logic phía client phức tạp | SKU bán chạy ngoài thực tế khiến việc thử lại tốn kém |
| Khoá Redis ở cấp ứng dụng | Dùng được liên DB | Rủi ro khoá treo; điểm hỏng đơn lẻ; tốn đồng bộ | Tệ hơn so với primitive sẵn có của DB |
Unique index trên (orderId, itemType, itemId) + ON CONFLICT INCREMENT | Chỉ cần DB | Không chạy được với item CUSTOM (mỗi lần thêm là một dòng riêng); không ghi được giảm giá ở cấp dòng | Không phù hợp ngữ nghĩa mode=CUSTOM |
Tham chiếu
sale/src/services/sale.service.ts(cách khoá trongaddSaleOrderItem)sale/src/services/sale-order-item.service.ts(khoá khi cập nhật hàng loạt)core/src/models/schemas/sale/sale-item/schema.ts(trườngmode)