Họ đang đo gì
Bạn có nghĩ tới trường hợp phản hồi bị mất không — khác hẳn với trường hợp yêu cầu bị mất.
Trả lời ngắn~30 giây
Client sinh một khoá duy nhất cho mỗi ý định (thường là UUID) và gửi kèm trong header Idempotency-Key. Server lưu khoá đó cùng kết quả trong một bảng có ràng buộc UNIQUE; nếu khoá đã tồn tại thì trả lại đúng phản hồi cũ thay vì thực hiện lần nữa. Điểm mấu chốt là ghi khoá và thực hiện nghiệp vụ phải nằm trong CÙNG một transaction, nếu không bạn chỉ thu hẹp cửa sổ đua chứ không đóng nó.
Giải thích sâu
Phần khó nằm ở chỗ ai sinh khoá. Nếu server sinh thì nó vô dụng — mỗi lần retry là một yêu cầu mới với khoá mới. Khoá phải do CLIENT sinh, gắn với ý định của người dùng (một lần bấm nút), và giữ nguyên qua mọi lần thử lại. Đây là điều thường phải viết rõ trong tài liệu API, vì client không tự nghĩ ra.
Trạng thái trung gian cũng cần xử lý: hai yêu cầu cùng khoá tới CÙNG LÚC. Cách thường dùng là chèn hàng khoá với trạng thái in_progress ngay từ đầu; nếu chèn thất bại vì trùng thì hoặc chờ ngắn rồi đọc kết quả, hoặc trả 409 để client thử lại. Bỏ qua trường hợp này là lý do các hệ thống “đã có idempotency” vẫn tạo đơn đôi khi tải cao.
Cuối cùng là thời hạn. Giữ khoá vĩnh viễn thì bảng phình vô hạn; xoá quá sớm thì một client retry muộn tạo bản ghi trùng. 24 giờ là mốc thường gặp (Stripe dùng 24 giờ), và nên gắn thêm hash của body để phát hiện trường hợp client dùng lại khoá cho một nội dung khác — lúc đó trả 422 thay vì âm thầm trả kết quả cũ.
CREATE TABLE idempotency (
key text PRIMARY KEY,
request_hash text NOT NULL,
status text NOT NULL, -- in_progress | done
response jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Cùng MỘT transaction với việc tạo đơn:
BEGIN;
INSERT INTO idempotency (key, request_hash, status)
VALUES ($1, $2, 'in_progress'); -- trùng khoá -> lỗi, xử lý ở tầng trên
INSERT INTO orders (…) VALUES (…);
UPDATE idempotency SET status = 'done', response = $3 WHERE key = $1;
COMMIT;Câu hỏi tiếp theo họ sẽ hỏi
?Vì sao không dùng PUT cho tạo mới?
Được, khi client biết trước định danh: PUT /orders/{uuid-do-client-sinh} là idempotent theo đúng đặc tả, không cần header nào. Nó chỉ không dùng được khi id do server sinh, hoặc khi nghiệp vụ có tác dụng phụ bên ngoài như trừ tiền.
?Bên thứ ba không hỗ trợ idempotency key thì sao?
Thì bạn phải tự đối chiếu: lưu ý định trước khi gọi, sau đó truy vấn lại bên kia bằng tham chiếu của mình để xem đã thực hiện chưa trước khi thử lại. Chậm hơn và phức tạp hơn, nhưng đó là cách duy nhất khi không kiểm soát được đầu bên kia.
Trả lời thế này là mất điểm
- “Kiểm tra xem đơn đã tồn tại chưa rồi mới tạo.” Đó là read-modify-write không có khoá — chính là race condition bạn đang cố sửa.
- Lưu khoá ở Redis mà không lưu kết quả. Retry sẽ nhận 409 thay vì nhận lại đơn đã tạo, và client không có cách nào lấy được id.