Optimistic concurrency control trong cơ sở dữ liệu quan hệ

Optimistic concurrency control with sql

Bạn có một hệ thống thanh toán. Khi khách hàng giao dịch, code của bạn sẽ trừ tiền trong tài khoản của khách, chắc chắn rồi.

Giả sử khách hàng có user_id = 1 có giao dịch 100 VND, cách làm thông thường trông sẽ giống như thế này (bài viết sử dụng PostgreSQL để demo):

SELECT balance FROM accounts WHERE user_id = 1;
-- trong code, trừ 100 từ balance nếu balance >= 100
-- với ? là giá trị balance sau khi trừ, thực hiện
UPDATE accounts SET balance = ? WHERE user_id =1;

Mọi thứ hoạt động tốt, cho đến khi nó sai và sẽ sai ngay khi thông tin của khách hàng đó được update bởi hai sessions cùng lúc.

Tưởng tượng khách hàng có hai sessions, mỗi session có một giao dịch trị giá 100 VND, số dư ban đầu là 500 VND.

SESSION 1 SESSION 2
SELECT balance FROM accounts WHERE user_id = 1; (trả về 500)
SELECT balance FROM accounts WHERE user_id = 1; (cũng trả về 500)
UPDATE balance SET balance = 400 WHERE user_id = 1; (500 – 100 = 400)
UPDATE balance SET balance = 400 WHERE user_id = 1; (500 – 100 = 400)

Oops! Số dư lúc này là 400 VND, nhưng thực tế phải là 300 VND.

Cách làm trên được gọi là read-modify-write. Có nhiều cách để xử lý vấn đề này, tuy nhiên trong bài này mình sẽ sử dụng optimistic concurrency control (optimistic /ˌɑːp.təˈmɪs.tɪk/ – lạc quan – cứ coi là sẽ không xảy ra chuyện gì, xảy ra thì tính).

Optimistic concurrency control (optimistic locking)

Optimistic concurrency control thường được implement ở phía application, một cách để xử lý đồng thời (concurrency handling).

Các table sẽ có thêm một column version hoặc timestamp, và khi thực hiện UPDATE thì ta sẽ thêm WHERE để kiểm tra xem row đã bị thay đổi so với lúc SELECT hay chưa.

Thêm column version vào table accounts:

ALTER TABLE accounts ADD COLUMN version integer NOT NULL DEFAULT 1;

Lúc này, ví dụ ban đầu sẽ trở thành như sau:

SESSION 1 SESSION 2
BEGIN; BEGIN;
SELECT balance, version FROM accounts WHERE user_id = 1; (trả về 1, 500)
SELECT version, balance FROM accounts WHERE user_id = 1; (cũng trả về 1, 500)
COMMIT; COMMIT;
BEGIN; BEGIN;
UPDATE accounts SET balance = 400, version = 2 WHERE user_id = 1 AND version = 1; (500 – 100 = 400. Thành công, 1 row changed.)
UPDATE accounts SET balance = 400, version = 2 WHERE user_id = 1 AND version = 1; (500 – 100 = 400). Sẽ bị session 1 block lại do database lock row khi write.
COMMIT;
UPDATE trả về kết quả không có row nào được cập nhật bởi vì lúc này nó thấy version = 2 thay vì version = 1 trong WHERE
ROLLBACK; bởi vì dữ liệu đã bị thay đổi trong quá trình UPDATE

Giao dịch tại session 2 bị ROLLBACK, ta xử lý giao dịch này lại từ đầu.

Kết luận

Ngoài optimistic locking, chúng ta còn các lựa chọn khác như: Pessimistic Locking, Serializability.

Đọc thêm

#sql #postgresql #concurrency