Bài trước cho thấy MVCC khiến mỗi giao dịch thấy một "ảnh chụp" (snapshot) dữ liệu. Nhưng chụp lúc nào và chụp lại ra sao thì tùy mức cô lập giao dịch (transaction isolation level). Đây là núm chỉnh quan trọng nhất của lập trình đồng thời với database — chọn thấp thì nhanh nhưng dễ dính bug dữ liệu, chọn cao thì an toàn nhưng tốn hơn và có thể phải thử lại.
Chuẩn SQL định nghĩa bốn mức qua ba bất thường (anomaly) mà chúng ngăn hay cho phép. Bài này (phần 2 loạt PostgreSQL) demo thật từng anomaly trên pg-lab, và chỉ ra PostgreSQL khác chuẩn ở hai điểm quan trọng mà nhiều người không biết.
Ba bất thường và bốn mức cô lập
Khi hai giao dịch chạy song song, ba loại "nhìn thấy sai" có thể xảy ra:
- Dirty read (đọc bẩn): đọc thấy dữ liệu giao dịch khác chưa commit — nguy hiểm nhất, vì dữ liệu đó có thể bị rollback.
- Non-repeatable read (đọc lặp sai): đọc cùng một hàng hai lần, ra hai giá trị khác nhau (vì có UPDATE + commit xen giữa).
- Phantom read: chạy cùng một truy vấn hai lần, số hàng khác nhau (vì có INSERT + commit xen giữa).

Hình 1: Ba anomaly và bảng bốn mức cô lập chuẩn SQL. PostgreSQL khác chuẩn: không có dirty read (Read Uncommitted = Read Committed), và Repeatable Read ngăn luôn phantom (mạnh hơn chuẩn). Đặt mức bằng BEGIN ISOLATION LEVEL ....
BEGIN ISOLATION LEVEL READ COMMITTED; -- mac dinh cua PostgreSQL
BEGIN ISOLATION LEVEL REPEATABLE READ; -- snapshot dong bang tu dau tx
BEGIN ISOLATION LEVEL SERIALIZABLE; -- nhu chay tuan tu (bai 4)
Đo thật: non-repeatable read ở RC vs RR
Mình mở phiên A đọc cùng một hàng hai lần quanh một khoảng chờ; ở giữa, phiên B UPDATE giá trị rồi commit. Chạy với hai mức cô lập khác nhau. Kết quả thật từ pg-lab:

Hình 2: Kết quả thật. Non-repeatable read xảy ra ở Read Committed (100→200), biến mất ở Repeatable Read (100→100). Phantom xảy ra ở Read Committed (3→5), biến mất ở Repeatable Read (3→3). Dirty read không xảy ra kể cả ở Read Uncommitted (A đọc v=100, không thấy 777 chưa commit).
- Read Committed:
read1=100, sau khi B commit v=200,read2=200. Hai lần đọc cùng hàng ra hai kết quả — đọc lặp sai xảy ra. Đây là mặc định của PostgreSQL: mỗi câu lệnh lấy snapshot mới nhất đã commit. - Repeatable Read:
read1=100, dù B commit v=200,read2=100— an toàn. Repeatable Read đóng băng snapshot từ đầu giao dịch, nên mọi lần đọc trong giao dịch thấy cùng dữ liệu.
Đo thật: phantom read và dirty read
Phantom read (đếm số hàng hai lần, B chèn thêm 2 hàng giữa chừng):
- Read Committed:
count1=3, sau khi B chèn,count2=5— phantom xảy ra. - Repeatable Read:
count1=3, dù B chèn,count2=3— an toàn. Đây là điểm PostgreSQL mạnh hơn chuẩn SQL: chuẩn cho phép phantom ở Repeatable Read, nhưng PostgreSQL (dùng snapshot isolation) ngăn luôn.
Dirty read (B đang UPDATE v=777 chưa commit, A đọc ở Read Uncommitted):
- A đọc được
v=100(bản cũ), không thấy 777. PostgreSQL không bao giờ đọc dữ liệu chưa commit, kể cả khi bạn cố đặtREAD UNCOMMITTED— nó ánh xạ mức này thànhREAD COMMITTED. Đây là hệ quả trực tiếp của MVCC (bài 1).
Hệ quả: PostgreSQL thực tế chỉ có 3 mức khác biệt
Gộp lại, PostgreSQL chỉ có ba mức hành xử khác nhau (dù khai được bốn):
- Read Committed (mặc định): chống dirty read; vẫn có non-repeatable + phantom.
- Repeatable Read: chống dirty read + non-repeatable + phantom (snapshot đóng băng từ đầu tx).
- Serializable: chống mọi anomaly, như thể các giao dịch chạy tuần tự (bài 4).
(READ UNCOMMITTED chỉ là bí danh của READ COMMITTED.)
Đánh đổi cần cân nhắc
Mức cao hơn an toàn hơn nhưng tốn hơn và có thể phải thử lại. Read Committed rẻ nhất và đủ cho đa số ứng dụng CRUD — nhưng bạn phải ý thức rằng hai câu SELECT liên tiếp trong cùng giao dịch có thể thấy dữ liệu khác nhau. Repeatable Read cho nhất quán trong suốt giao dịch (hợp cho báo cáo đọc nhiều bảng cần ảnh chụp đồng nhất), nhưng giữ snapshot lâu hơn nên cản trở vacuum (bài 8) và có thể gặp lỗi could not serialize access khi ghi xung đột — phải bắt lỗi và thử lại. Serializable an toàn tuyệt đối nhưng tốn nhất và tỉ lệ thử lại cao (bài 4). Chọn mức thấp nhất đủ dùng cho logic nghiệp vụ.
Read Committed có một cạm bẫy tinh vi với đọc-rồi-ghi. Mẫu SELECT balance rồi UPDATE balance = balance - 100 dựa trên giá trị vừa đọc, ở Read Committed, có thể dính lost update: giữa SELECT và UPDATE, một giao dịch khác đã đổi balance. Mức cô lập cao hơn (Repeatable Read) sẽ phát hiện và báo lỗi thay vì ghi đè âm thầm. Nhưng cách đúng thường là dùng khóa tường minh (SELECT ... FOR UPDATE, bài 5) hoặc câu UPDATE nguyên tử — không chỉ dựa vào mức cô lập.
"Repeatable Read ngăn phantom" là đặc thù PostgreSQL — đừng mang giả định này sang database khác. Ở MySQL/InnoDB, Repeatable Read cũng ngăn nhiều phantom nhưng theo cơ chế khác (gap lock) với hành vi biên khác; ở các database theo đúng chuẩn SQL thì Repeatable Read không ngăn phantom. Nếu code của bạn chạy trên nhiều loại database, đừng phụ thuộc vào việc Repeatable Read ngăn phantom — hành vi này không di động.
Ba ý mang về
- Mức cô lập quyết định anomaly nào xảy ra. Đo thật: non-repeatable read xảy ra ở Read Committed (đọc 100 rồi 200), biến mất ở Repeatable Read (100 cả hai lần); phantom tương tự (3→5 ở RC, 3→3 ở RR). Mức cao hơn đóng băng snapshot nên nhất quán hơn.
- PostgreSQL khác chuẩn SQL ở hai điểm. Đo thật: không có dirty read kể cả khi đặt Read Uncommitted (A đọc v=100, không thấy 777 chưa commit — ánh xạ thành Read Committed); và Repeatable Read ngăn luôn phantom (mạnh hơn chuẩn). Thực tế PostgreSQL chỉ có 3 mức khác biệt.
- Chọn mức thấp nhất đủ dùng, và biết giới hạn. Read Committed rẻ nhưng hai SELECT có thể khác nhau; Repeatable Read nhất quán nhưng cản vacuum + có thể phải thử lại; Serializable an toàn nhất nhưng tốn nhất. Với đọc-rồi-ghi, dùng FOR UPDATE (bài 5) hoặc UPDATE nguyên tử, đừng chỉ dựa vào mức cô lập.
Nguồn
- PostgreSQL docs — Transaction Isolation: https://www.postgresql.org/docs/current/transaction-iso.html
- PostgreSQL docs — SET TRANSACTION: https://www.postgresql.org/docs/current/sql-set-transaction.html
- Wikipedia — Isolation (database systems): https://en.wikipedia.org/wiki/Isolation_(database_systems)
Phần sau ta đi sâu vào khác biệt thực chiến giữa Read Committed và Repeatable Read: cùng một logic đọc-rồi-tính cho kết quả khác nhau khi có ghi đồng thời, và khi nào bạn thực sự cần nâng lên Repeatable Read.