+1

Idempotency: "Lá chắn" cuối cùng ngăn chặn trùng lặp giao dịch trong Hệ thống Phân tán

chào các bạn, mình là một Backend Engineer. Chắc hẳn trong lập trình, ít nhất một lần bạn đã từng gặp tình huống "dỡ khóc dở cười" thế này:

Hệ thống thanh toán đang chạy ngon lành, bỗng một ngày đẹp trời. Client báo cáo mất mạng ngay lúc bấm nút "Thanh Toán". Người lo lắng, bấm liên tiếp 3 đến 4 lần nữa. Kết quả? Database nổ 4 giao dịch, tài khoản khách hàng bị trừ tiền 4 lần, còn bạn bị Ops gọi dậy lúc 2 giờ sáng để giải trình.

Vấn đề này không chỉ nằm ở UX, mà nó là bài toán kinh điển trong hệ thống phân tán: Làm sao để một API bị gọi N lần nhưng kết quả thực hiện chỉ diễn ra DUY NHẤT một lần? Chào mừng bạn đến với thế giới của Idempotency.

1. Idempotency là gì? (Giải thích kiểu "bình dân học vụ")

Trong toán học và tin học, Idempotency (Tính bù trừ) là tính chất của một thao tác mà khi bạn thực hiện nó nhiều lần, kết quả cuối cùng không thay đổi so với lần thực hiện đầu tiên.

Hãy tưởng tượng cái nút thang máy.

Nếu bạn bấm 1 lần: Thang máy nhận lệnh và đi đến tầng của bạn Nếu bạn spam bấm 10 lần: Thang máy vẫn chỉ nhận lệnh đó và đi đến đúng tầng đó. Kết quả không đổi. Đây chính là Idempotent

Ngược lại, nút "Tăng âm lượng" trên TV không phải là idempotent. Bấm 1 lần là âm lượng lên 10, bấm 10 lần là âm lượng lên 100

2. Giải pháp kỹ thuật: Cơ chế Idempotency-Key

Để xử lý việc này trong API, giải pháp chuẩn công nghiệp (như Stripe, PayPal đang dùng) là sử dụng một mã định danh duy nhất gọi là Idempotency-Key.

Luồng đi của dữ liệu (Data Flow)

  1. Client: Trước khi gửi Request(Ví dụ: POST /v1/payments), Client tạo ra một UUID duy nhất (Idempotency-Key) và gửi kèm trong HTTP Header

  2. API Layer: Khi nhận Request, Server sẽ kiểm tra trong Storage (thường là Redis để đảm bảo tốc độ):

  • Nếu Key đã tồn tại: Trả về ngay kết quả của lần xử lý trước đó mà không thực hiện lại logic nghiệp vụ.
  • Nếu Key chưa tồn tại: Lưu key này vào Storage với trạng thái "In Progress" và bắt đầu xử lý.
  1. Database: Thực hiện trừ tiền, ghi log.
  2. Finish: Sau khi xong, Server cập nhật kết quả vào Storage gắn với Key đó và trả về cho Client.

3. Case Study: Thiết kế và Xử lý Race Condition

Đây là phần "xương xẩu" nhất mà một Senior cần quan tâm.

Thiết kế bảng lưu trữ/Schema Nếu dùng Redis (khuyên dùng), chúng ta nên cấu hình mã giả như sau:

// Ví dụ bằng Golang
type IdempotencyRecord struct {
    ResponseCode int    `json:"response_code"`
    ResponseBody string `json:"response_body"`
    Status       string `json:"status"` // PENDING, SUCCESS, FAILED
}

TTL (Time To Live): Key này không nên lưu vĩnh viễn. Thông thường, tùy vào nghiệp vụ mà đặt từ 24h đến 7 ngày.

Atomic Lock: Điều gì xảy ra nếu 2 request cùng một Idempotency-Key đến cùng một mili giây?

Xử lý Race Condition

Để tránh việc cả 2 request cùng thấy Key chưa tồn tại và cùng nhảy vào xử lý, bạn phải dùng Distributed Lock hoặc lệnh SETNX (Set if Not Exists) trong Redis.

// Minh họa mã giả Node.js + ioredis
const idempotencyKey = req.headers['idempotency-key'];

const isLocked = await redis.set(idempotencyKey, 'IN_PROGRESS', 'NX', 'EX', 3600);

if (!isLocked) {
    // Nếu không set được nghĩa là đã có request đang xử lý hoặc đã xong
    const result = await redis.get(idempotencyKey);
    return res.status(200).json(JSON.parse(result));
}

// Thực hiện logic thanh toán tại đây...

4. Mở rộng: Idempotency trong HTTP Methods

Không phải mọi API đều cần cơ chế "Key" phức tạp này, vì bản thân HTTP đã quy định tính chất này cho các phương thức:

Method Method Giải thích
GET Yes Chỉ lấy dữ liệu, gọi 100 lần kết quả (về mặt side-effect) vẫn như nhau.
PUT Yes Ghi đè toàn bộ tài nguyên. Nếu bạn cập nhật tên là "Hoàng" 10 lần, kết quả vẫn là "Hoàng".
DELETE Yes Xóa một tài nguyên. Lần đầu xóa thành công (200), các lần sau báo (404) nhưng tài nguyên vẫn là "đã bị xóa".
POST Yes Thường dùng để tạo mới. Gọi 2 lần tạo ra 2 bản ghi. Đây là nơi cần áp dụng Idempotency-Key.

Lời kết

dempotency không chỉ là một kỹ thuật, nó là tư duy về sự an toàn của hệ thống. Trong các giao dịch tài chính hoặc các hệ thống đòi hỏi tính chính xác cao, việc thiếu cơ chế này là một "lỗ hổng" chí mạng.

Hy vọng bài viết này giúp các bạn có cái nhìn sâu sắc hơn về cách thiết kế API chuẩn chỉnh. Nếu thấy hay, đừng quên Upvote và để lại bình luận thảo luận cùng mình nhé!

Happy Coding!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí