Cơ chế idempotency key (client sinh key duy nhất, server trả lại kết quả đã lưu khi gặp lại key) đã có ở câu riêng — ở đây là cách cài cụ thể cho nút "Đặt hàng":
- Key sinh lúc nào: một key cho mỗi phiên checkout (sinh khi render trang thanh toán, gửi qua header
Idempotency-Key) — nhờ đó double-click và retry timeout mang cùng một key. Sinh key mới mỗi lần bấm là vô hiệu hóa toàn bộ cơ chế. - Trọng tài là unique constraint trong DB, không phải logic ứng dụng: hai request đồng thời cùng key thì DB chỉ cho một dòng thắng.
sql
-- unique constraint on idempotency_key arbitrates concurrent retries
INSERT INTO orders (idempotency_key, cart_id, status)
VALUES ($1, $2, 'pending')
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
-- 0 rows returned → key already used → fetch and return the existing order- Lưu response cùng transaction với việc tạo đơn: lưu tách rời thì crash giữa chừng để lại key "mồ côi" không có kết quả, retry sau kẹt vĩnh viễn. Key cần TTL dọn dẹp (vd 24h).
- Request thứ hai đến khi request đầu còn đang chạy: chưa có response để trả → trả
409/425cho client chờ rồi retry, đừng xử lý song song. - Hàng rào cuối theo nghiệp vụ: partial unique index
(cart_id) WHERE status = 'pending'— chặn cả khi client sinh key sai. Disable nút sau click đầu chỉ là UX, không phải bảo vệ.
Lỗi hay gặp: chỉ chặn ở frontend; check "đã tồn tại chưa?" rồi mới insert theo hai bước tách rời (race condition) — hãy để unique constraint làm trọng tài.