Transaction Isolation Levels — Hiểu đúng để không mất dữ liệu trong hệ thống concurrent
Khi nhiều user cùng thao tác trên database, chuyện gì xảy ra nếu 2 transaction đọc/ghi cùng 1 dòng dữ liệu? Câu trả lời phụ thuộc vào Isolation Level — mức độ "cách ly" mà database cung cấp giữa các transaction chạy song song.
Bài viết này sẽ giúp bạn:
- Hiểu bản chất từng isolation level qua analogy trực quan
- Nhận diện 3 loại anomaly nguy hiểm: Dirty Read, Non-repeatable Read, Phantom Read
- Biết cách set isolation level trong Laravel
- Chọn đúng level cho từng use case thực tế
1. Isolation Level là gì?
Isolation Level quyết định mức độ nhìn thấy dữ liệu giữa các concurrent transactions. Level càng cao → transaction càng "riêng tư" → càng an toàn, nhưng đánh đổi bằng performance.
SQL Standard định nghĩa 4 mức:
| Isolation Level | Dirty Read | Non-repeatable Read | Phantom Read | Performance |
|---|---|---|---|---|
| READ UNCOMMITTED | ❌ Có thể xảy ra | ❌ Có thể xảy ra | ❌ Có thể xảy ra | ⚡⚡⚡⚡ Nhanh nhất |
| READ COMMITTED | ✅ Ngăn được | ❌ Có thể xảy ra | ❌ Có thể xảy ra | ⚡⚡⚡ |
| REPEATABLE READ (default MySQL) | ✅ Ngăn được | ✅ Ngăn được | ❌ Có thể xảy ra | ⚡⚡ |
| SERIALIZABLE | ✅ Ngăn được | ✅ Ngăn được | ✅ Ngăn được | ⚡ Chậm nhất |
2. Analogy: Khách sạn & Phòng làm việc
Hãy tưởng tượng database là một khách sạn, mỗi transaction là một khách hàng vào phòng làm việc chung để đọc/sửa tài liệu trên bàn.
READ UNCOMMITTED — Phòng không có cửa
Bạn đang viết dở trên bảng trắng, chưa hoàn thành. Ai đi ngang cũng nhìn thấy và chụp lại. Nếu bạn xóa đi viết lại (rollback) → người kia đã mang đi dữ liệu sai.
→ Dirty Read xảy ra.
READ COMMITTED — Phòng có rèm, chỉ mở khi "Done"
Người khác chỉ thấy bảng trắng sau khi bạn ký tên xác nhận (commit). Nhưng nếu họ nhìn 2 lần cách nhau vài giây, bảng có thể đã bị sửa xong bởi người khác → kết quả khác.
→ Ngăn Dirty Read, nhưng Non-repeatable Read vẫn xảy ra.
REPEATABLE READ — Chụp snapshot lúc vào phòng
Khi bạn bước vào phòng, hệ thống chụp snapshot toàn bộ dữ liệu. Suốt phiên, bạn luôn nhìn thấy đúng bức ảnh ban đầu. Nhưng nếu có người thêm row mới → snapshot không bắt kịp.
→ Ngăn Dirty Read + Non-repeatable Read, nhưng Phantom Read vẫn có thể xảy ra.
SERIALIZABLE — Phòng VIP, vào từng người một
Mỗi lần chỉ 1 người được vào phòng. An toàn tuyệt đối — không ai chen ngang, không ai nhìn trộm. Nhưng chậm nhất.
→ Ngăn được tất cả anomalies.
3. Ba loại Anomaly nguy hiểm
3.1 Dirty Read — Đọc dữ liệu chưa commit
Transaction A đọc dữ liệu mà transaction B đã sửa nhưng chưa commit. Nếu B rollback → A đã quyết định dựa trên dữ liệu không bao giờ tồn tại.
-- Transaction B (chuyển tiền): | Transaction A (kiểm tra số dư):
BEGIN; |
UPDATE accounts |
SET balance = balance - 500 |
WHERE id = 1; |
-- balance = 500 (chưa commit) |
| BEGIN;
| SELECT balance FROM accounts
| WHERE id = 1;
| -- Đọc được: 500 ← DỮ LIỆU BẨN!
| COMMIT;
ROLLBACK; |
-- balance thực sự vẫn = 1000 |
Ngăn bằng: READ COMMITTED trở lên.
3.2 Non-repeatable Read — Cùng query, khác kết quả
Trong cùng 1 transaction, đọc cùng 1 row 2 lần nhưng kết quả khác nhau vì transaction khác đã UPDATE và commit xen giữa.
-- Transaction A (tạo báo cáo): | Transaction B (admin sửa giá):
BEGIN; |
SELECT price FROM products |
WHERE id = 1; |
-- Lần 1: price = 100 |
| BEGIN;
| UPDATE products SET price = 200
| WHERE id = 1;
| COMMIT;
SELECT price FROM products |
WHERE id = 1; |
-- Lần 2: price = 200 ← KHÁC! |
COMMIT; |
Ngăn bằng: REPEATABLE READ trở lên.
3.3 Phantom Read — Row "ma" xuất hiện
Query theo cùng điều kiện WHERE 2 lần nhưng số lượng row khác nhau vì transaction khác đã INSERT/DELETE xen giữa.
-- Transaction A (kiểm tra phòng): | Transaction B (đặt phòng):
BEGIN; |
SELECT COUNT(*) FROM rooms |
WHERE status = 'available' |
AND date = '2026-04-01'; |
-- Lần 1: COUNT = 3 |
| BEGIN;
| INSERT INTO rooms (status, date)
| VALUES ('available','2026-04-01');
| COMMIT;
SELECT COUNT(*) FROM rooms |
WHERE status = 'available' |
AND date = '2026-04-01'; |
-- Lần 2: COUNT = 4 ← ROW MA! |
COMMIT; |
Ngăn bằng: SERIALIZABLE.
4. Cách set Isolation Level trong Laravel
use Illuminate\Support\Facades\DB;
// Set isolation level trước khi bắt đầu transaction
DB::connection()->statement(
'SET TRANSACTION ISOLATION LEVEL READ COMMITTED'
);
DB::transaction(function () {
// Logic xử lý trong transaction
$balance = DB::table('accounts')
->where('id', 1)
->value('balance');
DB::table('accounts')
->where('id', 1)
->update(['balance' => $balance - 500]);
});
Các level có thể set:
// ⚠️ Nguy hiểm — có thể đọc uncommitted data
'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED'
// Ngăn dirty reads — phổ biến trong reporting
'SET TRANSACTION ISOLATION LEVEL READ COMMITTED'
// MySQL default — cân bằng tốt cho CRUD
'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'
// Mạnh nhất nhưng chậm nhất — dùng cho giao dịch tài chính nhạy cảm
'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'
5. Chọn Isolation Level nào cho dự án thực tế?
| Use case | Isolation Level | Lý do |
|---|---|---|
| CRUD app thông thường (e-commerce, booking) | REPEATABLE READ | Default MySQL, đủ an toàn cho 90% trường hợp |
| Reporting / Analytics dashboard | READ COMMITTED | Cần đọc dữ liệu mới nhất, chấp nhận non-repeatable read |
| Chuyển tiền ngân hàng, audit tài chính | SERIALIZABLE | Yêu cầu consistency tuyệt đối, chấp nhận hy sinh performance |
| Debug / Monitoring nhanh | READ UNCOMMITTED | Chỉ dùng trong development, không bao giờ dùng production |
Tổng kết
Isolation Level là một trong những khái niệm cốt lõi mà bất kỳ backend developer nào làm việc với relational database đều cần nắm vững. Quy tắc đơn giản:
- Đừng thay đổi default (REPEATABLE READ) trừ khi bạn có lý do rõ ràng.
- Hiểu trade-off: an toàn hơn = chậm hơn. Chọn mức vừa đủ cho use case.
- Luôn test với concurrent requests trước khi deploy — isolation bug chỉ lộ ra dưới tải thực.