0

Đừng để Race Condition làm hỏng Database: Khi nào cần dùng SELECT ... FOR UPDATE?

Hồi mới đi làm, mình cứ nghĩ đơn giản: Muốn cập nhật số dư hay số lượng hàng trong kho thì cứ SELECT nó ra, tính toán xong rồi UPDATE lại là xong. Nghe thì có vẻ ổn, nhưng đó là khi chỉ có một mình bạn dùng hệ thống.

Khi hệ thống có hàng ngàn request cùng ập đến một lúc (ví dụ: săn flash sale), kịch bản "kinh dị" mang tên Race Condition sẽ xuất hiện. Hôm nay mình sẽ chia sẻ về một "vũ khí" hạng nặng để xử lý vấn đề này trong MySQL/PostgreSQL: SELECT ... FOR UPDATE.

1. Kịch bản "Mất tiền oan"

Giả sử bạn đang làm tính năng thanh toán. Quy trình thường thấy sẽ là:

Check số dư: SELECT balance FROM users WHERE id = 1; (Giả sử có 100k).

Kiểm tra xem có đủ tiền mua món đồ 80k không.

Nếu đủ, trừ tiền: UPDATE users SET balance = balance - 80000 WHERE id = 1;

Chuyện gì xảy ra nếu 2 request cùng chạy song song? Cả hai đều thấy số dư là 100k. Cả hai đều thấy đủ điều kiện trừ tiền. Và bùm! Khách hàng tiêu 160k trong khi chỉ có 100k trong túi. Hệ thống thất thoát, và người bị "gõ đầu" đầu tiên chính là anh em Backend chúng ta.

2. SELECT ... FOR UPDATE là cái gì?

Về cơ bản, khi bạn thêm FOR UPDATE vào cuối câu lệnh SELECT, bạn đang nói với Database rằng: "Này, tôi đang nhắm tới dòng này để cập nhật đấy, ông khóa nó lại hộ tôi. Thằng nào định đọc để sửa hay định Update thì bảo nó đứng xếp hàng chờ tôi làm xong đã!"

Lúc này, Database sẽ thực hiện một cái gọi là Exclusive Lock (Khóa độc quyền) trên các dòng dữ liệu mà bạn vừa select.

3. Triển khai với Go (Gopher Style)

Trong Go, khi làm việc với Database, bạn phải chạy lệnh này bên trong một Transaction (db.Begin()). Khóa sẽ chỉ được giải phóng khi bạn gọi Commit() hoặc Rollback().

4. Những "hố tử thần" cần tránh

Dùng FOR UPDATE sướng thì sướng thật, nhưng đừng lạm dụng kẻo "vỡ trận":

Deadlock (Khóa chết): Nếu Transaction A khóa dòng 1 và đợi dòng 2, trong khi Transaction B khóa dòng 2 và đợi dòng 1... Chúc mừng, hệ thống của bạn đã đứng hình. Hãy luôn cố gắng update dữ liệu theo một thứ tự nhất định.

Performance: Vì nó là khóa (Lock), nên các request khác phải xếp hàng (Wait). Nếu xử lý bên trong Transaction quá lâu (ví dụ: gọi thêm API bên thứ 3 chậm chạp), hàng đợi sẽ dài dằng dặc và làm chết server.

Tip: Chỉ giữ Transaction ngắn nhất có thể.

Quên Index: Nếu câu lệnh SELECT của bạn không dùng Index, Database có thể sẽ khóa cả bảng (Table Lock) thay vì khóa từng dòng (Row Lock). Đây là thảm họa thực sự!

5. Lời kết

SELECT ... FOR UPDATE là một kỹ thuật mạnh mẽ nhưng cần sự cẩn trọng. Nó thuộc nhóm Pessimistic Locking (Khóa bi quan - tức là luôn lo sợ dữ liệu bị sửa đổi nên phải khóa trước cho chắc).

Nếu hệ thống của bạn không quá khắt khe về tính nhất quán hoặc ít xảy ra xung đột, bạn có thể tìm hiểu thêm về Optimistic Locking (dùng version column) để có hiệu năng tốt hơn.

Anh em đã bao giờ bị "vả" bởi Race Condition chưa? Cùng chia sẻ kinh nghiệm xử lý ở dưới comment nhé!


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í