Giao diện
Concurrency Control: Isolation Levels & Read Phenomena
Khi có nhiều transaction chạy đồng thời, chuyện gì có thể sai? Bài này sẽ đi sâu vào các "hiện tượng đọc" nguy hiểm và cách Isolation Levels giúp kiểm soát chúng.
1️⃣ Read Phenomena: Ba hiện tượng đọc đáng sợ
Khi hai transaction chạy đồng thời và truy cập cùng dữ liệu, có thể xảy ra các hiện tượng kỳ lạ.
Dirty Read (Đọc bẩn) - Nghiêm trọng nhất
Transaction A đọc được dữ liệu mà Transaction B chưa COMMIT.
Timeline:
T1: BEGIN; UPDATE products SET stock = 0 WHERE id = 1; [chưa COMMIT]
T2: BEGIN; SELECT stock FROM products WHERE id = 1; -> Thấy stock = 0!
T1: ROLLBACK;
T2: [Đã quyết định based on stock = 0,
NHƯNG thực tế stock CHƯA TỪNG = 0!]Hậu quả: T2 có thể đã hiển thị "Hết hàng" cho khách, hoặc từ chối đơn hàng. Nhưng T1 rollback -> stock thực tế vẫn còn. Khách mất đơn oan.
sql
-- Mô phỏng Dirty Read (chỉ xảy ra ở READ UNCOMMITTED)
-- Session 1:
BEGIN;
UPDATE products SET stock = 0 WHERE id = 1;
-- Không commit, chờ...
-- Session 2:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT stock FROM products WHERE id = 1; -- Thấy stock = 0 (dirty!)Non-Repeatable Read (Đọc không lặp lại)
Transaction A đọc cùng một row 2 lần, nhưng kết quả khác nhau vì Transaction B đã UPDATE và COMMIT ở giữa.
Timeline:
T1: BEGIN; SELECT price FROM products WHERE id = 1; -> 100.000
T2: BEGIN; UPDATE products SET price = 150000 WHERE id = 1; COMMIT;
T1: SELECT price FROM products WHERE id = 1; -> 150.000 (Khác rồi!)
T1: [Tính toán dựa trên giá nào? 100k hay 150k?]Hậu quả: Báo cáo tài chính có thể sai nếu cùng một query trong cùng transaction trả về kết quả khác nhau.
sql
-- Mô phỏng Non-Repeatable Read (xảy ra ở READ COMMITTED)
-- Session 1:
BEGIN;
SELECT price FROM products WHERE id = 1; -- 100000
-- Chờ session 2...
SELECT price FROM products WHERE id = 1; -- 150000 (đã thay đổi!)
-- Session 2:
BEGIN;
UPDATE products SET price = 150000 WHERE id = 1;
COMMIT;Phantom Read (Đọc ma)
Transaction A đọc một tập hợp rows 2 lần, nhưng số lượng rows khác nhau vì Transaction B đã INSERT/DELETE ở giữa.
Timeline:
T1: BEGIN; SELECT COUNT(*) FROM orders WHERE status = 'pending'; -> 10
T2: BEGIN; INSERT INTO orders (status) VALUES ('pending'); COMMIT;
T1: SELECT COUNT(*) FROM orders WHERE status = 'pending'; -> 11 (Thêm 1 "ma"!)Khác với Non-Repeatable Read:
- Non-Repeatable: Row cũ bị UPDATE.
- Phantom: Row mới xuất hiện (hoặc biến mất).
sql
-- Mô phỏng Phantom Read (xảy ra ở REPEATABLE READ trong MySQL)
-- Session 1:
BEGIN;
SELECT * FROM orders WHERE total > 1000000; -- 3 rows
-- Chờ session 2...
SELECT * FROM orders WHERE total > 1000000; -- 4 rows (phantom!)
-- Session 2:
BEGIN;
INSERT INTO orders (total) VALUES (5000000); -- Thỏa điều kiện total > 1000000
COMMIT;2️⃣ Isolation Levels: 4 Tầng phòng thủ
SQL standard định nghĩa 4 mức độ cách ly, từ thấp nhất (ít cách ly, nhiều performance) đến cao nhất (cách ly hoàn toàn, ít performance hơn).
Bảng so sánh Isolation Levels
| 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 | ⚡⚡⚡⚡ Cực nhanh |
| Read Committed | ❌ Ngăn chặn | ✅ Có thể xảy ra | ✅ Có thể xảy ra | ⚡⚡⚡ Nhanh |
| Repeatable Read | ❌ Ngăn chặn | ❌ Ngăn chặn | ✅ Có thể xảy ra* | ⚡⚡ Trung bình |
| Serializable | ❌ Ngăn chặn | ❌ Ngăn chặn | ❌ Ngăn chặn | ⚡ Chậm nhất |
*PostgreSQL implement REPEATABLE READ mạnh hơn SQL standard, ngăn cả Phantom Read.
Level 1: Read Uncommitted (Không ai dùng cả)
sql
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;- Cho phép đọc dữ liệu chưa commit của transaction khác.
- Use case: Gần như không có. Chỉ dùng cho thống kê "gần đúng" không cần chính xác.
- Cảnh báo: MySQL InnoDB KHÔNG hỗ trợ level này, nó tự động nâng lên Read Committed.
Level 2: Read Committed (Default PostgreSQL)
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Hoặc không cần set, đây là default của PostgreSQL- Chỉ đọc dữ liệu đã commit.
- Mỗi câu SELECT thấy snapshot TẠI THỜI ĐIỂM CÂU SELECT CHẠY.
- Use case: Hầu hết ứng dụng web, CRUD thông thường.
- Vấn đề: Non-Repeatable Read vẫn xảy ra.
sql
-- Ví dụ: Hai lần SELECT trong cùng transaction
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 1000000 (tại 10:00:00)
-- Chờ 5 giây, transaction khác đã update và commit
SELECT balance FROM accounts WHERE id = 1; -- 1500000 (tại 10:00:05)
-- Hai kết quả khác nhau trong cùng transaction!
COMMIT;Level 3: Repeatable Read (Default MySQL InnoDB)
sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;- Transaction thấy snapshot cố định tại thời điểm BẮT ĐẦU TRANSACTION (hoặc câu SELECT đầu tiên).
- Mọi câu SELECT sau đó đều thấy cùng dữ liệu.
- Use case: Báo cáo tài chính, thống kê cần consistency cao.
sql
-- PostgreSQL REPEATABLE READ
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- 1000000
-- Transaction khác commit thay đổi balance thành 1500000 ở đây
SELECT balance FROM accounts WHERE id = 1; -- VẪN 1000000!
COMMIT;⚠️ Phantom Read và Gap Locking (MySQL)
MySQL InnoDB dùng Gap Locking để ngăn Phantom Read ở REPEATABLE READ. Điều này có thể gây deadlock nếu nhiều transaction INSERT vào cùng "gap" cùng lúc.
Level 4: Serializable (Nghiêm ngặt nhất)
sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;- Các transaction chạy như thể chúng thực hiện tuần tự, từng cái một.
- Ngăn chặn TẤT CẢ các read phenomena.
- Trade-off: Performance thấp, có thể gây nhiều lỗi
serialization_failurephải retry.
sql
-- PostgreSQL Serializable với SSI (Serializable Snapshot Isolation)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(balance) FROM accounts; -- Tính tổng
-- Nếu transaction khác cũng đọc và modify accounts ->
-- Một trong hai sẽ bị abort với "could not serialize access"
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT; -- Có thể thất bại!3️⃣ Trade-off: Performance vs Consistency
Khi nào dùng cái gì?
| Isolation Level | Use Case | Ví dụ |
|---|---|---|
| Read Committed | CRUD thông thường, không cần consistency tuyệt đối | Web app API, form submit |
| Repeatable Read | Báo cáo, thống kê trong transaction dài | Generate monthly report |
| Serializable | Tài chính nghiêm ngặt, đấu giá, đặt vé | Bank transfer, auction bidding |
Chi phí ẩn của Serializable
python
# Pseudo-code retry pattern cho Serializable
import random
import time
MAX_RETRIES = 5
def transfer_money(from_id, to_id, amount):
for attempt in range(MAX_RETRIES):
try:
with db.transaction(isolation='serializable'):
# Check balance
balance = db.query("SELECT balance FROM accounts WHERE id = %s", from_id)
if balance < amount:
raise ValueError("Insufficient funds")
# Transfer
db.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", amount, from_id)
db.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", amount, to_id)
return True # Success
except SerializationFailure:
# Retry với exponential backoff
wait_time = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait_time)
raise Exception("Transaction failed after max retries")💡 HPN Pro Tip: Đừng dùng Serializable mù quáng
Serializable không phải "silver bullet". Với workload cao (100+ concurrent transactions), retry rate có thể lên tới 30-50%, giết chết throughput.
Chiến lược thực tế:
- Dùng Read Committed cho 90% queries.
- Dùng Application-level locking (Optimistic/Pessimistic) cho critical paths.
- Serializable chỉ cho các transaction cực kỳ quan trọng và ngắn.
4️⃣ Pessimistic vs Optimistic Locking (Khi Isolation Levels không đủ)
Đôi khi bạn cần kiểm soát cụ thể hơn Isolation Levels.
Pessimistic Locking: SELECT ... FOR UPDATE
Lock row TRƯỚC khi đọc, ngăn transaction khác modify.
sql
BEGIN;
-- Lock row để đảm bảo không ai đổi trong lúc mình xử lý
SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- Tính toán business logic...
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- Transaction khác cố SELECT FOR UPDATE cùng row sẽ phải CHỜOptimistic Locking: Version Column
Không lock, nhưng check version khi UPDATE.
sql
-- Lấy dữ liệu kèm version
SELECT id, stock, version FROM products WHERE id = 1;
-- Trả về: id=1, stock=10, version=5
-- Sau khi tính toán, update với điều kiện version
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- Nếu affected_rows = 0 -> Ai đó đã update trước -> RETRY!5️⃣ PostgreSQL vs MySQL: Khác biệt quan trọng
| Đặc điểm | PostgreSQL | MySQL InnoDB |
|---|---|---|
| Default Level | Read Committed | Repeatable Read |
| Repeatable Read ngăn Phantom? | ✅ Có (MVCC mạnh) | ⚠️ Có (Gap Lock, có thể deadlock) |
| Serializable mechanism | SSI (Serializable Snapshot Isolation) | 2PL (Two-Phase Locking) |
PostgreSQL MVCC (Multi-Version Concurrency Control)
PostgreSQL giữ nhiều versions của mỗi row. Mỗi transaction thấy version phù hợp với thời điểm nó bắt đầu.
Row versions:
[version 1: created by T1] -> [version 2: created by T2] -> [version 3: created by T3]
T4 với REPEATABLE READ bắt đầu tại thời điểm version 2 đã commit:
- Thấy version 2
- KHÔNG thấy version 3 (chưa commit tại thời điểm T4 bắt đầu)MySQL Gap Locking
MySQL lock không chỉ row, mà cả "gap" giữa các rows để ngăn INSERT mới.
Index: 1, 3, 5, 7
Gap locks: [before 1], [1-3], [3-5], [5-7], [after 7]
Transaction lock row 3 -> Lock cả gap [1-3] và [3-5]
-> Transaction khác KHÔNG THỂ INSERT row với key 2 hoặc 4Tổng kết
| Thuật ngữ | Mô tả |
|---|---|
| Dirty Read | Đọc dữ liệu chưa commit |
| Non-Repeatable Read | Đọc cùng row 2 lần, kết quả khác |
| Phantom Read | Đọc cùng điều kiện 2 lần, số row khác |
| Read Committed | Chỉ đọc dữ liệu đã commit |
| Repeatable Read | Snapshot cố định trong transaction |
| Serializable | Transaction chạy tuần tự |
💡 HPN Golden Rule
1. Hiểu workload của bạn trước.
2. Bắt đầu với Read Committed (đủ cho 90% cases).
3. Nếu cần consistency cao hơn, thử Optimistic Locking trước.
4. Serializable là giải pháp cuối cùng, kèm retry strategy.