Suốt mười một bài, loạt "PostgreSQL nội bộ và đồng thời" này theo đuổi một cách làm: mở nhiều phiên psql chạy song song trên pg-lab và quan sát hành vi thật — một giao dịch thấy gì khi giao dịch khác đang chạy, ai chờ ai, dữ liệu phình ra sao, WAL sinh bao nhiêu. Không lý thuyết suông, chỉ output thật. Bài cuối này nối tất cả thành khung tư duy để bạn chọn đúng công cụ và tránh đúng cạm bẫy khi viết code backend chạm database đồng thời.
Toàn bộ số liệu đã đo
Mọi con số và hành vi dưới đây đều quan sát thật trên pg-lab (PostgreSQL 16.15):

Hình 1: Mười một phép đo thật trong pg-lab. Mỗi dòng là một cơ chế đồng thời và bằng chứng quan sát được — nền cho mọi kết luận của loạt bài.
Khung quyết định: cần đảm bảo gì thì dùng gì
Phần lớn bug đồng thời đến từ việc chọn sai công cụ cho yêu cầu đúng đắn. Dưới đây là ánh xạ rút ra từ 11 bài:

Hình 2: Checklist ba phần — ánh xạ nhu cầu sang công cụ, cạm bẫy đồng thời cần tránh, và nguyên tắc vận hành. Mọi thứ bắt nguồn từ MVCC.
- Đọc-rồi-ghi đơn giản (counter, tồn kho) → UPDATE nguyên tử (
SET x = x + 1), không lo lost update, không cần retry (bài 3). - Logic phức tạp tính ở app → SELECT FOR UPDATE khóa đúng hàng (bài 5).
- Báo cáo cần ảnh chụp nhất quán → REPEATABLE READ (bài 2-3).
- Ràng buộc phức tạp (write skew) → SERIALIZABLE + retry, hoặc khóa/CHECK đúng (bài 4).
- Hàng đợi công việc, nhiều worker → FOR UPDATE SKIP LOCKED (bài 11).
- Chỉ một tiến trình chạy tác vụ → advisory lock (bài 11).
Cạm bẫy đồng thời đã đo — nhớ tránh
Loạt bài đã tạo ra và quan sát các bug đồng thời điển hình, gom lại để tra nhanh:
- Read Committed có lost update âm thầm (bài 3): hai giao dịch SELECT-rồi-UPDATE song song làm mất một cập nhật mà không báo lỗi (110 thay vì 120). Dùng UPDATE nguyên tử hoặc FOR UPDATE.
- Write skew mà Repeatable Read không bắt được (bài 4): hai giao dịch ghi hàng khác nhau cùng phá một ràng buộc chung (0 người trực). Cần Serializable hoặc khóa đúng.
- Deadlock khi khóa sai thứ tự (bài 6): hai giao dịch khóa chéo → PostgreSQL hủy một nạn nhân. Tránh bằng cách luôn khóa theo cùng thứ tự (id tăng dần) + bắt lỗi 40P01 để retry.
- Giao dịch mở lâu (bài 1, 7): cản VACUUM dọn, gây bloat. Đóng giao dịch sớm, đừng giữ
BEGINqua một thao tác I/O bên ngoài hay chờ người dùng.
Nguyên tắc vận hành
- Đừng tắt autovacuum (bài 8): nó chống cả bloat lẫn transaction ID wraparound; bảng lớn/update nhiều thì hạ
scale_factorthay vì tắt. - Mọi giao dịch đồng thời phải bọc retry:
serialization_failure(40001) vàdeadlock(40P01) là lỗi thoáng qua, thử lại được — không phải lỗi logic. - Giữ khóa NGẮN, COMMIT sớm (bài 5-6): khóa lâu gây tắc nghẽn và deadlock; dùng
fillfactorthấp cho bảng update nhiều để bật HOT + giảm bloat (bài 9). - WAL là độ bền (bài 10): mặc định (fsync, synchronous_commit on) an toàn nhất; chỉ nới khi hiểu rõ rủi ro mất giao dịch/hỏng dữ liệu.
Mọi thứ bắt nguồn từ MVCC
Nếu chỉ nhớ một điều từ cả loạt: mọi hành vi đồng thời của PostgreSQL đều là hệ quả của MVCC (bài 1). Đọc không chặn ghi vì mỗi hàng có nhiều phiên bản. Các mức cô lập chính là quy tắc chọn phiên bản nào. Lost update và write skew là hệ quả của việc hai giao dịch thấy snapshot khác nhau. Bloat là hệ quả của việc UPDATE để lại phiên bản chết; VACUUM dọn chúng; HOT giảm chi phí tạo chúng. WAL ghi lại mọi phiên bản để bền và phục hồi. Hiểu MVCC là nắm được sợi dây xuyên suốt toàn bộ — từ đó mọi cơ chế khác đều hợp lý.
Đánh đổi cần cân nhắc (cho cả loạt)
Chọn mức đảm bảo thấp nhất đủ dùng, không phải cao nhất. Nâng mọi thứ lên Serializable "cho an toàn" làm chậm (predicate lock) và tăng tỉ lệ retry; dùng Read Committed (mặc định) cho phần lớn truy vấn, chỉ nâng mức/thêm khóa cho đúng những giao dịch có mẫu nguy hiểm. Tương tự, đừng khóa khi UPDATE nguyên tử đủ, đừng thêm index giết HOT nếu không cần. Mỗi mức đảm bảo cao hơn có cái giá — trả đúng chỗ.
Đồng thời là nơi bug ẩn mình — test dưới tải song song, không chỉ test tuần tự. Nhiều bug trong loạt này (lost update, write skew, deadlock) không bao giờ xuất hiện khi test một luồng; chúng chỉ lộ khi hai giao dịch chạy chồng nhau đúng thời điểm. Test đơn luồng xanh không đảm bảo code đúng dưới đồng thời. Khi logic nghiệp vụ nhạy cảm (tiền, tồn kho, trạng thái), hãy test với nhiều kết nối song song, hoặc ít nhất lý luận kỹ về các xen kẽ có thể xảy ra.
PostgreSQL cho công cụ, nhưng tính đúng là trách nhiệm của bạn. MVCC, isolation, khóa, SKIP LOCKED — tất cả là công cụ mạnh, nhưng chọn sai hoặc dùng sai vẫn cho bug. Database không tự biết "ràng buộc nghiệp vụ của bạn là gì"; nó chỉ đảm bảo những gì bạn yêu cầu (qua mức cô lập, khóa, ràng buộc). Hiểu cơ chế (như loạt này) để yêu cầu đúng điều cần — đó là khác biệt giữa code "chạy được lúc test" và code "đúng dưới tải thật".
Ba ý mang về
- Chọn đúng công cụ cho đúng yêu cầu đồng thời. UPDATE nguyên tử cho đọc-rồi-ghi đơn giản; FOR UPDATE cho logic phức tạp; REPEATABLE READ cho ảnh chụp nhất quán; SERIALIZABLE cho ràng buộc phức tạp (write skew); SKIP LOCKED cho hàng đợi; advisory lock cho "chỉ 1 tiến trình". Chọn mức đảm bảo thấp nhất đủ dùng.
- Biết và tránh các cạm bẫy đã đo. Lost update âm thầm ở Read Committed, write skew RR không bắt, deadlock do khóa sai thứ tự, bloat do giao dịch mở lâu. Mọi giao dịch đồng thời phải bọc retry cho 40001/40P01. Test dưới tải song song, không chỉ tuần tự.
- Mọi thứ bắt nguồn từ MVCC. Đọc không chặn ghi, các mức cô lập, anomaly, bloat/vacuum, HOT, WAL — tất cả là hệ quả của việc PostgreSQL giữ nhiều phiên bản mỗi hàng. Hiểu MVCC là hiểu toàn bộ hành vi đồng thời; đừng tắt autovacuum, giữ khóa/giao dịch ngắn, để database tự lo đúng cách.
Nguồn
- PostgreSQL docs — Concurrency Control (toàn chương): https://www.postgresql.org/docs/current/mvcc.html
- PostgreSQL docs — Transaction Isolation & Explicit Locking: https://www.postgresql.org/docs/current/transaction-iso.html
- PostgreSQL docs — Routine Vacuuming & WAL: https://www.postgresql.org/docs/current/maintenance.html