Gần đây mình nhận ra có khá nhiều người nhầm lần giữa hai khái niệm này và cho rằng có thể sử dụng Optimistic Locking để thực hiện Idempotency. Bài viết này mình muốn chỉ rõ đây là hai khái niệm khác nhau hoàn toàn và không nên nhầm lẫn.
1. Idempotency
Theo Wiki thì:
Idempotence is the property of certain operations in mathematics and computer science, that can be applied multiple times without changing the result beyond the initial application.
Một cách đơn giản thì một operation được coi là idempotent khi mà nó được gọi đi gọi lại nhiều lần thì vẫn không thay đổi kết quả của của hệ thống.
Ví dụ như HTTP GET method, khi bạn gọi 3 lần lấy status của 1 payment transaction kết quả vẫn trả về như nhau mà không ảnh hưởng trạng thái của system.
GET http://api.example.com/payment/status/12345
Kết quả luôn luôn là:
{ "transactionId": "12345", "status": "SUCCESS", "amount": 100.00
}
Nhưng với HTTP POST thì khác, nếu bạn call 3 lần api thanh toán thì sẽ có thêm 3 giao dịch thêm vào hệ thống.
POST http://api.example.com/payment/charge Payload: { "userId": "56789", "amount": 100.00, "paymentMethod": "credit_card"
}
Kết quả sau mỗi lần call:
{ "transactionId": "67890", "status": "SUCCESS", "amount": 100.00
}
Tại sao xử lý Idempotency lại quan trọng?
Như ví dụ về HTTP POST ở trên, nếu chẳng may có lỗi trong quá trình xử lý làm cho 1 giao dịch của khách hàng bị gọi lại 3 lần.
Bạn mua 1 cuốn sách 300k nó charge cho gần 1 triệu T_T
Vậy là đã hiểu tại sao xử lý Itempotency lại quan trọng rồi nhé.
Việc handle idempotency là bắt buộc trong một số hệ thống yêu cầu độ chính xác cao. Có nhiều cách để handle nhưng bài viết này mình sẽ không đi sâu vào nó.
2. Optimistic Locking
Optimistic Locking được thực hiện theo cơ chế Compare and Swap (CAS), cụ thể hơn thì nó sẽ check trạng thái của data có bị thay đổi hay không (Compare) trước khi thực hiện thay đổi (Swap)
Một chút về database, Optimistic Locking đảm bảo data không bị ghi đè lên nhau khi nhiều thread cùng update chung 1 record, field 'version' thường được sử dụng để compare trạng thái
Lấy ví dụ đơn giản bạn mua 1 ly cafe 80k và thanh toán qua app ngân hàng, nếu tài khoản bạn có 100k thì sẽ trừ còn 20k, nếu bạn chỉ còn 50k thì .... COOK 😂. Code sẽ nhìn giống như này (psudocode):
SELECT balance, version FROM balance_tab WHERE uid=$uid; If ($balance > $price) { $new_balance = $balance - $price $version_new = $version + 1 } else { return “not enough balance”; } UPDATE balance_tab SET balance=$new_balance, version=$version_new WHERE uid=$uid AND version=$version
Như vậy có thể thấy Optimistic Locking không thực sự Lock mà nó chỉ compare trước trạng thái (version) nếu chưa bị ai update thì mới ghi vào, còn không sẽ ném lỗi. Lại hơi lan mang rồi 😂 Về Locking mình sẽ có bài viết riêng nhé
3. Optimistic Locking không phải là Idempotency
Có nhiều người đang nhầm lẫn về cái này nhưng:
Việc bạn sử dụng Optimistic Locking không có nghĩa API của bạn đã Idempotent
Cũng ví dụ về ly cafe 80k ở trên, trong trường hợp số tiền đã bị trừ vào tài khoản nhưng khi response lại găp Network Error, client sẽ thực hiện retry lần thứ 2, data flow sẽ như thế này:
Init: balance = 100k, version=10 Expect: balance = 20k, version = 11 Request #1:
version=10 ⇒ version=11 balance=100 ⇒ balance=20 ___
Response fail vì network error
___ Request #2 (client retry): version=11 ⇒ version=12 balance=20 ⇒ balance= -60k (tượng trưng thôi vì application sẽ ném lỗi ko đủ tiền)
Qua ví dụ có thể nói là chỉ sử dụng Optimistic Lock không làm cho API trở nên Idempotent được mà phải áp dụng thêm nhiều cách khác.
4. Kết luận
Idempotency và Optimistic Locking là hai khái niệm hoàn toàn khác nhau và không nên bị nhầm lần
Optimistic Lock sử dụng để tránh data bị ghi đè lên nhau (data race) nhưng nếu call 2 lần khác nhau thì nó vẫn update 2 lần. Còn Itempotency là để diễn tả việc 1 API có bị gọi 2 (hoặc nhiều lần) với cùng 1 payload thì data vẫn chỉ update 1 lần duy nhất.
Mặc dù vậy vẫn có có nhiều case apply Optimistic Locking có thể làm API Idempotent luôn, nhưng cần hiểu rõ bản chất là Optimistic Locking không phải là Idempotency