Họ đang đo gì
Bạn có biết code của mình đang chạy ở mức nào không. Đây là thứ ảnh hưởng trực tiếp tới đúng/sai của nghiệp vụ mà phần lớn dev chưa từng kiểm tra.
Trả lời ngắn~30 giây
READ UNCOMMITTED cho phép đọc bẩn. READ COMMITTED chặn đọc bẩn nhưng vẫn có đọc lặp không nhất quán và phantom. REPEATABLE READ chặn thêm đọc lặp không nhất quán. SERIALIZABLE chặn tất cả, kể cả write skew. Quan trọng: Postgres mặc định READ COMMITTED, còn InnoDB mặc định REPEATABLE READ — nên cùng một đoạn code có thể đúng ở máy này và sai ở máy kia.
Hai transaction chạy chồng thời gian nhau. Ở mức cô lập thấp, mỗi bên chỉ thấy một phần việc của bên kia.
Giải thích sâu
Bảng bốn hiện tượng là chuẩn SQL, nhưng engine thật không cài đặt đúng như chuẩn mô tả. Postgres không có READ UNCOMMITTED thật — yêu cầu mức đó thì bạn được READ COMMITTED, vì MVCC không có cách nào cho bạn đọc dữ liệu chưa commit. REPEATABLE READ của Postgres thực chất là snapshot isolation, mạnh hơn chuẩn ở chỗ nó chặn luôn phantom read.
InnoDB chặn phantom ở REPEATABLE READ bằng cách khác: next-key lock, tức là khoá bản ghi cộng với khoá khoảng trống trước nó, để không ai chèn được hàng mới vào khoảng bạn vừa quét. Chi tiết này quan trọng vì nó cũng là nguồn deadlock phổ biến nhất của MySQL — hai transaction khoá hai khoảng chồng nhau theo thứ tự ngược nhau.
Điều chuẩn SQL bỏ sót là write skew: hai transaction đọc cùng một tập hàng, mỗi bên quyết định dựa trên tập đó rồi ghi vào hàng KHÁC nhau, nên không đụng độ mà vẫn phá vỡ bất biến chung. Ví dụ kinh điển là lịch trực: ràng buộc “luôn có ít nhất một bác sĩ trực”, hai người cùng xin nghỉ, mỗi transaction thấy còn người kia, cả hai commit, ca trực trống. Snapshot isolation không chặn được; chỉ SERIALIZABLE hoặc một khoá tường minh mới chặn được.
Trong thực tế, SERIALIZABLE của Postgres (SSI) không khoá mà phát hiện xung đột rồi huỷ một transaction với mã lỗi 40001. Nghĩa là chọn mức này thì ứng dụng BẮT BUỘC phải có vòng retry. Đội nào bật SERIALIZABLE mà quên retry sẽ thấy lỗi ngẫu nhiên dưới tải cao và thường đổ oan cho database.
Write skew — cả hai đều commit thành công
-- Bất biến: luôn còn ít nhất 1 bác sĩ trực. Hiện có: Alice, Bob.
-- T1 -- T2
BEGIN ISOLATION LEVEL REPEATABLE READ; BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM oncall SELECT count(*) FROM oncall
WHERE on_shift; -- 2 WHERE on_shift; -- 2
-- "còn 2 người, mình nghỉ được" -- "còn 2 người, mình nghỉ được"
UPDATE oncall SET on_shift = false UPDATE oncall SET on_shift = false
WHERE name = 'Alice'; WHERE name = 'Bob';
COMMIT; COMMIT;
-- Không hàng nào bị ghi trùng -> không xung đột -> ca trực trống.Ở SERIALIZABLE, Postgres huỷ một trong hai với lỗi 40001 và bạn retry.
Câu hỏi tiếp theo họ sẽ hỏi
?SELECT … FOR UPDATE giải quyết được write skew không?
Có, nếu bạn khoá đúng những hàng mà quyết định phụ thuộc vào — ở ví dụ trên là khoá cả bảng ca trực chứ không chỉ hàng mình sửa. Đó chính là điểm khó: bạn phải khoá thứ mình ĐỌC, không phải thứ mình GHI, và đó là điều SERIALIZABLE làm giúp bạn.
?Vì sao ứng dụng chạy READ COMMITTED vẫn hầu như đúng?
Vì phần lớn nghiệp vụ chỉ đọc rồi ghi trong cùng một câu lệnh (UPDATE … SET n = n + 1), mà câu lệnh đơn thì luôn nguyên tử. Vấn đề chỉ xuất hiện khi bạn ĐỌC ở ứng dụng, tính toán, rồi GHI lại — mẫu read-modify-write. Đó là lúc cần FOR UPDATE hoặc khoá lạc quan bằng cột version.
?Mức cô lập nào cho hàng đợi job trong DB?
READ COMMITTED cộng FOR UPDATE SKIP LOCKED. SKIP LOCKED cho mỗi worker nhặt hàng chưa bị khoá thay vì xếp hàng chờ, nên n worker chạy song song thật. Đây là mẫu chuẩn cho hàng đợi nhỏ mà chưa cần Kafka.
Trả lời thế này là mất điểm
- Nói “cứ để SERIALIZABLE cho chắc”. Không có vòng retry thì đó là đổi bug logic lấy lỗi 500 ngẫu nhiên.
- Đọc thuộc bảng bốn hiện tượng mà không biết mặc định của engine mình đang dùng.
- Cho rằng REPEATABLE READ ở đâu cũng như nhau. Postgres chặn phantom, chuẩn SQL thì không đòi hỏi vậy.
Trả lời thế này là ghi điểm
- Nói được mình đã KIỂM TRA mức cô lập trong dự án gần nhất, và bằng câu lệnh nào (
SHOW transaction_isolation).