1. 00:19 06/08/2026
Trong phát triển web/app, có một lỗi khá “đời thường” nhưng gây hậu quả lớn: người dùng bấm nút gửi hai lần, trình duyệt tự retry request, mạng chập chờn khiến app gửi lại thao tác, hoặc worker xử lý lại một job tưởng như chưa thành công. Nếu hệ thống không được thiết kế cẩn thận, kết quả có thể là tạo hai đơn hàng, trừ tiền hai lần, gửi hai email giống nhau hoặc ghi trùng dữ liệu.
Khái niệm đáng chú ý ở đây là idempotency. Hiểu đơn giản: cùng một thao tác được gửi nhiều lần nhưng kết quả cuối cùng vẫn như gửi một lần. Đây là một nguyên tắc rất hữu ích khi xây dựng API, đặc biệt với các hành động quan trọng như thanh toán, đặt vé, tạo đơn hàng, đăng ký tài khoản, gửi biểu mẫu hoặc chạy tác vụ nền.
Một ví dụ thực tế: người dùng bấm “Thanh toán”, app gửi request POST /payments. Mạng bị chậm, giao diện chưa phản hồi, người dùng bấm lại. Nếu backend cứ nhận request nào tạo giao dịch request đó, rủi ro rất cao. Cách xử lý tốt hơn là mỗi lần client bắt đầu một thao tác quan trọng, nó tạo một idempotency key, ví dụ một UUID, rồi gửi kèm trong header hoặc body. Backend lưu key này cùng trạng thái xử lý. Nếu request giống key đó được gửi lại, backend không tạo giao dịch mới mà trả về kết quả cũ hoặc trạng thái hiện tại.
Một luồng triển khai đơn giản có thể như sau:
- Client tạo idempotency_key cho mỗi thao tác người dùng chủ động thực hiện.
- Backend kiểm tra key này đã tồn tại chưa.
- Nếu chưa tồn tại, ghi nhận key ở trạng thái “processing”, sau đó thực hiện xử lý chính.
- Khi xử lý xong, lưu kết quả tương ứng với key: thành công, thất bại, mã đơn hàng, mã giao dịch...
- Nếu nhận lại cùng key, backend trả về kết quả đã lưu thay vì chạy lại logic tạo mới.
Điểm quan trọng là idempotency key phải gắn với đúng ngữ cảnh. Ví dụ key nên đi cùng user_id, endpoint và payload chính. Nếu cùng một key nhưng nội dung request khác hẳn, backend nên từ chối thay vì đoán ý. Điều này tránh trường hợp client dùng nhầm key cho hai thao tác khác nhau.
Ngoài idempotency key, database constraint cũng là lớp bảo vệ rất đáng có. Ví dụ bảng orders có thể có unique constraint trên client_request_id hoặc checkout_session_id. Với email đăng ký, có thể đặt unique trên email. Với giao dịch thanh toán, có thể unique trên external_transaction_id từ cổng thanh toán. Đừng chỉ dựa vào kiểm tra trong code kiểu “select trước rồi insert sau”, vì hai request đồng thời vẫn có thể vượt qua nếu không có ràng buộc ở database.
Một số lời khuyên có thể áp dụng ngay:
- Với nút gửi quan trọng trên giao diện, nên disable sau lần bấm đầu tiên, nhưng đừng xem đây là biện pháp duy nhất. Backend vẫn phải tự bảo vệ.
- Với API tạo tài nguyên quan trọng, cân nhắc hỗ trợ idempotency key.
- Với webhook từ hệ thống ngoài, luôn giả định webhook có thể được gửi nhiều lần. Hãy lưu event_id và bỏ qua event đã xử lý.
- Với queue/job nền, thiết kế job sao cho chạy lại không gây hỏng dữ liệu. Ví dụ gửi email cần có bảng log hoặc trạng thái “đã gửi”.
- Với thao tác tài chính, đơn hàng, tồn kho, hãy dùng transaction và unique constraint phù hợp.
- Nên đặt thời hạn lưu idempotency key, chẳng hạn vài giờ hoặc vài ngày tùy nghiệp vụ, để tránh bảng lưu key phình ra mãi.
Cũng cần phân biệt idempotency với việc “không cho người dùng thao tác lại”. Người dùng có thể mua thêm một đơn hàng khác, gửi một yêu cầu mới hoặc thanh toán một giao dịch mới. Vấn đề là hệ thống phải biết đâu là thao tác mới thật sự, đâu là bản gửi lại của cùng một thao tác cũ.
Một thiết kế tốt thường kết hợp nhiều lớp: giao diện hạn chế bấm lặp, API nhận idempotency key, database có unique constraint, worker xử lý được retry, log đủ thông tin để truy vết. Khi làm như vậy, ứng dụng sẽ bền hơn trước các tình huống mạng không ổn định, người dùng thao tác nhanh, hệ thống retry tự động hoặc tích hợp bên thứ ba gửi lại dữ liệu.
Idempotency không phải kỹ thuật quá phức tạp, nhưng nếu được nghĩ đến từ sớm, nó giúp tránh nhiều lỗi khó chịu và tốn kém. Đặc biệt với các chức năng liên quan đến tiền, đơn hàng, tài khoản và thông báo, đây nên là một phần trong checklist thiết kế backend chứ không phải việc chỉ xử lý khi đã có sự cố.
0