MySQL Isolation Levels: Đừng để Transaction làm hỏng dữ liệu của bạn

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Khi dữ liệu bỗng nhiên… “nhảy số”

Lúc mới vận hành một sàn thương mại điện tử quy mô khoảng 500 đơn hàng mỗi phút, mình từng gặp một bug “ma quái” khiến cả team mất ngủ. Hệ thống chạy test local cực mượt. Tuy nhiên, khi đẩy lên production với bảng orders vượt mốc 10 triệu dòng, những ca vô lý bắt đầu xuất hiện. Khách đã bị trừ tiền nhưng đơn hàng vẫn báo “Chờ thanh toán”, hoặc cùng một báo cáo kho nhưng mỗi lần F5 lại ra một con số khác nhau.

Mình nhận ra rằng chỉ dùng BEGINCOMMIT là chưa đủ. Khi hàng trăm giao dịch cùng đổ vào một bảng dữ liệu, việc thiếu kiểm soát Isolation Level sẽ khiến database rơi vào trạng thái hỗn loạn. Chúng ta cần hiểu rõ cách MySQL quản lý các phiên làm việc để tránh tranh chấp dữ liệu (concurrency issues).

Ba hiện tượng kinh điển gây sai lệch dữ liệu

Trước khi cấu hình, bạn cần phân biệt rõ 3 vấn đề mà mọi backend developer đều phải đối mặt.

1. Dirty Read (Đọc dữ liệu chưa chốt)

Hiện tượng này xảy ra khi Transaction A đọc một dòng dữ liệu mà Transaction B đang sửa nhưng chưa COMMIT. Hãy tưởng tượng khách hàng A vừa nạp 500k, hệ thống mới ghi tạm vào DB. Nếu Transaction B đọc con số 500k đó nhưng sau đó Transaction A bị lỗi và ROLLBACK, khách hàng A thực tế vẫn có 0 đồng. Transaction B đã đọc phải dữ liệu rác.

2. Non-repeatable Read (Đọc không nhất quán)

Bạn SELECT số dư lần một thấy 100k. Ngay sau đó, một tiến trình khác UPDATE số dư này thành 50k và COMMIT ngay lập tức. Bạn SELECT lần hai (vẫn trong transaction cũ) và thấy nó biến thành 50k. Việc dữ liệu thay đổi ngay giữa lòng một giao dịch thường phá hỏng các logic tính toán kiểm kê.

3. Phantom Read (Đọc bóng ma)

Khác với việc sửa dòng cũ, Phantom Read liên quan đến việc INSERT/DELETE. Ví dụ: Bạn SELECT COUNT(*) để đếm đơn hàng và thấy có 10. Một giây sau, ai đó chèn thêm 1 đơn mới. Bạn SELECT lại thì thấy 11. Những dòng dữ liệu “bóng ma” này là tác nhân chính gây sai lệch trong các báo cáo thống kê cuối kỳ.

4 cấp độ cô lập trong MySQL và cách vận hành

MySQL (engine InnoDB) cung cấp 4 cấp độ tùy chỉnh. Cấp độ càng cao, dữ liệu càng an toàn nhưng cái giá phải trả là hiệu năng (performance) giảm mạnh do cơ chế khóa (locking).

Cấp độ 1: READ UNCOMMITTED

Đây là mức độ lỏng lẻo nhất. Các Transaction có thể nhìn thấy cả dữ liệu chưa commit của nhau. Thực tế, mình hiếm khi dùng mức này vì rủi ro Dirty Read quá lớn.

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
-- Bạn sẽ thấy cả những thay đổi tạm thời của người khác
SELECT balance FROM accounts WHERE id = 1;

Cấp độ 2: READ COMMITTED

Đây là lựa chọn mặc định của SQL Server hay PostgreSQL. Nó đảm bảo bạn chỉ đọc được những gì đã COMMIT. Mức này chặn được Dirty Read, nhưng vẫn để lọt Non-repeatable ReadPhantom Read.

Cấp độ 3: REPEATABLE READ (Mặc định của MySQL)

InnoDB sử dụng cơ chế MVCC (Multi-Version Concurrency Control) rất thông minh. Trong một giao dịch, dù bạn SELECT bao nhiêu lần, dữ liệu vẫn y hệt lần đầu tiên. MySQL còn dùng kỹ thuật Next-Key Locking để ngăn chặn phần lớn trường hợp Phantom Read. Đây là điểm cộng lớn giúp MySQL cân bằng giữa tốc độ và an toàn.

Cấp độ 4: SERIALIZABLE

Đây là mức độ cực đoan nhất. Các Transaction phải xếp hàng thực thi lần lượt. Nếu Transaction A đang đọc vùng dữ liệu X, Transaction B muốn chèn vào đó phải đứng đợi. Tốc độ xử lý sẽ tụt dốc không phanh nếu hệ thống có lượng request lớn.

Thực hành: Kiểm tra và thay đổi Isolation Level

Để kiểm tra chế độ hiện tại, bạn dùng lệnh:

SELECT @@transaction_isolation;

Nếu đang xử lý một giao dịch tài chính nhạy cảm, bạn có thể ép session đó chạy ở mức cao nhất:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- Thực hiện logic thanh toán
UPDATE accounts SET balance = balance - 100 WHERE user_id = 10;
COMMIT;

Kinh nghiệm thực tế: Chọn mức nào để tối ưu?

Dưới đây là vài quy tắc mình đúc kết sau nhiều năm làm việc với database lớn:

  • Ưu tiên REPEATABLE READ: Phù hợp cho 90% ứng dụng web. MySQL đã tối ưu cực tốt bằng MVCC nên bạn không cần quá lo lắng về tốc độ.
  • Dùng READ COMMITTED khi nào? Khi bạn có bảng dữ liệu khổng lồ và chấp nhận dữ liệu thay đổi nhẹ để đổi lấy tốc độ ghi log hoặc giảm thiểu Deadlock.
  • Né SERIALIZABLE nếu có thể: Chỉ dùng cho các lõi ngân hàng cực kỳ khắt khe. Nếu áp dụng cái này cho web bán hàng lúc Flash Sale, hệ thống chắc chắn sẽ treo do các giao dịch đợi khóa lẫn nhau.

Một lưu ý quan trọng: Khi bảng vượt quá 10 triệu dòng, các câu lệnh SELECT thiếu Index sẽ gây ra Gap Locking. Database sẽ khóa cả một khoảng dữ liệu lớn khiến các tiến trình khác không thể INSERT. Hãy luôn kiểm tra Index cho các cột trong điều kiện WHERE.

Tóm lại

Lựa chọn Isolation Level không chỉ là chỉnh cấu hình, mà là sự đánh đổi giữa tính chính xác và hiệu suất. Đừng phó mặc cho mặc định. Hãy căn cứ vào đặc thù dữ liệu để chọn cấp độ phù hợp nhất. Nếu hệ thống đang gặp lỗi Deadlock hoặc dữ liệu sai lệch khi chạy đa luồng, hãy rà soát lại Isolation Level đầu tiên.

Share: