Isolation level (mức cô lập) quyết định một giao dịch "thấy" gì khi các giao dịch khác đang chạy song song. PostgreSQL có ba mức: Read Committed (mặc định), Repeatable Read, và Serializable. Chọn sai mức có thể cho kết quả sai tinh vi, hoặc trả giá hiệu năng không cần thiết. Bài này đo thật cả hành vi lẫn chi phí của từng mức, và làm rõ vì sao mức cao đòi hỏi ứng dụng phải có logic thử lại.

Ba mức cô lập

Read Committed (mặc định): mỗi câu lệnh thấy dữ liệu đã committed mới nhất tại thời điểm câu đó bắt đầu. Hệ quả: hai SELECT trong cùng một giao dịch có thể trả kết quả khác nhau nếu một giao dịch khác commit thay đổi ở giữa (gọi là non-repeatable read).

Repeatable Read: toàn giao dịch dùng một ảnh chụp (snapshot) dữ liệu tại thời điểm giao dịch bắt đầu. Mọi SELECT thấy cùng dữ liệu, bất kể ai commit sau đó — đọc nhất quán. Nhưng nếu ghi đụng hàng mà giao dịch khác vừa sửa, PostgreSQL báo lỗi serialize và giao dịch phải thử lại.

Serializable: đảm bảo mạnh nhất — kết quả như thể các giao dịch chạy lần lượt, không xen kẽ, dùng cơ chế SSI (Serializable Snapshot Isolation). Nó bắt được cả write skew mà Repeatable Read bỏ sót, nhưng đổi lại chi phí theo dõi phụ thuộc và khả năng abort cao hơn.

BEGIN ISOLATION LEVEL READ COMMITTED;   -- mặc định
BEGIN ISOLATION LEVEL REPEATABLE READ;  -- ảnh chụp nhất quán
BEGIN ISOLATION LEVEL SERIALIZABLE;     -- như chạy tuần tự

Ảnh chụp đoạn mã SQL nền tối minh hoạ isolation level ba mức cô lập giao dịch của PostgreSQL, READ COMMITTED mặc định mỗi câu thấy dữ liệu committed mới nhất BEGIN ISOLATION LEVEL READ COMMITTED SELECT val lần 1 giao dịch khác commit thay đổi vào đây SELECT val lần 2 có thể thấy giá trị khác non-repeatable read nhanh nhất đủ cho hầu hết ứng dụng web, REPEATABLE READ ảnh chụp snapshot tại đầu giao dịch BEGIN ISOLATION LEVEL REPEATABLE READ mọi SELECT trong giao dịch thấy cùng một ảnh chụp dữ liệu lúc BEGIN bất kể ai commit sau đó đọc nhất quán nhưng nếu ghi đụng hàng bị giao dịch khác sửa lỗi serialize phải thử lại, SERIALIZABLE như thể các giao dịch chạy tuần tự BEGIN ISOLATION LEVEL SERIALIZABLE đảm bảo mạnh nhất kết quả như thể chạy lần lượt không xen kẽ dùng SSI bắt được cả write skew mà REPEATABLE READ bỏ sót đổi lại chi phí theo dõi phụ thuộc cộng có thể abort could not serialize, quy tắc vàng mức cao cần logic thử lại Repeatable Read và Serializable có thể hủy giao dịch với SQLSTATE 40001 could not serialize access ứng dụng phải bắt và chạy lại từ đầu không có retry lỗi trả về người dùng thay vì tự phục hồi

Hình 1: Ba mức cô lập — Read Committed (thấy committed mới nhất mỗi câu), Repeatable Read (ảnh chụp nhất quán), Serializable (như chạy tuần tự). Mức cao hơn cần logic thử lại vì có thể abort với SQLSTATE 40001.

Đo thật: hành vi khác nhau

Tôi cho một hàng val=100, rồi trong một giao dịch đọc nó hai lần, giữa hai lần đọc có một giao dịch khác commit thay đổi:

Ảnh chụp bảng kết quả đo thật nền tối hành vi và chi phí ba mức isolation hàng val bằng 100 một giao dịch khác commit thay đổi ở giữa PostgreSQL 16, đọc 2 lần giữa chừng B commit thay đổi READ COMMITTED đọc lần 1 bằng 100 đọc lần 2 bằng 200 thấy giá trị mới non-repeatable REPEATABLE READ đọc lần 1 bằng 100 đọc lần 2 bằng 100 giữ ảnh chụp nhất quán, ghi đụng nhau ở REPEATABLE READ lỗi serialize phải thử lại A REPEATABLE READ UPDATE dem SET val bằng val cộng 1 commit B REPEATABLE READ UPDATE dem SET val bằng val cộng 10 cùng hàng ERROR could not serialize access due to concurrent update ROLLBACK B phải bắt lỗi 40001 và chạy lại giao dịch, chi phí hiệu năng pgbench -N 8 client tải ghi ít tranh chấp read committed tps 11.021 mốc repeatable read tps 10.389 khoảng 6 phần trăm chậm hơn serializable tps 8.683 khoảng 21 phần trăm chậm hơn chi phí theo dõi SSI 0 giao dịch thất bại ở đây vì pgbench đụng ngẫu nhiên 5 triệu tài khoản ít tranh chấp dưới tải tranh chấp cao serializable còn tốn thêm retry, bảng mức Read Committed đảm bảo thấy committed mới nhất mỗi câu chi phí rẻ nhất mặc định Repeatable Read ảnh chụp nhất quán chi phí khoảng 6 phần trăm cộng cần retry khi ghi đụng Serializable như chạy tuần tự bắt write skew chi phí khoảng 21 phần trăm cộng retry

Hình 2: Read Committed đọc lần 1=100, lần 2=200 (thấy giá trị mới). Repeatable Read cả hai lần=100 (giữ ảnh chụp). Ghi đụng nhau ở Repeatable Read → ERROR: could not serialize access. Hiệu năng: Read Committed 11.021 TPS, Serializable 8.683 TPS (~21% chậm hơn).

  • Read Committed: đọc lần 1 = 100, đọc lần 2 = 200 — thấy giá trị mới mà giao dịch khác vừa commit (non-repeatable read).
  • Repeatable Read: đọc lần 1 = 100, đọc lần 2 = 100 — vẫn thấy ảnh chụp lúc bắt đầu, commit của giao dịch khác vô hình.

Đo thật: lỗi serialize và cái giá thử lại

Khi hai giao dịch Repeatable Read cùng UPDATE một hàng, PostgreSQL không cho phép cả hai thành công nếu chúng dựa trên cùng ảnh chụp cũ. Đo thật: A và B (đều Repeatable Read) cùng cập nhật một hàng — B nhận:

ERROR:  could not serialize access due to concurrent update
ROLLBACK

B bị hủy hoàn toàn và phải bắt lỗi SQLSTATE 40001 để chạy lại giao dịch từ đầu. Đây là điểm mấu chốt: mức cô lập cao đánh đổi tính đúng đắn tự động lấy nghĩa vụ retry của ứng dụng. Read Committed hiếm khi gặp lỗi này (nó chờ và đọc lại giá trị mới); Repeatable Read và Serializable thì thường xuyên hơn dưới tranh chấp.

Đo thật: chi phí hiệu năng

Chạy pgbench -N (tải ghi) với 8 client ở ba mức:

  • Read Committed: 11.021 TPS (mốc).
  • Repeatable Read: 10.389 TPS — chậm hơn ~6%.
  • Serializable: 8.683 TPS — chậm hơn ~21%.

Đáng chú ý: 0 giao dịch thất bại ở cả ba, vì pgbench cập nhật ngẫu nhiên trong 5 triệu tài khoản nên tranh chấp rất thấp. Nghĩa là ~21% chậm hơn của Serializable ở đây hoàn toàn là chi phí theo dõi phụ thuộc của SSI (predicate lock), chưa tính retry. Dưới tải tranh chấp cao (nhiều giao dịch đụng cùng hàng), Serializable còn tốn thêm chi phí abort-và-thử-lại, nên khoảng cách có thể lớn hơn nhiều.

Đánh đổi cần cân nhắc

Read Committed đủ cho hầu hết ứng dụng web. Đừng nâng mức cô lập theo phản xạ. Phần lớn thao tác CRUD không cần đọc nhất quán xuyên nhiều câu lệnh trong một giao dịch. Chỉ nâng khi có nhu cầu đúng đắn cụ thể (báo cáo đọc nhiều bảng cần nhất quán → Repeatable Read; logic nghiệp vụ dễ dính write skew → Serializable).

Write skew là lý do thật để dùng Serializable. Repeatable Read ngăn non-repeatable read nhưng không bắt write skew — hai giao dịch mỗi cái đọc một tập, kiểm điều kiện, rồi ghi, dẫn tới vi phạm bất biến mà từng giao dịch riêng lẻ nhìn thì hợp lệ (ví dụ kinh điển: hai bác sĩ cùng xin nghỉ khi ràng buộc "luôn phải có ít nhất một bác sĩ trực"). Chỉ Serializable bắt được. Nếu có bất biến kiểu này, hoặc dùng Serializable, hoặc dùng khóa tường minh (SELECT ... FOR UPDATE).

Mọi ứng dụng dùng Repeatable Read/Serializable BẮT BUỘC có retry. Không có vòng lặp retry bắt 40001 thì lỗi serialize sẽ nổi lên tận người dùng cuối. Retry phải chạy lại toàn bộ giao dịch (không chỉ câu lệnh cuối), vì cả giao dịch đã rollback. Đây là chi phí kỹ thuật thật của mức cô lập cao.

Ba ý mang về

  1. Ba mức cô lập khác nhau ở những gì giao dịch "thấy": đo thật, Read Committed đọc lại thấy giá trị mới (100→200, non-repeatable), Repeatable Read giữ ảnh chụp nhất quán (100→100), Serializable đảm bảo như chạy tuần tự (bắt cả write skew).
  2. Mức cao cần logic thử lại: đo thật, hai giao dịch Repeatable Read ghi đụng nhau → ERROR: could not serialize access (SQLSTATE 40001), phải bắt và chạy lại toàn bộ giao dịch — không có retry thì lỗi nổi lên người dùng.
  3. Mức cao trả giá hiệu năng: đo thật pgbench, Serializable chậm hơn ~21% so với Read Committed chỉ do chi phí theo dõi SSI (tranh chấp thấp, 0 retry); dưới tải cao còn tốn thêm retry — nên giữ Read Committed trừ khi có nhu cầu đúng đắn cụ thể.

Phần sau ta xét một tác dụng phụ nguy hiểm của giao dịch mở lâu — liên quan trực tiếp tới snapshot vừa học: Phần sau đo vì sao một giao dịch dài giữ snapshot ngăn VACUUM dọn dead tuple, khiến bảng phình bloat dù bạn không ghi gì thêm.