Câu hỏi phỏng vấn SQL transaction & isolation
Bộ đề này harvest từ khoá SQL & Database và PostgreSQL Internals — mỗi câu link bài học đào sâu cơ chế, không dừng ở đáp án thuộc lòng.
01
ACID — từng chữ nghĩa là gì thật sự, ngoài định nghĩa tutorial?
JuniorAtomicity không phải magic: mọi thay đổi ghi vào write-ahead log (WAL) trước, và dòng log "TX COMMITTED" là ranh giới — crash trước dòng đó thì transaction coi như chưa từng tồn tại. Consistency theo nghĩa gốc của Jim Gray rất hẹp: transaction chỉ được đưa DB giữa các trạng thái không vi phạm integrity constraint (NOT NULL, CHECK, UNIQUE, FK) — nó KHÔNG đảm bảo business invariant, đây là chữ bị hiểu sai nhiều nhất. Isolation là "không thấy trạng thái trung gian của nhau", nhưng mức độ cụ thể phụ thuộc isolation level. Durability nghĩa là COMMIT chỉ return sau khi WAL đã
fsync()xuống storage thật — nằm trong OS page cache là chưa đủ.CHECK constraint nhưbalance >= 0không chặn được race đọc-tính-ghi: hai transaction cùng đọc 1000, cùng trừ 800, cả hai đều pass CHECK — cầnSELECT FOR UPDATEhoặc atomic UPDATE, không phải constraint.02
Dirty read, non-repeatable read và phantom read khác nhau thế nào?
JuniorDirty read là đọc data của transaction khác CHƯA commit — nếu bên kia rollback, bạn đã thao tác trên data không tồn tại. Non-repeatable read là đọc CÙNG MỘT ROW hai lần trong một transaction mà giá trị khác nhau, vì transaction khác đã UPDATE và commit ở giữa. Phantom read tinh vi hơn: không row nào bị sửa, nhưng chạy lại cùng một query điều kiện thì TẬP KẾT QUẢ đổi (thêm/bớt row) do transaction khác INSERT/DELETE và commit. Phân loại này theo đúng paper Berenson et al. 1995 — bài phê bình chuẩn ANSI SQL mà interviewer hay lấy làm gốc.
Còn anomaly thứ tư mà SQL-92 bỏ sót: write skew — hai transaction cùng đọc một tập data, cùng ra quyết định, ghi vào HAI ROW KHÁC NHAU nên không có write conflict, nhưng invariant chung bị phá. Chỉ Serializable chặn được.03
Bốn isolation level chuẩn SQL — level nào chặn anomaly nào?
JuniorRead Uncommitted không chặn gì; Read Committed chặn dirty read; Repeatable Read chặn thêm non-repeatable read; Serializable chặn tất cả, kể cả write skew. Điểm cộng lúc phỏng vấn: chuẩn ANSI cho phép phantom ở Repeatable Read, nhưng PostgreSQL implement level này bằng Snapshot Isolation nên thực tế chặn luôn cả phantom — mạnh hơn chuẩn yêu cầu. Đổi lại, không level nào dưới Serializable chặn được write skew, vì hai bên không hề ghi đè cùng một row.
Trả lời yếu: đọc thuộc ma trận mà không nói được implement thật khác chuẩn thế nào (PG Repeatable Read = Snapshot Isolation, chặn phantom) — interviewer thường hỏi ngược đúng chỗ này.04
PostgreSQL mặc định chạy isolation level nào? Chọn Read Uncommitted thì được gì?
JuniorMặc định của PostgreSQL là Read Committed — đủ chặn dirty read với chi phí thấp nhất. Còn Read Uncommitted trong PostgreSQL chỉ là cái tên: bạn set được, nhưng engine xử lý y hệt Read Committed, vì kiến trúc MVCC không bao giờ cho đọc dữ liệu chưa commit — muốn dirty read cũng không được. Serializable ở mọi engine đều phải opt-in tường minh, không nơi nào bật mặc định vì giá throughput của nó.
Nghĩ 'hạ xuống Read Uncommitted cho nhanh' — trong PostgreSQL điều đó không đổi gì cả; tối ưu đúng chỗ nằm ở query và index, không phải hạ isolation.05
MVCC hoạt động thế nào mà reader không block writer?
MidMVCC lưu NHIỀU PHIÊN BẢN của cùng một row: mỗi tuple mang
xmin(transaction tạo ra nó) vàxmax(transaction xoá/update nó), reader tự chọn phiên bản nhìn thấy được theo snapshot của mình — nên đọc không cần lock, reader và writer không chặn nhau. Hệ quả quan trọng: UPDATE trong PostgreSQL không sửa tại chỗ mà là INSERT tuple mới + đánh dấuxmaxtuple cũ; tuple cũ thành "dead tuple" nằm lại trên heap chờ VACUUM dọn. Khác biệt với MySQL InnoDB: InnoDB sửa tại chỗ và giữ bản cũ trong undo log riêng — nên long transaction ở PG gây heap bloat, ở InnoDB gây undo log bloat.Hai cái giá của MVCC hay bị hỏi ngược: transaction id 32-bit có wraparound (~2 tỷ tx, autovacuum phải freeze kịp), và một transaction mở lâu sẽ chặn VACUUM dọn dead tuple của TOÀN BỘ hệ thống.06
SAVEPOINT dùng để làm gì — khi nào cần rollback một phần transaction?
JuniorSAVEPOINTđặt điểm đánh dấu giữa transaction;ROLLBACK TO savepointhuỷ những gì xảy ra từ điểm đó về sau nhưng GIỮ NGUYÊN phần trước, và transaction vẫn mở để đi tiếp. Use case điển hình là bulk import theo batch: import 1000 dòng chia batch 100, batch nào lỗi thì rollback riêng batch đó rồi tiếp tục, không mất các batch đã xong. Trong PL/pgSQL, khốiBEGIN...EXCEPTION...ENDthực chất tự tạo một savepoint ngầm — nhờ vậy bắt được lỗi nội bộ mà không làm abort transaction bên ngoài.Quên rằng sau khi một statement lỗi trong transaction, PostgreSQL từ chối mọi lệnh tiếp theo cho tới ROLLBACK — savepoint chính là công cụ để 'lỗi cục bộ' không giết cả transaction.07
Lost update xảy ra thế nào và chống bằng gì?
MidHai session cùng đọc balance = 100, cùng tính toán ở tầng app, cùng ghi đè — một lần trừ tiền "bốc hơi" mà không có lỗi nào báo, vì Read Committed cho phép chuyện này. Cách chống xếp theo tầng chi phí: (1) atomic UPDATE expression —
SET balance = balance - 100 WHERE id = 1 AND balance >= 100, tính toán ngay trong SQL, đủ cho đa số case; (2)SELECT FOR UPDATE— lock row trước khi đọc-tính-ghi, transaction khác phải chờ; (3) Serializable + retry khi nhận SQLSTATE 40001, cho các invariant phức tạp nhiều row. Nguyên tắc: chọn tầng thấp nhất đủ dùng.Nhảy thẳng lên Serializable cho một phép trừ đơn giản — atomic UPDATE với điều kiện ngay trong WHERE đã đóng race đó với chi phí gần như bằng không.08
Bật SERIALIZABLE thì trả giá gì? PostgreSQL implement nó ra sao?
MidPostgreSQL không dùng lock-based serializable kiểu truyền thống mà dùng SSI — Serializable Snapshot Isolation (paper Cahill 2008): vẫn chạy trên snapshot như Repeatable Read, thêm SIREAD lock chỉ để THEO DÕI dependency giữa các transaction (không block ai), rồi phát hiện chu trình trong dependency graph. Giá phải trả: transaction có thể bị ABORT với SQLSTATE 40001 — kể cả false positive khi không có anomaly thật — nên app BẮT BUỘC có retry logic; và throughput thấp hơn các level dưới. Đổi lại, đây là level duy nhất chặn được write skew.
Nhầm 40001 (serialization failure) với 40P01 (deadlock) — cả hai đều cần retry nhưng nguyên nhân khác nhau; và quên viết retry là bug sản xuất chắc chắn xảy ra khi bật Serializable.
Topic kế cùng track: Câu hỏi phỏng vấn SQL index (B-tree, composite, covering) →
Trả lời trôi chảy bắt đầu từ hiểu cơ chế
Mỗi câu ở trên đều có bài học đứng sau. Học tuần tự cả khoá SQL & Database — Tư tưởng & Nguyên lý để không chỉ trả lời được, mà giải thích được vì sao.