Đây là trường hợp không thể phân biệt từ phía client: request hết thời gian chờ có thể là (a) server chưa nhận, (b) đã commit nhưng phản hồi mất trên đường về. Retry mù sẽ tạo bản ghi trùng; không retry thì mất giao dịch hợp lệ. Cách duy nhất đúng là làm thao tác idempotent.
1. Idempotency key do client sinh, database ép duy nhất:
CREATE UNIQUE INDEX ON payments (idempotency_key);
INSERT INTO payments (idempotency_key, order_id, amount)
VALUES ('req-8f21', 77, 500)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;Retry với cùng key: lần đầu chèn được, các lần sau không chèn thêm. Không trả về hàng nghĩa là đã xử lý trước đó — đọc lại kết quả cũ và trả về cho client.
2. Lưu cả phản hồi. Chỉ chặn trùng là chưa đủ; retry cần nhận lại đúng kết quả lần đầu (mã giao dịch, trạng thái). Lưu response vào cùng hàng đó trong cùng transaction.
3. Chuyển trạng thái có điều kiện thay vì gán mù. UPDATE orders SET status = 'paid' WHERE id = 77 AND status = 'pending' chạy lại lần hai sẽ không đổi gì.
4. Retry cần backoff + giới hạn số lần, và phải phân biệt lỗi đáng retry (mất kết nối, serialization failure) với lỗi không đáng (vi phạm ràng buộc, dữ liệu sai).
Một lưu ý về "khoá phân tán để chống trùng": khoá có thời hạn không đảm bảo loại trừ khi tiến trình bị dừng rồi chạy tiếp sau khi khoá hết hạn. Ràng buộc duy nhất trong database vẫn là chốt chặn cuối cùng đáng tin nhất.