Wavi Books · System Design, Backend & Microservices
Idempotency là gì? Thiết kế API retry an toàn, không tạo dữ liệu trùng
Nguyễn Minh Trí · Biên tập chuyên môn Wavi Books

Idempotency là gì trong API? Cách dùng Idempotency-Key, chống tạo đơn trùng, xử lý retry, race condition và đo độ tin cậy cho backend.
## Idempotency là gì trong API?
Idempotency là tính chất cho phép bạn gửi lại cùng một thao tác mà kết quả nghiệp vụ không bị nhân đôi sau lần xử lý đầu tiên. Trong backend, mục tiêu không phải là làm cho mọi request đều thành công; mục tiêu là khi mạng chập chờn, người dùng bấm lại nút thanh toán hoặc worker retry, hệ thống không tạo thêm đơn hàng, không trừ tiền hai lần và không gửi hai email xác nhận.
Đây là một vấn đề rất thực tế: client có thể timeout sau khi server đã ghi dữ liệu thành công. Nếu client chỉ nhìn thấy lỗi mạng rồi gửi lại POST, server phải phân biệt được đây là lần thử lại của cùng một ý định hay một yêu cầu mới. Idempotency biến quyết định đó thành một phần có chủ đích của hợp đồng API thay vì hy vọng retry sẽ không xảy ra.
## Đừng nhầm HTTP method với an toàn nghiệp vụ
GET thường được xem là idempotent vì đọc lại tài nguyên không nên thay đổi trạng thái. PUT và DELETE có ngữ nghĩa idempotent trong HTTP khi lặp cùng trạng thái mong muốn. Nhưng API nghiệp vụ vẫn có thể tạo tác dụng phụ: một DELETE có thể kích hoạt webhook, còn một PUT có thể gửi thông báo. Vì vậy bạn cần xác định rõ điều gì được bảo vệ: trạng thái database, giao dịch thanh toán, email, message queue hay toàn bộ workflow.
POST tạo đơn hàng, nạp tiền hoặc gửi lệnh giao hàng thường không idempotent nếu chỉ gọi lại endpoint với cùng payload. Đó là lúc một idempotency key có ích: client sinh một khóa cho một ý định nghiệp vụ, giữ nguyên khóa khi retry, còn server lưu dấu vết kết quả của lần xử lý đầu tiên.
## Idempotency-Key hoạt động như thế nào
Một thiết kế phổ biến là client gửi header `Idempotency-Key` cùng POST. Server tạo bản ghi gồm khóa, phạm vi endpoint hoặc tenant, dấu vân tay payload, trạng thái đang xử lý hoặc đã hoàn tất, mã phản hồi và body phản hồi. Khi gặp lại đúng khóa với đúng phạm vi, server trả kết quả đã lưu thay vì chạy tác dụng phụ lần nữa.
Khóa phải gắn với một hành động logic, không gắn với từng lần gửi HTTP. Ví dụ, nút “Tạo đơn” có thể sinh UUID khi người dùng bắt đầu gửi biểu mẫu; nếu ứng dụng retry vì timeout thì dùng lại UUID đó. Một lần đặt đơn mới phải có khóa mới. Bản nháp IETF về `Idempotency-Key` cũng nhấn mạnh khóa không được tái sử dụng cho payload khác; tài liệu Stripe khuyến nghị khóa ngẫu nhiên đủ entropy như UUID v4.
## Ví dụ: tạo đơn hàng và thanh toán
Giả sử ứng dụng gọi `POST /orders` với khóa `a1b2`. Service kiểm tra bảng idempotency trước khi tạo order. Nếu chưa có khóa, service ghi bản ghi trạng thái `processing` và tạo order trong transaction phù hợp. Sau khi thành công, service lưu `completed`, orderId và response. Lần retry cùng khóa sẽ nhận lại orderId cũ; người dùng thấy cùng một kết quả thay vì hai đơn.
Nếu thao tác còn gọi cổng thanh toán hoặc publish event, database transaction một mình chưa đủ. Bạn cần một chiến lược như outbox: ghi order và event cần phát trong cùng transaction, sau đó worker phát event có cơ chế chống trùng ở consumer. Không có một “exactly once” thần kỳ xuyên mọi hệ thống; điều đáng tin cậy hơn là kết hợp at-least-once delivery với consumer idempotent và khả năng đối soát.
## Xử lý request đang chạy và race condition
Hai retry có thể đến gần như cùng lúc. Nếu cả hai cùng thấy chưa có khóa rồi đều tạo order, idempotency thất bại. Hãy đặt unique constraint cho phạm vi khóa, ví dụ `(tenant_id, endpoint, idempotency_key)`, và coi lỗi tranh chấp là một nhánh bình thường. Request đến sau có thể chờ ngắn để đọc kết quả, hoặc nhận phản hồi rõ ràng rằng thao tác đang được xử lý để client retry có kiểm soát.
Bạn cũng cần phân biệt cùng key nhưng payload khác. Im lặng trả lại kết quả cũ có thể che lỗi client; xử lý lại payload mới có thể phá cam kết. Một phản hồi 4xx với thông điệp nhất quán, kèm dấu vân tay payload đã lưu, giúp đội tích hợp phát hiện việc tái sử dụng key sai mục đích.
## Chọn thời gian lưu khóa theo rủi ro
Giữ khóa quá ngắn có thể khiến retry muộn tạo lại dữ liệu; giữ vô hạn làm bảng tăng vô ích và khiến key cũ che một nghiệp vụ mới. Không có con số chung cho mọi hệ thống. Hãy bắt đầu từ cửa sổ retry thực tế, độ trễ queue, thời gian người dùng có thể gửi lại thao tác và nghĩa vụ đối soát của nghiệp vụ. Thanh toán hoặc tạo đơn thường cần cửa sổ dài hơn một thao tác nội bộ có thể chạy lại dễ dàng.
Khi dọn dữ liệu, đừng chỉ xóa key. Hãy bảo đảm dữ liệu nghiệp vụ có unique key phù hợp, như mã order do server tạo hoặc external reference, để một lỗi vận hành sau thời hạn idempotency vẫn không phá tính đúng đắn cốt lõi.
## Retry đúng cách: giới hạn, backoff và quan sát
Idempotency không phải giấy phép retry vô hạn. Client nên retry những lỗi tạm thời như timeout, 429 hoặc 503 theo chính sách của API, tôn trọng `Retry-After` nếu có, dùng exponential backoff kèm jitter và giới hạn số lần thử. Với lỗi validation hoặc thiếu quyền, retry y nguyên thường không có ích; người gọi cần sửa request hoặc xin lại quyền.
Mỗi request nên có correlation ID hoặc request ID riêng, bên cạnh idempotency key. Idempotency key trả lời “đây có phải cùng ý định không?”, còn correlation ID giúp bạn lần theo mọi bước của một lượt thực thi qua gateway, service, queue và worker. Đừng ghi raw key hay dữ liệu nhạy cảm vào log công khai nếu key có thể liên hệ tới người dùng.
## Những sai lầm thường gặp
Sai lầm đầu tiên là chỉ cache HTTP response trong memory. Cách này mất tác dụng khi deploy lại, chạy nhiều instance hoặc retry đi qua worker khác. Sai lầm thứ hai là chỉ deduplicate ở client; client crash hoặc có hai thiết bị vẫn làm server nhận các request trùng. Sai lầm thứ ba là lưu key nhưng không ràng buộc payload, dẫn đến một key cũ vô tình được dùng cho hành động khác.
Một sai lầm khác là đặt idempotency vào mọi endpoint mà không mô tả hành vi. API consumer cần biết endpoint nào yêu cầu key, phạm vi key là gì, key sống bao lâu, response nào có thể replay và xử lý thế nào khi request cũ vẫn `processing`. Đây là tài liệu sản phẩm của API, không phải chi tiết nội bộ có thể bỏ qua.
## Trade-off: đơn giản, chi phí và độ đúng
Idempotency thêm bảng trạng thái, unique index, logic cleanup, kiểm thử concurrency và tình huống response replay. Đổi lại, nó giảm rủi ro lỗi đắt tiền ở các thao tác có tác dụng phụ. Với endpoint đọc dữ liệu hoặc job có thể tính lại an toàn, chi phí này có thể không đáng. Với tạo đơn, webhook, cấp quyền hoặc ghi dữ liệu tài chính, chi phí thiết kế thường nhỏ hơn chi phí xử lý sự cố.
Bạn không cần biến toàn bộ hệ thống thành distributed transaction. Hãy bảo vệ trước những điểm biên nơi retry có thể biến một ý định thành nhiều tác dụng phụ, rồi mở rộng bằng outbox, consumer deduplication và reconciliation khi workflow đi qua nhiều dịch vụ.
## Cách đo idempotency có đang giúp hay không
Theo dõi tỷ lệ request replay theo idempotency key, số key bị tái sử dụng với payload khác, thời gian request ở trạng thái `processing`, số unique-constraint conflict và số bản ghi phải đối soát thủ công. Với đơn hàng, bạn cũng nên đối chiếu số lần gọi tạo đơn, số order được tạo và số giao dịch thanh toán được ghi nhận theo external reference.
Nếu tỷ lệ retry tăng, đừng chỉ tăng thời gian lưu key. Hãy xem latency, timeout ở gateway, lỗi mạng, hàng đợi và UX nút gửi có đang khuyến khích người dùng bấm lặp không. Chỉ số tốt nhất là số tác dụng phụ trùng thực tế giảm mà không làm request hợp lệ bị chặn nhầm.
## Bài tập thực hành cho backend developer
Hãy xây endpoint tạo booking nhỏ với PostgreSQL hoặc database bạn đang dùng. Thêm `Idempotency-Key`, unique constraint theo user và endpoint, lưu hash payload, mô phỏng timeout sau khi commit, rồi gửi lại cùng request bằng đúng key. Tiếp theo, chạy hai request song song cùng key và thử dùng lại key với payload khác. Cuối cùng, viết test chứng minh chỉ có một booking được tạo và log cho thấy request thứ hai là replay.
Sau bài tập này, bạn có thể nối kiến thức với bài về REST API, Event-Driven Architecture và Microservices trên Wavi Books. Các sách liên quan trong bài phù hợp để đào sâu thiết kế giao dịch, message delivery và trade-off của hệ thống phân tán.
## Nguồn kỹ thuật để bạn kiểm chứng
Tài liệu Stripe về Idempotent requests mô tả cách dùng khóa để retry an toàn và cách phản hồi được lưu theo khóa. Bản Internet-Draft IETF về `Idempotency-Key` nêu cú pháp, tính duy nhất, thời hạn và các lỗi khi thiếu hoặc tái sử dụng khóa. Khi áp dụng, hãy ưu tiên hợp đồng API và hành vi thực tế của nền tảng bạn dùng thay vì sao chép nguyên một chính sách thời hạn từ ví dụ khác. Nguồn: https://docs.stripe.com/api/idempotent_requests và https://www.ietf.org/archive/id/draft-ietf-httpapi-idempotency-key-header-07.html
Câu hỏi thường gặp
Idempotency là gì, có phải request nào cũng nên dùng không?
Idempotency giúp gửi lại cùng một ý định mà không nhân đôi tác dụng phụ. Không cần thêm cho mọi request; hãy ưu tiên thao tác tạo dữ liệu, thanh toán, webhook hoặc job dễ bị retry và gây hậu quả khi lặp.
Idempotency-Key khác request ID như thế nào?
Idempotency-Key nhận diện một ý định nghiệp vụ để server replay kết quả thay vì chạy lại. Request ID hoặc correlation ID nhận diện một lượt xử lý để truy vết log; một ý định có thể có nhiều request ID khi retry.
Có Idempotency-Key thì đã đảm bảo exactly once chưa?
Chưa. Khóa bảo vệ một phạm vi API đã định nghĩa. Workflow qua database, payment gateway và queue vẫn cần transaction phù hợp, outbox, consumer chống trùng và đối soát để kiểm soát tác dụng phụ ở từng biên.