Idempotency là gì? Retry an toàn và tránh xử lý trùng lặp
Trả lời nhanh: Idempotency là gì? Đó là thuộc tính cho phép cùng một yêu cầu được áp dụng nhiều lần nhưng hiệu ứng quan sát được tương đương một lần. Trong hệ thống phân tán, idempotency giúp retry sau timeout mà không tạo hai đơn, trừ tiền hai lần hoặc gửi trùng. Nó thường cần idempotency key, lưu trạng thái và cập nhật nguyên tử.
Cập nhật lần cuối: 20/08/2026
Khách hàng bấm “Thanh toán”. Dịch vụ thanh toán đã trừ tiền nhưng response bị mất do mạng ngắt. Ứng dụng chỉ thấy timeout.
Nếu không retry, khách có thể đã trả tiền nhưng đơn chưa được xác nhận. Nếu retry mù quáng, khách có thể bị trừ tiền lần hai. Đây là kết quả không xác định: phía gọi không biết thao tác đầu thất bại hay chỉ mất phản hồi.
Hệ thống phân tán không thể loại bỏ hoàn toàn timeout, retry và giao hàng lặp. Cách bền vững là làm cho lặp lại trở nên an toàn.
Idempotency là gì?
Idempotency là thuộc tính trong đó thực hiện cùng một thao tác nhiều lần tạo ra hiệu ứng nghiệp vụ tương đương thực hiện một lần.
Nếu f là thao tác, trực giác toán học là:
f(f(x)) = f(x)
Trong API, điều này không có nghĩa mọi response phải giống từng byte hoặc log chỉ xuất hiện một lần. Server có thể ghi thêm access log, metric hay thời gian xử lý. Điều cần giữ là hiệu ứng mà client yêu cầu, ví dụ chỉ có một payment hoặc một order được tạo cho cùng ý định.
RFC 9110 định nghĩa một HTTP method là idempotent khi hiệu ứng dự kiến của nhiều request giống hệt nhau tương đương hiệu ứng của một request. PUT, DELETE và các safe method được định nghĩa là idempotent theo semantics, nhưng implementation vẫn phải đúng.
Idempotent tự nhiên và idempotent được thiết kế khác nhau thế nào?
Idempotent tự nhiên
Đặt trạng thái về một giá trị cố định thường idempotent:
PUT /users/123/preferences
{"newsletter": false}
Gửi lại nhiều lần vẫn để newsletter = false.
Không idempotent tự nhiên
Tăng một biến hoặc tạo tài nguyên mới thường không idempotent:
POST /wallets/123/credit
{"amount": 100000}
Nếu thực thi hai lần, số dư tăng hai lần.
Idempotent được thiết kế
Ta có thể làm request credit trở nên an toàn bằng một idempotency key đại diện cho ý định nghiệp vụ duy nhất:
POST /wallets/123/credit
Idempotency-Key: topup-order-8742
{"amount": 100000, "currency": "VND"}
Server ghi nhận key, payload và kết quả. Lần lặp với cùng key trả lại kết quả đã có thay vì cộng tiền lần nữa.
At-most-once, at-least-once và exactly-once là gì?
| Semantics | Điều có thể xảy ra | Đổi lại |
|---|---|---|
| At-most-once | Mất message, không xử lý lặp | Ưu tiên không trùng |
| At-least-once | Message được xử lý, có thể lặp | Ưu tiên không mất |
| Exactly-once | Hiệu ứng xuất hiện đúng một lần trong phạm vi cam kết | Cần coordination và giới hạn rõ |
At-most-once
Consumer đánh dấu message trước khi xử lý. Nếu crash sau khi đánh dấu nhưng trước khi hoàn tất, message mất. Phù hợp với telemetry ít quan trọng hơn là thanh toán.
At-least-once
Consumer chỉ acknowledge sau khi xử lý. Nếu xử lý xong rồi crash trước ack, broker gửi lại và consumer thấy duplicate. Đây là lựa chọn phổ biến khi có thể làm handler idempotent.
Exactly-once
Tên gọi hấp dẫn nhưng phải đọc phạm vi. Tài liệu thiết kế Kafka nhấn mạnh delivery guarantee tách thành việc publish và consume, đồng thời cảnh báo cần đọc kỹ tuyên bố exactly-once. Kafka có transaction và exactly-once processing trong phạm vi cụ thể của hệ sinh thái, nhưng side effect ở database, email hoặc API ngoài vẫn cần phối hợp.
Một cách nói chính xác hơn là exactly-once effect trong một ranh giới đã định, không phải “message chỉ tồn tại vật lý một lần ở mọi nơi”.
Duplicate đi vào hệ thống từ đâu?
Producer tạo trùng
- người dùng bấm hai lần;
- frontend retry;
- job scheduler chạy lại;
- producer crash sau send nhưng trước khi lưu trạng thái;
- hai instance cùng xử lý một schedule.
Broker giao lại
- acknowledgement bị mất;
- visibility timeout hết;
- rebalance consumer group;
- broker hoặc network retry;
- consumer xử lý lâu hơn lease.
Consumer tạo side effect trùng
- ghi database xong nhưng crash trước commit offset;
- gọi API ngoài xong nhưng chưa lưu kết quả;
- gửi email rồi retry toàn message;
- handler không có unique constraint;
- nhiều worker xử lý cùng key đồng thời.
Dedup ở producer không giải quyết redelivery của broker. Exactly-once trong broker cũng không tự bảo vệ một API ngoài. Cần thiết kế từng đoạn end-to-end.
Idempotency key cần những thuộc tính nào?
Một idempotency key tốt cần:
- duy nhất theo ý định nghiệp vụ, không theo lần retry;
- ổn định qua retry, client dùng lại đúng key;
- khó va chạm, thường dùng UUID hoặc business ID đủ scope;
- có namespace, ví dụ tenant + endpoint + key;
- gắn với request fingerprint, ngăn tái dùng key cho payload khác;
- có vòng đời rõ, đủ dài cho cửa sổ retry và replay;
- không chứa secret hoặc PII nếu đi qua log và proxy.
Key không nên được server tạo sau khi request tới, vì client cần gửi lại cùng key khi không nhận response. Với workflow do server khởi tạo, business event ID có thể đóng vai trò key.
Server xử lý idempotency key thế nào?
Một bảng đơn giản có thể gồm:
| Trường | Ý nghĩa |
|---|---|
| scope | tenant và loại operation |
| key | idempotency key từ client |
| request_hash | fingerprint của payload chuẩn hóa |
| status | processing, succeeded, failed |
| resource_id | tài nguyên đã tạo |
| response_code | HTTP status cần replay |
| response_body | kết quả hoặc tham chiếu kết quả |
| created_at | thời điểm tạo |
| expires_at | thời điểm được phép dọn |
Luồng xử lý:
- nhận key và chuẩn hóa request;
- tạo
request_hash; - insert bản ghi idempotency với unique constraint;
- nếu insert thành công, giành quyền xử lý;
- thực hiện nghiệp vụ trong transaction phù hợp;
- lưu kết quả và trạng thái;
- nếu key đã tồn tại, so hash rồi trả kết quả cũ hoặc trạng thái đang xử lý.
Điểm khó là bước ghi key và side effect phải nguyên tử trong cùng ranh giới, hoặc có cơ chế khôi phục. Nếu tạo order xong nhưng chưa đánh dấu key, retry vẫn có thể tạo order thứ hai.
Vì sao request fingerprint cần thiết?
Giả sử client gửi:
key = checkout-999
amount = 500000
Sau đó do bug gửi lại cùng key nhưng amount = 900000. Server không nên trả kết quả cũ như thể hai request giống nhau, cũng không nên tạo giao dịch mới dưới cùng key.
Giải pháp là hash payload đã chuẩn hóa cùng các trường ảnh hưởng semantics:
fingerprint = SHA-256(method + path + tenant + canonical_body)
Nếu key giống nhưng fingerprint khác, trả lỗi conflict, thường có thể dùng HTTP 409 theo thiết kế API. Stripe mô tả việc so tham số của request lặp để tránh sử dụng lại key sai mục đích.
Tránh hash trực tiếp JSON chưa chuẩn hóa vì thứ tự key hoặc whitespace có thể khác dù nghĩa giống nhau.
Xử lý hai request đồng thời cùng key ra sao?
Kiểm tra rồi mới insert bằng hai câu lệnh tách biệt tạo race condition:
Worker A: key chưa có
Worker B: key chưa có
Worker A: tạo payment
Worker B: tạo payment
Phòng thủ đúng dùng một primitive nguyên tử:
- unique constraint và
INSERT ... ON CONFLICT; - compare-and-set;
- conditional write;
- transaction isolation phù hợp;
- distributed lock khi thực sự cần, có fencing token.
Một worker giành quyền xử lý. Worker còn lại thấy trạng thái processing và có thể:
- chờ ngắn rồi đọc kết quả;
- trả
202 Acceptedcùng location để poll; - trả conflict có thể retry;
- subscribe vào completion event.
Không giữ database lock dài trong lúc gọi API bên ngoài nếu có thể tránh.
TTL cho idempotency key nên dài bao lâu?
TTL là business decision, không chỉ là cache setting. Nếu key bị xóa quá sớm, một retry muộn tạo duplicate. Nếu giữ mãi, storage tăng và key không thể tái dùng.
Cần xem:
- retry policy của client, proxy và queue;
- thời gian người dùng có thể refresh hoặc gửi lại;
- thời gian event có thể bị replay;
- yêu cầu chargeback, đối soát và audit;
- vòng đời nghiệp vụ;
- quy định lưu dữ liệu.
Một lựa chọn tốt là gắn idempotency với business object bền vững. Ví dụ order_id có unique constraint trong payment table, nhờ đó hiệu ứng vẫn được bảo vệ ngay cả khi cache key đã hết hạn.
Deduplication khác idempotency thế nào?
Deduplication phát hiện hai message hoặc record được coi là trùng và bỏ hoặc hợp nhất một bản. Idempotency đảm bảo xử lý lặp không tạo thêm hiệu ứng.
Dedup thường phụ thuộc cửa sổ thời gian và định danh. Idempotency có thể nằm trong logic cập nhật trạng thái.
Ví dụ:
- bỏ event có
event_idđã thấy là dedup; UPDATE subscription SET status='active'lặp vẫn cùng trạng thái là idempotent;INSERT ledger_entryvới uniquetransaction_iddùng cả dedup và invariant database.
Hai kỹ thuật bổ sung cho nhau. Dedup layer bị mất state vẫn có thể thất bại; idempotent business invariant cung cấp lớp bảo vệ sâu hơn.
Transactional outbox giải quyết vấn đề gì?
Một service cần vừa ghi order vào database, vừa publish OrderCreated. Nếu làm hai thao tác riêng:
- ghi order thành công;
- publish event thất bại;
thì dữ liệu và message lệch nhau. Nếu publish trước rồi ghi order thất bại, consumer thấy event cho order không tồn tại.
Transactional outbox ghi order và một hàng outbox trong cùng database transaction. Worker khác đọc outbox và publish message. Publisher có thể gửi trùng, nên consumer vẫn phải idempotent.
Transaction:
INSERT order
INSERT outbox(event_id, payload)
Commit
Publisher:
đọc outbox -> gửi broker -> đánh dấu đã gửi
Outbox giải quyết atomicity giữa business state và ý định publish, không tự bảo đảm consumer side effect chỉ xảy ra một lần.
Inbox pattern bảo vệ consumer thế nào?
Consumer lưu message_id vào bảng inbox cùng transaction với business update:
BEGIN
INSERT inbox(message_id) -- unique
UPDATE inventory ...
COMMIT
Nếu message được giao lại, insert inbox xung đột và handler biết đã xử lý. Điều kiện quan trọng là inbox record và side effect database nằm trong cùng transaction.
Nếu side effect là gửi email hoặc gọi API ngoài, cần thêm state machine, outbox hoặc idempotency key chuyển tiếp tới downstream. Một transaction local không bao phủ Internet.
Retry policy đúng cần những gì?
Idempotency làm retry an toàn hơn nhưng không có nghĩa retry vô hạn.
Một policy production nên có:
- phân loại lỗi retryable và non-retryable;
- exponential backoff;
- jitter để tránh đồng bộ retry;
- deadline tổng;
- số lần thử tối đa;
- idempotency key ổn định;
- circuit breaker khi downstream lỗi diện rộng;
- dead-letter queue cho message cần điều tra;
- metric theo attempt và final outcome.
Không retry validation error, authentication failure hoặc request vi phạm business rule. Với timeout, retry sau backoff thường hợp lý nếu operation đã idempotent.
Quy trình 8 bước thiết kế thao tác retry an toàn
Bước 1: Xác định hiệu ứng nghiệp vụ
Ghi rõ thao tác tạo, tăng, chuyển, gửi hay xóa gì. “Gọi API thành công” chưa phải mô tả nghiệp vụ.
Bước 2: Chọn identity của ý định
Dùng order ID, payment attempt ID, event ID hoặc idempotency key do client tạo. Scope theo tenant và operation.
Bước 3: Định nghĩa trạng thái
Ít nhất có processing, succeeded và failed. Với nghiệp vụ dài, thêm pending review, compensating hoặc expired.
Bước 4: Bảo vệ bằng ràng buộc nguyên tử
Unique constraint hoặc conditional write phải là nguồn phân xử, không chỉ kiểm tra ở application memory.
Bước 5: Gắn payload với key
Tạo fingerprint và từ chối cùng key với semantics khác.
Bước 6: Thiết kế response replay
Quyết định trả resource hiện có, response đã lưu hay trạng thái poll. Client phải phân biệt đang xử lý với thất bại cuối.
Bước 7: Kết nối downstream an toàn
Chuyển tiếp key, dùng outbox/inbox hoặc API downstream idempotent. Không để lớp cuối cùng phá vỡ toàn chuỗi.
Bước 8: Kiểm thử failure injection
Crash tại mọi điểm: trước ghi, sau ghi, trước ack, sau ack, giữa API call và state update. Gửi đồng thời và replay sau TTL để tìm lỗ hổng.
Idempotency trong AI Agent có gì đặc biệt?
Agent thường retry tool call khi timeout, đổi kế hoạch hoặc khôi phục loop. Nếu tool tạo side effect, duplicate có thể là:
- gửi cùng email hai lần;
- tạo hai ticket;
- đăng hai bài;
- đặt hai lịch;
- tính phí hai lần;
- chạy hai workflow dữ liệu.
Mỗi tool ghi nên nhận operation_id hoặc idempotency key. Agent runtime phải giữ key ổn định cho cùng một ý định, kể cả sau restart. Approval cũng cần gắn với payload hash để model không thay tham số sau khi con người duyệt.
Ngoài ra, log tool call nên phân biệt:
- logical operation;
- attempt number;
- provider request ID;
- final business resource ID;
- trạng thái compensation.
Quan sát theo logical operation giúp tránh đếm retry thành nhiều giao dịch.
Những lỗi thiết kế thường gặp
Dùng timestamp làm key
Mỗi retry tạo timestamp mới nên không dedup được. Key phải đại diện cùng ý định.
Chỉ lưu key sau khi hoàn tất
Hai request đồng thời có thể cùng chạy. Cần claim key trước bằng thao tác nguyên tử.
Không kiểm tra payload
Cùng key với số tiền hoặc người nhận khác gây kết quả khó hiểu và có thể nguy hiểm.
TTL ngắn hơn retry window
Retry muộn trở thành thao tác mới. Cần guardrail ở business object hoặc giữ key đủ lâu.
Coi HTTP DELETE luôn an toàn
DELETE được định nghĩa idempotent về hiệu ứng dự kiến, nhưng handler có thể gửi email, hoàn tiền hoặc tăng counter mỗi lần. Side effect phụ cũng phải được xem xét.
Tin vào exactly-once như phép màu
Guarantee luôn có scope. Kiểm tra database, broker, consumer và API ngoài có nằm trong cùng transaction hay không.
FAQ - Câu hỏi thường gặp
Idempotency là gì trong một câu?
Idempotency là khả năng lặp lại cùng thao tác mà không tạo thêm hiệu ứng nghiệp vụ ngoài lần đầu tiên.
GET có luôn idempotent không?
Theo HTTP semantics, GET là safe và idempotent. Nhưng implementation sai vẫn có thể tạo side effect nghiệp vụ. Analytics hoặc log nội bộ không nhất thiết phá semantics, còn trừ tiền thì có.
POST có thể idempotent không?
Có. POST không mặc định idempotent, nhưng endpoint có thể hỗ trợ idempotency key và ràng buộc nghiệp vụ để retry an toàn.
Có nên trả cùng response cho request lặp không?
Thường nên trả lại kết quả hoặc tài nguyên của lần đầu. Cần quy định rõ khi lần đầu còn processing, đã failed hoặc payload khác.
Unique constraint có đủ không?
Nó là lớp bảo vệ mạnh cho một invariant trong database, nhưng chưa đủ nếu có side effect ngoài transaction như email hoặc API khác. Cần outbox, state machine hoặc idempotency downstream.
Exactly-once và idempotency có thay thế nhau không?
Không. Exactly-once có phạm vi cụ thể. Idempotency bảo vệ handler và business effect khi vẫn xảy ra retry hoặc replay ngoài phạm vi đó.
Tóm lại
Idempotency biến retry từ một rủi ro thành cơ chế phục hồi có thể dự đoán. Nó đặc biệt quan trọng vì timeout chỉ nói rằng phía gọi không nhận được câu trả lời, không chứng minh phía nhận chưa thực hiện thao tác.
Năm nguyên tắc thực tế:
- dùng một identity ổn định cho mỗi ý định nghiệp vụ;
- claim key bằng thao tác nguyên tử;
- buộc key khớp request fingerprint;
- bảo vệ toàn chuỗi side effect, không chỉ broker hoặc API đầu tiên;
- kiểm thử bằng crash, concurrency và replay thật.
Nếu workflow ghép nhiều service, xem API Composition là gì? để quản lý timeout và partial failure. Bài Read path và write path giúp hiểu nơi state idempotency được đọc ghi. Với tool-use tự động, Rủi ro AI Agent bổ sung approval và quyền hạn.
Nguồn tham khảo
- A Detailed Guide to Idempotency, Delivery Semantics, and Deduplication, ByteByteGo.
- RFC 9110: Idempotent Methods, RFC Editor.
- Idempotent requests, Stripe.
- Message Delivery Semantics, Apache Kafka.