Chốt cơ chế trước: client gửi kèm một idempotency key duy nhất cho mỗi ý định đặt hàng; lần đầu server tạo đơn rồi lưu kết quả theo key, gặp lại đúng key thì trả lại kết quả cũ thay vì tạo đơn mới. Cách cài 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 hoá toàn bộ cơ chế. - Chốt chặn 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 trên idempotency_key phân xử các retry chạy song song
INSERT INTO orders (idempotency_key, cart_id, status)
VALUES ($1, $2, 'pending')
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
-- trả về 0 dòng → key đã dùng → lấy và trả lại order đã có- 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 quyết định.